@ccp-nc/crystvis-js 0.7.1 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/render.js +41 -15
- package/package.json +1 -1
- package/test/render.js +168 -0
- package/test/setup-dom.cjs +65 -3
package/lib/render.js
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
|
|
28
28
|
// Ratio of secondary directional light intensity to primary
|
|
29
29
|
const SECONDARY_LIGHT_INTENSITY_RATIO = 0.4;
|
|
30
|
+
const DRAG_THRESHOLD_SQ = 25;
|
|
30
31
|
|
|
31
32
|
// themes:
|
|
32
33
|
const themes = {
|
|
@@ -116,8 +117,34 @@ class Renderer {
|
|
|
116
117
|
|
|
117
118
|
// Raycast for clicks
|
|
118
119
|
this._rcastlist = [];
|
|
119
|
-
this.
|
|
120
|
-
this.
|
|
120
|
+
this._raycaster = new THREE.Raycaster();
|
|
121
|
+
this._ndcVector = new THREE.Vector2();
|
|
122
|
+
this._clickDownX = 0;
|
|
123
|
+
this._clickDownY = 0;
|
|
124
|
+
this._pointerDownValid = false;
|
|
125
|
+
this._boundClickDown = (e) => {
|
|
126
|
+
if (!e.isPrimary) return;
|
|
127
|
+
this._clickDownX = e.clientX;
|
|
128
|
+
this._clickDownY = e.clientY;
|
|
129
|
+
this._pointerDownValid = true;
|
|
130
|
+
};
|
|
131
|
+
this._boundClickUp = (e) => {
|
|
132
|
+
if (!e.isPrimary) return;
|
|
133
|
+
if (!this._pointerDownValid) return;
|
|
134
|
+
this._pointerDownValid = false;
|
|
135
|
+
const dx = e.clientX - this._clickDownX;
|
|
136
|
+
const dy = e.clientY - this._clickDownY;
|
|
137
|
+
if (dx * dx + dy * dy <= DRAG_THRESHOLD_SQ) {
|
|
138
|
+
this._raycastClick(e);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
this._boundClickCancel = (e) => {
|
|
142
|
+
if (!e.isPrimary) return;
|
|
143
|
+
this._pointerDownValid = false;
|
|
144
|
+
};
|
|
145
|
+
this._r.domElement.addEventListener('pointerdown', this._boundClickDown);
|
|
146
|
+
this._r.domElement.addEventListener('pointerup', this._boundClickUp);
|
|
147
|
+
this._r.domElement.addEventListener('pointercancel', this._boundClickCancel);
|
|
121
148
|
|
|
122
149
|
// Groups
|
|
123
150
|
this._groups = {
|
|
@@ -170,7 +197,6 @@ class Renderer {
|
|
|
170
197
|
_updateSize() {
|
|
171
198
|
this._w = this._w || this._div.innerWidth();
|
|
172
199
|
this._h = this._h || this._div.innerHeight();
|
|
173
|
-
this._offset = this._div.offset();
|
|
174
200
|
}
|
|
175
201
|
|
|
176
202
|
_resize() {
|
|
@@ -191,11 +217,10 @@ class Renderer {
|
|
|
191
217
|
// We create a 2D vector
|
|
192
218
|
var vector = this.documentToWorld(e.clientX, e.clientY);
|
|
193
219
|
|
|
194
|
-
//
|
|
195
|
-
var raycaster = new THREE.Raycaster();
|
|
220
|
+
// Reuse the shared raycaster instance
|
|
196
221
|
// We apply two parameters to the 'laser', its origin (where the user clicked)
|
|
197
222
|
// and the direction (what the camera 'sees')
|
|
198
|
-
|
|
223
|
+
this._raycaster.setFromCamera(vector, this._c);
|
|
199
224
|
|
|
200
225
|
// We get all the objects the 'laser' find on its way (it returns an array containing the objects)
|
|
201
226
|
for (var i = 0; i < this._rcastlist.length; ++i) {
|
|
@@ -205,7 +230,7 @@ class Renderer {
|
|
|
205
230
|
var filter = this._rcastlist[i][2];
|
|
206
231
|
|
|
207
232
|
targ = targ || this._s;
|
|
208
|
-
var intersects =
|
|
233
|
+
var intersects = this._raycaster.intersectObjects(targ.children);
|
|
209
234
|
|
|
210
235
|
var objects = [];
|
|
211
236
|
for (var j = 0; j < intersects.length; ++j) {
|
|
@@ -349,8 +374,10 @@ class Renderer {
|
|
|
349
374
|
this._animFrameId = null;
|
|
350
375
|
}
|
|
351
376
|
|
|
352
|
-
// 2. Remove the raycaster's own
|
|
353
|
-
this._r.domElement.removeEventListener('pointerdown', this.
|
|
377
|
+
// 2. Remove the raycaster's own pointer listeners
|
|
378
|
+
this._r.domElement.removeEventListener('pointerdown', this._boundClickDown);
|
|
379
|
+
this._r.domElement.removeEventListener('pointerup', this._boundClickUp);
|
|
380
|
+
this._r.domElement.removeEventListener('pointercancel', this._boundClickCancel);
|
|
354
381
|
this._rcastlist = [];
|
|
355
382
|
this._sboxlist = [];
|
|
356
383
|
|
|
@@ -525,15 +552,14 @@ class Renderer {
|
|
|
525
552
|
* Convert coordinates in the dom element frame to coordinates in the world frame
|
|
526
553
|
*/
|
|
527
554
|
documentToWorld(x, y) {
|
|
528
|
-
|
|
529
|
-
var vector = new THREE.Vector2();
|
|
555
|
+
const rect = this._r.domElement.getBoundingClientRect();
|
|
530
556
|
// We set its position where the user clicked and we convert it to a number between -1 & 1
|
|
531
|
-
|
|
532
|
-
2 * ((x -
|
|
533
|
-
1 - 2 * ((y -
|
|
557
|
+
this._ndcVector.set(
|
|
558
|
+
2 * ((x - rect.left) / rect.width) - 1,
|
|
559
|
+
1 - 2 * ((y - rect.top) / rect.height)
|
|
534
560
|
);
|
|
535
561
|
|
|
536
|
-
return
|
|
562
|
+
return this._ndcVector;
|
|
537
563
|
}
|
|
538
564
|
|
|
539
565
|
// Style
|
package/package.json
CHANGED
package/test/render.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import * as chai from 'chai';
|
|
4
|
+
import { Renderer } from '../lib/render.js';
|
|
5
|
+
|
|
6
|
+
const expect = chai.expect;
|
|
7
|
+
|
|
8
|
+
function dispatchPointerEvent(target, type, x, y, { isPrimary = true } = {}) {
|
|
9
|
+
const event = new window.MouseEvent(type, {
|
|
10
|
+
bubbles: true,
|
|
11
|
+
clientX: x,
|
|
12
|
+
clientY: y,
|
|
13
|
+
});
|
|
14
|
+
Object.defineProperty(event, 'isPrimary', { value: isPrimary });
|
|
15
|
+
target.dispatchEvent(event);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('Renderer click drag guard', function () {
|
|
19
|
+
let renderer;
|
|
20
|
+
let originalAnimate;
|
|
21
|
+
|
|
22
|
+
before(function () {
|
|
23
|
+
originalAnimate = Renderer.prototype._animate;
|
|
24
|
+
Renderer.prototype._animate = function () {};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
after(function () {
|
|
28
|
+
Renderer.prototype._animate = originalAnimate;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
beforeEach(function () {
|
|
32
|
+
const host = document.getElementById('crystvis');
|
|
33
|
+
host.innerHTML = '';
|
|
34
|
+
renderer = new Renderer('#crystvis', 320, 240);
|
|
35
|
+
renderer._r.dispose = () => {};
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(function () {
|
|
39
|
+
if (renderer && renderer._r) {
|
|
40
|
+
renderer.dispose();
|
|
41
|
+
}
|
|
42
|
+
renderer = null;
|
|
43
|
+
const host = document.getElementById('crystvis');
|
|
44
|
+
host.innerHTML = '';
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should raycast on pointerup when the pointer stays within the drag threshold', function () {
|
|
48
|
+
let raycastCalls = 0;
|
|
49
|
+
renderer._raycastClick = () => {
|
|
50
|
+
raycastCalls++;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
dispatchPointerEvent(renderer._r.domElement, 'pointerdown', 100, 100);
|
|
54
|
+
dispatchPointerEvent(renderer._r.domElement, 'pointerup', 103, 104);
|
|
55
|
+
|
|
56
|
+
expect(raycastCalls).to.equal(1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should not raycast on pointerup after a drag larger than the threshold', function () {
|
|
60
|
+
let raycastCalls = 0;
|
|
61
|
+
renderer._raycastClick = () => {
|
|
62
|
+
raycastCalls++;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
dispatchPointerEvent(renderer._r.domElement, 'pointerdown', 100, 100);
|
|
66
|
+
dispatchPointerEvent(renderer._r.domElement, 'pointerup', 110, 100);
|
|
67
|
+
|
|
68
|
+
expect(raycastCalls).to.equal(0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should remove pointer listeners during dispose', function () {
|
|
72
|
+
let raycastCalls = 0;
|
|
73
|
+
const canvas = renderer._r.domElement;
|
|
74
|
+
renderer._raycastClick = () => {
|
|
75
|
+
raycastCalls++;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
renderer.dispose();
|
|
79
|
+
renderer = null;
|
|
80
|
+
|
|
81
|
+
dispatchPointerEvent(canvas, 'pointerdown', 100, 100);
|
|
82
|
+
dispatchPointerEvent(canvas, 'pointerup', 100, 100);
|
|
83
|
+
|
|
84
|
+
expect(raycastCalls).to.equal(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should ignore non-primary pointer events', function () {
|
|
88
|
+
let raycastCalls = 0;
|
|
89
|
+
renderer._raycastClick = () => {
|
|
90
|
+
raycastCalls++;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
dispatchPointerEvent(renderer._r.domElement, 'pointerdown', 100, 100, { isPrimary: false });
|
|
94
|
+
dispatchPointerEvent(renderer._r.domElement, 'pointerup', 100, 100, { isPrimary: false });
|
|
95
|
+
|
|
96
|
+
expect(raycastCalls).to.equal(0);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should not raycast after pointercancel', function () {
|
|
100
|
+
let raycastCalls = 0;
|
|
101
|
+
renderer._raycastClick = () => {
|
|
102
|
+
raycastCalls++;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
dispatchPointerEvent(renderer._r.domElement, 'pointerdown', 100, 100);
|
|
106
|
+
dispatchPointerEvent(renderer._r.domElement, 'pointercancel', 100, 100);
|
|
107
|
+
dispatchPointerEvent(renderer._r.domElement, 'pointerup', 100, 100);
|
|
108
|
+
|
|
109
|
+
expect(raycastCalls).to.equal(0);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should convert document coordinates using the canvas bounding rect', function () {
|
|
113
|
+
renderer._r.domElement.getBoundingClientRect = () => ({
|
|
114
|
+
left: 40,
|
|
115
|
+
top: 20,
|
|
116
|
+
width: 200,
|
|
117
|
+
height: 100,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const vector = renderer.documentToWorld(140, 70);
|
|
121
|
+
|
|
122
|
+
expect(vector.x).to.equal(0);
|
|
123
|
+
expect(vector.y).to.equal(0);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should reuse the shared NDC vector', function () {
|
|
127
|
+
renderer._r.domElement.getBoundingClientRect = () => ({
|
|
128
|
+
left: 0,
|
|
129
|
+
top: 0,
|
|
130
|
+
width: 320,
|
|
131
|
+
height: 240,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const first = renderer.documentToWorld(160, 120);
|
|
135
|
+
const second = renderer.documentToWorld(80, 60);
|
|
136
|
+
|
|
137
|
+
expect(second).to.equal(first);
|
|
138
|
+
expect(second.x).to.equal(-0.5);
|
|
139
|
+
expect(second.y).to.equal(0.5);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should reuse the shared raycaster instance', function () {
|
|
143
|
+
let setFromCameraCalls = 0;
|
|
144
|
+
const sharedRaycaster = {
|
|
145
|
+
setFromCamera(vector, camera) {
|
|
146
|
+
setFromCameraCalls++;
|
|
147
|
+
expect(camera).to.equal(renderer._c);
|
|
148
|
+
expect(vector.x).to.equal(0);
|
|
149
|
+
expect(vector.y).to.equal(0);
|
|
150
|
+
},
|
|
151
|
+
intersectObjects() {
|
|
152
|
+
return [];
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
renderer._r.domElement.getBoundingClientRect = () => ({
|
|
157
|
+
left: 0,
|
|
158
|
+
top: 0,
|
|
159
|
+
width: 320,
|
|
160
|
+
height: 240,
|
|
161
|
+
});
|
|
162
|
+
renderer._raycaster = sharedRaycaster;
|
|
163
|
+
|
|
164
|
+
renderer._raycastClick({ clientX: 160, clientY: 120 });
|
|
165
|
+
|
|
166
|
+
expect(setFromCameraCalls).to.equal(1);
|
|
167
|
+
});
|
|
168
|
+
});
|
package/test/setup-dom.cjs
CHANGED
|
@@ -31,6 +31,8 @@ setGlobal('document', dom.window.document);
|
|
|
31
31
|
setGlobal('navigator', dom.window.navigator);
|
|
32
32
|
setGlobal('HTMLElement', dom.window.HTMLElement);
|
|
33
33
|
setGlobal('HTMLCanvasElement', dom.window.HTMLCanvasElement);
|
|
34
|
+
setGlobal('AbortController', dom.window.AbortController);
|
|
35
|
+
setGlobal('AbortSignal', dom.window.AbortSignal);
|
|
34
36
|
setGlobal('Image', dom.window.Image);
|
|
35
37
|
setGlobal('ImageData', dom.window.ImageData);
|
|
36
38
|
setGlobal('XMLHttpRequest', dom.window.XMLHttpRequest);
|
|
@@ -48,19 +50,73 @@ const _origGetContext = canvasProto.getContext;
|
|
|
48
50
|
canvasProto.getContext = function (type, ...args) {
|
|
49
51
|
if (type === 'webgl' || type === 'webgl2' || type === 'experimental-webgl') {
|
|
50
52
|
// Return a minimal WebGL stub that THREE can interrogate without causing
|
|
51
|
-
// errors.
|
|
53
|
+
// errors. Only the methods referenced during renderer initialisation
|
|
52
54
|
// need to be present.
|
|
53
|
-
|
|
55
|
+
const gl = {
|
|
56
|
+
VERSION: 0x1F02,
|
|
57
|
+
VENDOR: 0x1F00,
|
|
58
|
+
RENDERER: 0x1F01,
|
|
59
|
+
SHADING_LANGUAGE_VERSION: 0x8B8C,
|
|
60
|
+
MAX_TEXTURE_IMAGE_UNITS: 0x8872,
|
|
61
|
+
MAX_VERTEX_TEXTURE_IMAGE_UNITS: 0x8B4C,
|
|
62
|
+
MAX_TEXTURE_SIZE: 0x0D33,
|
|
63
|
+
MAX_CUBE_MAP_TEXTURE_SIZE: 0x851C,
|
|
64
|
+
MAX_VERTEX_ATTRIBS: 0x8869,
|
|
65
|
+
MAX_VERTEX_UNIFORM_VECTORS: 0x8DFB,
|
|
66
|
+
MAX_VARYING_VECTORS: 0x8DFC,
|
|
67
|
+
MAX_FRAGMENT_UNIFORM_VECTORS: 0x8DFD,
|
|
68
|
+
MAX_SAMPLES: 0x8D57,
|
|
69
|
+
FRAMEBUFFER: 0x8D40,
|
|
70
|
+
FRAMEBUFFER_COMPLETE: 0x8CD5,
|
|
54
71
|
canvas: this,
|
|
55
72
|
drawingBufferWidth: 300,
|
|
56
73
|
drawingBufferHeight: 150,
|
|
74
|
+
getContextAttributes: () => ({
|
|
75
|
+
alpha: true,
|
|
76
|
+
depth: true,
|
|
77
|
+
stencil: true,
|
|
78
|
+
antialias: false,
|
|
79
|
+
premultipliedAlpha: true,
|
|
80
|
+
preserveDrawingBuffer: false,
|
|
81
|
+
powerPreference: 'default',
|
|
82
|
+
failIfMajorPerformanceCaveat: false,
|
|
83
|
+
}),
|
|
84
|
+
getSupportedExtensions: () => [],
|
|
57
85
|
getExtension: () => null,
|
|
58
|
-
getParameter: () =>
|
|
86
|
+
getParameter: (pname) => {
|
|
87
|
+
switch (pname) {
|
|
88
|
+
case gl.VERSION:
|
|
89
|
+
return 'WebGL 1.0';
|
|
90
|
+
case gl.VENDOR:
|
|
91
|
+
return 'jsdom';
|
|
92
|
+
case gl.RENDERER:
|
|
93
|
+
return 'jsdom WebGL stub';
|
|
94
|
+
case gl.SHADING_LANGUAGE_VERSION:
|
|
95
|
+
return 'WebGL GLSL ES 1.0';
|
|
96
|
+
case gl.MAX_TEXTURE_IMAGE_UNITS:
|
|
97
|
+
case gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS:
|
|
98
|
+
return 16;
|
|
99
|
+
case gl.MAX_TEXTURE_SIZE:
|
|
100
|
+
case gl.MAX_CUBE_MAP_TEXTURE_SIZE:
|
|
101
|
+
return 4096;
|
|
102
|
+
case gl.MAX_VERTEX_ATTRIBS:
|
|
103
|
+
return 16;
|
|
104
|
+
case gl.MAX_VERTEX_UNIFORM_VECTORS:
|
|
105
|
+
case gl.MAX_VARYING_VECTORS:
|
|
106
|
+
case gl.MAX_FRAGMENT_UNIFORM_VECTORS:
|
|
107
|
+
return 1024;
|
|
108
|
+
case gl.MAX_SAMPLES:
|
|
109
|
+
return 4;
|
|
110
|
+
default:
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
},
|
|
59
114
|
getShaderPrecisionFormat: () => ({ rangeMin: 127, rangeMax: 127, precision: 23 }),
|
|
60
115
|
createTexture: () => ({}),
|
|
61
116
|
bindTexture: () => {},
|
|
62
117
|
texParameteri: () => {},
|
|
63
118
|
pixelStorei: () => {},
|
|
119
|
+
activeTexture: () => {},
|
|
64
120
|
enable: () => {},
|
|
65
121
|
disable: () => {},
|
|
66
122
|
blendEquation: () => {},
|
|
@@ -93,14 +149,18 @@ canvasProto.getContext = function (type, ...args) {
|
|
|
93
149
|
compileShader: () => {},
|
|
94
150
|
attachShader: () => {},
|
|
95
151
|
linkProgram: () => {},
|
|
152
|
+
getProgramInfoLog: () => '',
|
|
96
153
|
getProgramParameter: (p, pname) => {
|
|
97
154
|
// LINK_STATUS
|
|
98
155
|
if (pname === 35714) return true;
|
|
99
156
|
return null;
|
|
100
157
|
},
|
|
101
158
|
getShaderParameter: () => true,
|
|
159
|
+
getShaderInfoLog: () => '',
|
|
102
160
|
getUniformLocation: () => ({}),
|
|
103
161
|
getAttribLocation: () => 0,
|
|
162
|
+
getActiveAttrib: () => null,
|
|
163
|
+
getActiveUniform: () => null,
|
|
104
164
|
uniform1i: () => {},
|
|
105
165
|
uniform1f: () => {},
|
|
106
166
|
uniform2f: () => {},
|
|
@@ -132,6 +192,7 @@ canvasProto.getContext = function (type, ...args) {
|
|
|
132
192
|
blitFramebuffer: () => {},
|
|
133
193
|
readBuffer: () => {},
|
|
134
194
|
drawBuffers: () => {},
|
|
195
|
+
checkFramebufferStatus: () => gl.FRAMEBUFFER_COMPLETE,
|
|
135
196
|
renderbufferStorageMultisample: () => {},
|
|
136
197
|
createSampler: () => ({}),
|
|
137
198
|
deleteSampler: () => {},
|
|
@@ -139,6 +200,7 @@ canvasProto.getContext = function (type, ...args) {
|
|
|
139
200
|
samplerParameteri: () => {},
|
|
140
201
|
samplerParameterf: () => {},
|
|
141
202
|
};
|
|
203
|
+
return gl;
|
|
142
204
|
}
|
|
143
205
|
if (_origGetContext) {
|
|
144
206
|
return _origGetContext.call(this, type, ...args);
|