@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 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._boundRaycastClick = this._raycastClick.bind(this);
120
- this._r.domElement.addEventListener('pointerdown', this._boundRaycastClick);
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
- // We create a raycaster, which is some kind of laser going through your scene
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
- raycaster.setFromCamera(vector, this._c);
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 = raycaster.intersectObjects(targ.children);
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 pointerdown listener
353
- this._r.domElement.removeEventListener('pointerdown', this._boundRaycastClick);
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
- // We create a 2D vector
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
- vector.set(
532
- 2 * ((x - this._offset.left) / this._w) - 1,
533
- 1 - 2 * ((y - this._offset.top) / this._h)
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 vector;
562
+ return this._ndcVector;
537
563
  }
538
564
 
539
565
  // Style
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ccp-nc/crystvis-js",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "description": "A Three.js based crystallographic visualisation tool",
5
5
  "main": "index.js",
6
6
  "type": "module",
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
+ });
@@ -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. Only the methods referenced during renderer initialisation
53
+ // errors. Only the methods referenced during renderer initialisation
52
54
  // need to be present.
53
- return {
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: () => null,
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);