@es3d/es3d 1.0.0 → 1.0.1
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/LICENSE +201 -201
- package/README.md +86 -86
- package/docs/SPEC.md +417 -417
- package/package.json +1 -1
- package/scripts/es3d-cli.mjs +377 -377
- package/src/cache/geometry-store.js +392 -392
- package/src/cache/mesh-bundle.js +123 -123
- package/src/cache/sha256.js +73 -73
- package/src/format/ast.js +238 -238
- package/src/format/colors.js +93 -93
- package/src/format/compact-parser.js +171 -171
- package/src/format/compact-tokenizer.js +165 -165
- package/src/format/errors.js +247 -247
- package/src/format/json.js +55 -55
- package/src/format/limits.js +14 -14
- package/src/format/parse.js +92 -92
- package/src/format/schema.js +617 -617
- package/src/format/serializer.js +333 -333
- package/src/format/validator.js +330 -330
- package/src/format/value-error.js +12 -12
- package/src/format/value-format.js +314 -314
- package/src/format/values.js +459 -459
- package/src/format/xml-parser.js +136 -136
- package/src/format/xml-tokenizer.js +237 -237
- package/src/geometry/contour.js +86 -86
- package/src/geometry/extrude.js +175 -175
- package/src/geometry/gear.js +85 -85
- package/src/geometry/geometry-cache.js +159 -159
- package/src/geometry/lod-error.js +178 -178
- package/src/geometry/mesh.js +458 -458
- package/src/geometry/primitives.js +299 -299
- package/src/geometry/registry.js +645 -645
- package/src/geometry/revolve.js +136 -136
- package/src/geometry/sweep.js +441 -441
- package/src/index.js +47 -47
- package/src/io/compression.js +67 -67
- package/src/io/convert.js +236 -236
- package/src/io/download.js +13 -13
- package/src/io/external-scene.js +127 -127
- package/src/io/file-loader.js +28 -28
- package/src/io/gltf.js +630 -630
- package/src/io/mesh-import.js +104 -104
- package/src/io/obj.js +352 -352
- package/src/io/step-brep.js +439 -439
- package/src/io/step-exact.js +1149 -1149
- package/src/io/step.js +1418 -1418
- package/src/io/stl.js +193 -193
- package/src/io/svg-import.js +787 -787
- package/src/material/materials.js +154 -154
- package/src/math/mat4.js +475 -475
- package/src/math/random.js +30 -30
- package/src/math/trig.js +65 -65
- package/src/math/vec3.js +81 -81
- package/src/optimize/optimizer.js +579 -579
- package/src/path/path-flatten.js +322 -322
- package/src/path/path-parser.js +332 -332
- package/src/path/shapes.js +129 -129
- package/src/path/triangulate.js +517 -517
- package/src/pipeline.js +74 -74
- package/src/render/camera.js +168 -168
- package/src/render/frame-builder.js +362 -362
- package/src/render/geometry-pool.js +149 -149
- package/src/render/lighting.js +149 -149
- package/src/render/orbit-controls.js +237 -237
- package/src/render/renderer.js +258 -249
- package/src/render/scroll-timeline.js +104 -104
- package/src/render/shaders/glsl.js +285 -285
- package/src/render/shaders/wgsl.js +252 -252
- package/src/render/stage.js +364 -352
- package/src/render/texture-cache.js +40 -40
- package/src/render/textures.js +223 -223
- package/src/render/webgl2/webgl2-backend.js +586 -532
- package/src/render/webgpu/webgpu-backend.js +482 -480
- package/src/scene/flatten.js +754 -754
- package/src/scene/picking.js +161 -161
- package/src/scene/scene-graph.js +320 -320
- package/src/transform/transform.js +211 -211
- package/src/wasm/kernels-binary.js +3 -3
- package/src/wasm/kernels.js +385 -385
|
@@ -1,237 +1,237 @@
|
|
|
1
|
-
// Mouse / touch camera controls: orbit (left drag), pan (right or middle drag,
|
|
2
|
-
// or shift + left drag), zoom (wheel or pinch). Works for perspective and
|
|
3
|
-
// orthographic cameras. A document's <controls> element (scene.controls) can
|
|
4
|
-
// limit them and make the camera collide with the scene: see configure().
|
|
5
|
-
|
|
6
|
-
import { constrainCamera } from '../scene/camera-constraints.js';
|
|
7
|
-
|
|
8
|
-
export class OrbitControls {
|
|
9
|
-
/**
|
|
10
|
-
* @param {HTMLElement} element
|
|
11
|
-
* @param {import('./camera.js').Camera} camera
|
|
12
|
-
*/
|
|
13
|
-
constructor(element, camera) {
|
|
14
|
-
this.el = element;
|
|
15
|
-
this.camera = camera;
|
|
16
|
-
this.enabled = true;
|
|
17
|
-
this.enableZoom = true;
|
|
18
|
-
this.enablePan = true;
|
|
19
|
-
this.limits = null; // scene.controls: distance, angle and target limits, collision
|
|
20
|
-
this.world = null; // () => ({scene, meshOf, isVisible}) for collision
|
|
21
|
-
this.rotateSpeed = 0.006;
|
|
22
|
-
this.zoomSpeed = 0.0015;
|
|
23
|
-
this.onChange = null;
|
|
24
|
-
this.onClick = null; // (x, y, event) for picking
|
|
25
|
-
this.onDoubleClick = null;
|
|
26
|
-
this.pointers = new Map();
|
|
27
|
-
this.mode = null;
|
|
28
|
-
this.moved = 0;
|
|
29
|
-
this.last = null;
|
|
30
|
-
this.pinch = null;
|
|
31
|
-
|
|
32
|
-
this._down = (e) => this.pointerDown(e);
|
|
33
|
-
this._move = (e) => this.pointerMove(e);
|
|
34
|
-
this._up = (e) => this.pointerUp(e);
|
|
35
|
-
this._wheel = (e) => this.wheel(e);
|
|
36
|
-
this._ctx = (e) => e.preventDefault();
|
|
37
|
-
this._dbl = (e) => {
|
|
38
|
-
if (this.onDoubleClick) {
|
|
39
|
-
const r = this.el.getBoundingClientRect();
|
|
40
|
-
this.onDoubleClick(e.clientX - r.left, e.clientY - r.top, e);
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
element.addEventListener('pointerdown', this._down);
|
|
44
|
-
element.addEventListener('pointermove', this._move);
|
|
45
|
-
element.addEventListener('pointerup', this._up);
|
|
46
|
-
element.addEventListener('pointercancel', this._up);
|
|
47
|
-
element.addEventListener('wheel', this._wheel, { passive: false });
|
|
48
|
-
element.addEventListener('contextmenu', this._ctx);
|
|
49
|
-
element.addEventListener('dblclick', this._dbl);
|
|
50
|
-
element.style.touchAction = 'none';
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
dispose() {
|
|
54
|
-
const el = this.el;
|
|
55
|
-
el.removeEventListener('pointerdown', this._down);
|
|
56
|
-
el.removeEventListener('pointermove', this._move);
|
|
57
|
-
el.removeEventListener('pointerup', this._up);
|
|
58
|
-
el.removeEventListener('pointercancel', this._up);
|
|
59
|
-
el.removeEventListener('wheel', this._wheel);
|
|
60
|
-
el.removeEventListener('contextmenu', this._ctx);
|
|
61
|
-
el.removeEventListener('dblclick', this._dbl);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Apply a document's <controls> (scene.controls, or null for free orbiting).
|
|
66
|
-
* @param {object|null} spec
|
|
67
|
-
* @param {{world?: () => object, ignoreMode?: boolean}} [opts] world supplies the
|
|
68
|
-
* scene for collision; ignoreMode keeps dragging on (an editor previewing limits).
|
|
69
|
-
*/
|
|
70
|
-
configure(spec, opts = {}) {
|
|
71
|
-
this.limits = spec;
|
|
72
|
-
if (opts.world) this.world = opts.world;
|
|
73
|
-
const mode = spec ? spec.mode : 'orbit';
|
|
74
|
-
this.enabled = opts.ignoreMode || mode === 'orbit';
|
|
75
|
-
this.enableZoom = !spec || spec.zoom !== false;
|
|
76
|
-
this.enablePan = !spec || spec.pan !== false;
|
|
77
|
-
// a canvas that does not orbit must let touch scrolling through to the page
|
|
78
|
-
this.el.style.touchAction = this.enabled ? 'none' : 'auto';
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
changed() {
|
|
82
|
-
if (this.onChange) this.onChange();
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Camera state before a manipulation, for constrain(). */
|
|
86
|
-
snapshot() {
|
|
87
|
-
return { position: this.camera.position.slice(), target: this.camera.target.slice() };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** Enforce limits and collision after a manipulation that started at `prev`. */
|
|
91
|
-
constrain(prev) {
|
|
92
|
-
if (!this.limits) return;
|
|
93
|
-
const world = this.limits.collide && this.world ? this.world() : null;
|
|
94
|
-
constrainCamera(prev, this.camera, this.limits, world);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
pointerDown(e) {
|
|
98
|
-
if (!this.enabled) return;
|
|
99
|
-
this.el.setPointerCapture(e.pointerId);
|
|
100
|
-
this.pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
101
|
-
this.moved = 0;
|
|
102
|
-
if (this.pointers.size === 2) {
|
|
103
|
-
const [a, b] = [...this.pointers.values()];
|
|
104
|
-
this.pinch = { d: Math.hypot(a.x - b.x, a.y - b.y), cx: (a.x + b.x) / 2, cy: (a.y + b.y) / 2 };
|
|
105
|
-
this.mode = 'pinch';
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
if (e.button === 2 || e.button === 1 || e.shiftKey || e.ctrlKey || e.metaKey) this.mode = 'pan';
|
|
109
|
-
else this.mode = 'orbit';
|
|
110
|
-
this.last = { x: e.clientX, y: e.clientY };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
pointerMove(e) {
|
|
114
|
-
if (!this.enabled || !this.pointers.has(e.pointerId)) return;
|
|
115
|
-
this.pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
116
|
-
const prev = this.snapshot();
|
|
117
|
-
if (this.mode === 'pinch' && this.pointers.size >= 2) {
|
|
118
|
-
const [a, b] = [...this.pointers.values()];
|
|
119
|
-
const d = Math.hypot(a.x - b.x, a.y - b.y);
|
|
120
|
-
const cx = (a.x + b.x) / 2, cy = (a.y + b.y) / 2;
|
|
121
|
-
if (this.pinch.d > 0 && this.enableZoom) this.zoom(this.pinch.d / Math.max(d, 1));
|
|
122
|
-
if (this.enablePan) this.pan(cx - this.pinch.cx, cy - this.pinch.cy);
|
|
123
|
-
this.pinch = { d, cx, cy };
|
|
124
|
-
this.moved += 10;
|
|
125
|
-
this.constrain(prev);
|
|
126
|
-
this.changed();
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
if (!this.last) return;
|
|
130
|
-
const dx = e.clientX - this.last.x;
|
|
131
|
-
const dy = e.clientY - this.last.y;
|
|
132
|
-
this.last = { x: e.clientX, y: e.clientY };
|
|
133
|
-
this.moved += Math.abs(dx) + Math.abs(dy);
|
|
134
|
-
if (this.mode === 'orbit') this.orbit(dx, dy);
|
|
135
|
-
else if (this.mode === 'pan' && this.enablePan) this.pan(dx, dy);
|
|
136
|
-
this.constrain(prev);
|
|
137
|
-
this.changed();
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
pointerUp(e) {
|
|
141
|
-
if (!this.pointers.has(e.pointerId)) return;
|
|
142
|
-
this.pointers.delete(e.pointerId);
|
|
143
|
-
try {
|
|
144
|
-
this.el.releasePointerCapture(e.pointerId);
|
|
145
|
-
} catch {
|
|
146
|
-
// pointer capture may already be released
|
|
147
|
-
}
|
|
148
|
-
if (this.pointers.size === 0) {
|
|
149
|
-
if (this.moved < 4 && this.onClick && e.button === 0) {
|
|
150
|
-
const r = this.el.getBoundingClientRect();
|
|
151
|
-
this.onClick(e.clientX - r.left, e.clientY - r.top, e);
|
|
152
|
-
}
|
|
153
|
-
this.mode = null;
|
|
154
|
-
this.last = null;
|
|
155
|
-
this.pinch = null;
|
|
156
|
-
} else if (this.pointers.size === 1) {
|
|
157
|
-
const p = [...this.pointers.values()][0];
|
|
158
|
-
this.mode = 'orbit';
|
|
159
|
-
this.last = { x: p.x, y: p.y };
|
|
160
|
-
this.pinch = null;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
wheel(e) {
|
|
165
|
-
// without zoom the wheel scrolls the page
|
|
166
|
-
if (!this.enabled || !this.enableZoom) return;
|
|
167
|
-
e.preventDefault();
|
|
168
|
-
const delta = e.deltaMode === 1 ? e.deltaY * 33 : e.deltaMode === 2 ? e.deltaY * 400 : e.deltaY;
|
|
169
|
-
const prev = this.snapshot();
|
|
170
|
-
this.zoom(Math.exp(delta * this.zoomSpeed));
|
|
171
|
-
this.constrain(prev);
|
|
172
|
-
this.changed();
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
orbit(dx, dy) {
|
|
176
|
-
const c = this.camera;
|
|
177
|
-
const off = [c.position[0] - c.target[0], c.position[1] - c.target[1], c.position[2] - c.target[2]];
|
|
178
|
-
const r = Math.hypot(off[0], off[1], off[2]) || 1e-6;
|
|
179
|
-
let theta = Math.atan2(off[0], off[2]);
|
|
180
|
-
let phi = Math.acos(Math.max(-1, Math.min(1, off[1] / r)));
|
|
181
|
-
theta -= dx * this.rotateSpeed;
|
|
182
|
-
phi -= dy * this.rotateSpeed;
|
|
183
|
-
phi = Math.max(0.001, Math.min(Math.PI - 0.001, phi));
|
|
184
|
-
c.position = [
|
|
185
|
-
c.target[0] + r * Math.sin(phi) * Math.sin(theta),
|
|
186
|
-
c.target[1] + r * Math.cos(phi),
|
|
187
|
-
c.target[2] + r * Math.sin(phi) * Math.cos(theta),
|
|
188
|
-
];
|
|
189
|
-
c.up = [0, 1, 0];
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
pan(dx, dy) {
|
|
193
|
-
const c = this.camera;
|
|
194
|
-
const h = this.el.clientHeight || 1;
|
|
195
|
-
const dist = c.distance;
|
|
196
|
-
const unitsPerPixel = c.type === 'orthographic' ? c.orthoHeight / h : (2 * dist * Math.tan(((c.fov / 2) * Math.PI) / 180)) / h;
|
|
197
|
-
// camera basis
|
|
198
|
-
const f = [c.target[0] - c.position[0], c.target[1] - c.position[1], c.target[2] - c.position[2]];
|
|
199
|
-
const fl = Math.hypot(...f) || 1;
|
|
200
|
-
f[0] /= fl; f[1] /= fl; f[2] /= fl;
|
|
201
|
-
let right = [f[1] * c.up[2] - f[2] * c.up[1], f[2] * c.up[0] - f[0] * c.up[2], f[0] * c.up[1] - f[1] * c.up[0]];
|
|
202
|
-
const rl = Math.hypot(...right) || 1;
|
|
203
|
-
right = right.map((v) => v / rl);
|
|
204
|
-
const up = [right[1] * f[2] - right[2] * f[1], right[2] * f[0] - right[0] * f[2], right[0] * f[1] - right[1] * f[0]];
|
|
205
|
-
const mx = -dx * unitsPerPixel, my = dy * unitsPerPixel;
|
|
206
|
-
for (let k = 0; k < 3; k++) {
|
|
207
|
-
const d = right[k] * mx + up[k] * my;
|
|
208
|
-
c.position[k] += d;
|
|
209
|
-
c.target[k] += d;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
zoom(factor) {
|
|
214
|
-
const c = this.camera;
|
|
215
|
-
if (c.type === 'orthographic') {
|
|
216
|
-
c.orthoHeight = Math.max(1e-4, c.orthoHeight * factor);
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
const off = [c.position[0] - c.target[0], c.position[1] - c.target[1], c.position[2] - c.target[2]];
|
|
220
|
-
const r = Math.hypot(...off);
|
|
221
|
-
const nr = Math.max(1e-4, r * factor);
|
|
222
|
-
for (let k = 0; k < 3; k++) c.position[k] = c.target[k] + (off[k] / (r || 1)) * nr;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/** Move the orbit centre to a point, keeping the viewing direction. */
|
|
226
|
-
focus(point) {
|
|
227
|
-
const c = this.camera;
|
|
228
|
-
const prev = this.snapshot();
|
|
229
|
-
const d = [point[0] - c.target[0], point[1] - c.target[1], point[2] - c.target[2]];
|
|
230
|
-
for (let k = 0; k < 3; k++) {
|
|
231
|
-
c.target[k] += d[k];
|
|
232
|
-
c.position[k] += d[k];
|
|
233
|
-
}
|
|
234
|
-
this.constrain(prev);
|
|
235
|
-
this.changed();
|
|
236
|
-
}
|
|
237
|
-
}
|
|
1
|
+
// Mouse / touch camera controls: orbit (left drag), pan (right or middle drag,
|
|
2
|
+
// or shift + left drag), zoom (wheel or pinch). Works for perspective and
|
|
3
|
+
// orthographic cameras. A document's <controls> element (scene.controls) can
|
|
4
|
+
// limit them and make the camera collide with the scene: see configure().
|
|
5
|
+
|
|
6
|
+
import { constrainCamera } from '../scene/camera-constraints.js';
|
|
7
|
+
|
|
8
|
+
export class OrbitControls {
|
|
9
|
+
/**
|
|
10
|
+
* @param {HTMLElement} element
|
|
11
|
+
* @param {import('./camera.js').Camera} camera
|
|
12
|
+
*/
|
|
13
|
+
constructor(element, camera) {
|
|
14
|
+
this.el = element;
|
|
15
|
+
this.camera = camera;
|
|
16
|
+
this.enabled = true;
|
|
17
|
+
this.enableZoom = true;
|
|
18
|
+
this.enablePan = true;
|
|
19
|
+
this.limits = null; // scene.controls: distance, angle and target limits, collision
|
|
20
|
+
this.world = null; // () => ({scene, meshOf, isVisible}) for collision
|
|
21
|
+
this.rotateSpeed = 0.006;
|
|
22
|
+
this.zoomSpeed = 0.0015;
|
|
23
|
+
this.onChange = null;
|
|
24
|
+
this.onClick = null; // (x, y, event) for picking
|
|
25
|
+
this.onDoubleClick = null;
|
|
26
|
+
this.pointers = new Map();
|
|
27
|
+
this.mode = null;
|
|
28
|
+
this.moved = 0;
|
|
29
|
+
this.last = null;
|
|
30
|
+
this.pinch = null;
|
|
31
|
+
|
|
32
|
+
this._down = (e) => this.pointerDown(e);
|
|
33
|
+
this._move = (e) => this.pointerMove(e);
|
|
34
|
+
this._up = (e) => this.pointerUp(e);
|
|
35
|
+
this._wheel = (e) => this.wheel(e);
|
|
36
|
+
this._ctx = (e) => e.preventDefault();
|
|
37
|
+
this._dbl = (e) => {
|
|
38
|
+
if (this.onDoubleClick) {
|
|
39
|
+
const r = this.el.getBoundingClientRect();
|
|
40
|
+
this.onDoubleClick(e.clientX - r.left, e.clientY - r.top, e);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
element.addEventListener('pointerdown', this._down);
|
|
44
|
+
element.addEventListener('pointermove', this._move);
|
|
45
|
+
element.addEventListener('pointerup', this._up);
|
|
46
|
+
element.addEventListener('pointercancel', this._up);
|
|
47
|
+
element.addEventListener('wheel', this._wheel, { passive: false });
|
|
48
|
+
element.addEventListener('contextmenu', this._ctx);
|
|
49
|
+
element.addEventListener('dblclick', this._dbl);
|
|
50
|
+
element.style.touchAction = 'none';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
dispose() {
|
|
54
|
+
const el = this.el;
|
|
55
|
+
el.removeEventListener('pointerdown', this._down);
|
|
56
|
+
el.removeEventListener('pointermove', this._move);
|
|
57
|
+
el.removeEventListener('pointerup', this._up);
|
|
58
|
+
el.removeEventListener('pointercancel', this._up);
|
|
59
|
+
el.removeEventListener('wheel', this._wheel);
|
|
60
|
+
el.removeEventListener('contextmenu', this._ctx);
|
|
61
|
+
el.removeEventListener('dblclick', this._dbl);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Apply a document's <controls> (scene.controls, or null for free orbiting).
|
|
66
|
+
* @param {object|null} spec
|
|
67
|
+
* @param {{world?: () => object, ignoreMode?: boolean}} [opts] world supplies the
|
|
68
|
+
* scene for collision; ignoreMode keeps dragging on (an editor previewing limits).
|
|
69
|
+
*/
|
|
70
|
+
configure(spec, opts = {}) {
|
|
71
|
+
this.limits = spec;
|
|
72
|
+
if (opts.world) this.world = opts.world;
|
|
73
|
+
const mode = spec ? spec.mode : 'orbit';
|
|
74
|
+
this.enabled = opts.ignoreMode || mode === 'orbit';
|
|
75
|
+
this.enableZoom = !spec || spec.zoom !== false;
|
|
76
|
+
this.enablePan = !spec || spec.pan !== false;
|
|
77
|
+
// a canvas that does not orbit must let touch scrolling through to the page
|
|
78
|
+
this.el.style.touchAction = this.enabled ? 'none' : 'auto';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
changed() {
|
|
82
|
+
if (this.onChange) this.onChange();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Camera state before a manipulation, for constrain(). */
|
|
86
|
+
snapshot() {
|
|
87
|
+
return { position: this.camera.position.slice(), target: this.camera.target.slice() };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Enforce limits and collision after a manipulation that started at `prev`. */
|
|
91
|
+
constrain(prev) {
|
|
92
|
+
if (!this.limits) return;
|
|
93
|
+
const world = this.limits.collide && this.world ? this.world() : null;
|
|
94
|
+
constrainCamera(prev, this.camera, this.limits, world);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
pointerDown(e) {
|
|
98
|
+
if (!this.enabled) return;
|
|
99
|
+
this.el.setPointerCapture(e.pointerId);
|
|
100
|
+
this.pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
101
|
+
this.moved = 0;
|
|
102
|
+
if (this.pointers.size === 2) {
|
|
103
|
+
const [a, b] = [...this.pointers.values()];
|
|
104
|
+
this.pinch = { d: Math.hypot(a.x - b.x, a.y - b.y), cx: (a.x + b.x) / 2, cy: (a.y + b.y) / 2 };
|
|
105
|
+
this.mode = 'pinch';
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (e.button === 2 || e.button === 1 || e.shiftKey || e.ctrlKey || e.metaKey) this.mode = 'pan';
|
|
109
|
+
else this.mode = 'orbit';
|
|
110
|
+
this.last = { x: e.clientX, y: e.clientY };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
pointerMove(e) {
|
|
114
|
+
if (!this.enabled || !this.pointers.has(e.pointerId)) return;
|
|
115
|
+
this.pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
116
|
+
const prev = this.snapshot();
|
|
117
|
+
if (this.mode === 'pinch' && this.pointers.size >= 2) {
|
|
118
|
+
const [a, b] = [...this.pointers.values()];
|
|
119
|
+
const d = Math.hypot(a.x - b.x, a.y - b.y);
|
|
120
|
+
const cx = (a.x + b.x) / 2, cy = (a.y + b.y) / 2;
|
|
121
|
+
if (this.pinch.d > 0 && this.enableZoom) this.zoom(this.pinch.d / Math.max(d, 1));
|
|
122
|
+
if (this.enablePan) this.pan(cx - this.pinch.cx, cy - this.pinch.cy);
|
|
123
|
+
this.pinch = { d, cx, cy };
|
|
124
|
+
this.moved += 10;
|
|
125
|
+
this.constrain(prev);
|
|
126
|
+
this.changed();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (!this.last) return;
|
|
130
|
+
const dx = e.clientX - this.last.x;
|
|
131
|
+
const dy = e.clientY - this.last.y;
|
|
132
|
+
this.last = { x: e.clientX, y: e.clientY };
|
|
133
|
+
this.moved += Math.abs(dx) + Math.abs(dy);
|
|
134
|
+
if (this.mode === 'orbit') this.orbit(dx, dy);
|
|
135
|
+
else if (this.mode === 'pan' && this.enablePan) this.pan(dx, dy);
|
|
136
|
+
this.constrain(prev);
|
|
137
|
+
this.changed();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
pointerUp(e) {
|
|
141
|
+
if (!this.pointers.has(e.pointerId)) return;
|
|
142
|
+
this.pointers.delete(e.pointerId);
|
|
143
|
+
try {
|
|
144
|
+
this.el.releasePointerCapture(e.pointerId);
|
|
145
|
+
} catch {
|
|
146
|
+
// pointer capture may already be released
|
|
147
|
+
}
|
|
148
|
+
if (this.pointers.size === 0) {
|
|
149
|
+
if (this.moved < 4 && this.onClick && e.button === 0) {
|
|
150
|
+
const r = this.el.getBoundingClientRect();
|
|
151
|
+
this.onClick(e.clientX - r.left, e.clientY - r.top, e);
|
|
152
|
+
}
|
|
153
|
+
this.mode = null;
|
|
154
|
+
this.last = null;
|
|
155
|
+
this.pinch = null;
|
|
156
|
+
} else if (this.pointers.size === 1) {
|
|
157
|
+
const p = [...this.pointers.values()][0];
|
|
158
|
+
this.mode = 'orbit';
|
|
159
|
+
this.last = { x: p.x, y: p.y };
|
|
160
|
+
this.pinch = null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
wheel(e) {
|
|
165
|
+
// without zoom the wheel scrolls the page
|
|
166
|
+
if (!this.enabled || !this.enableZoom) return;
|
|
167
|
+
e.preventDefault();
|
|
168
|
+
const delta = e.deltaMode === 1 ? e.deltaY * 33 : e.deltaMode === 2 ? e.deltaY * 400 : e.deltaY;
|
|
169
|
+
const prev = this.snapshot();
|
|
170
|
+
this.zoom(Math.exp(delta * this.zoomSpeed));
|
|
171
|
+
this.constrain(prev);
|
|
172
|
+
this.changed();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
orbit(dx, dy) {
|
|
176
|
+
const c = this.camera;
|
|
177
|
+
const off = [c.position[0] - c.target[0], c.position[1] - c.target[1], c.position[2] - c.target[2]];
|
|
178
|
+
const r = Math.hypot(off[0], off[1], off[2]) || 1e-6;
|
|
179
|
+
let theta = Math.atan2(off[0], off[2]);
|
|
180
|
+
let phi = Math.acos(Math.max(-1, Math.min(1, off[1] / r)));
|
|
181
|
+
theta -= dx * this.rotateSpeed;
|
|
182
|
+
phi -= dy * this.rotateSpeed;
|
|
183
|
+
phi = Math.max(0.001, Math.min(Math.PI - 0.001, phi));
|
|
184
|
+
c.position = [
|
|
185
|
+
c.target[0] + r * Math.sin(phi) * Math.sin(theta),
|
|
186
|
+
c.target[1] + r * Math.cos(phi),
|
|
187
|
+
c.target[2] + r * Math.sin(phi) * Math.cos(theta),
|
|
188
|
+
];
|
|
189
|
+
c.up = [0, 1, 0];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
pan(dx, dy) {
|
|
193
|
+
const c = this.camera;
|
|
194
|
+
const h = this.el.clientHeight || 1;
|
|
195
|
+
const dist = c.distance;
|
|
196
|
+
const unitsPerPixel = c.type === 'orthographic' ? c.orthoHeight / h : (2 * dist * Math.tan(((c.fov / 2) * Math.PI) / 180)) / h;
|
|
197
|
+
// camera basis
|
|
198
|
+
const f = [c.target[0] - c.position[0], c.target[1] - c.position[1], c.target[2] - c.position[2]];
|
|
199
|
+
const fl = Math.hypot(...f) || 1;
|
|
200
|
+
f[0] /= fl; f[1] /= fl; f[2] /= fl;
|
|
201
|
+
let right = [f[1] * c.up[2] - f[2] * c.up[1], f[2] * c.up[0] - f[0] * c.up[2], f[0] * c.up[1] - f[1] * c.up[0]];
|
|
202
|
+
const rl = Math.hypot(...right) || 1;
|
|
203
|
+
right = right.map((v) => v / rl);
|
|
204
|
+
const up = [right[1] * f[2] - right[2] * f[1], right[2] * f[0] - right[0] * f[2], right[0] * f[1] - right[1] * f[0]];
|
|
205
|
+
const mx = -dx * unitsPerPixel, my = dy * unitsPerPixel;
|
|
206
|
+
for (let k = 0; k < 3; k++) {
|
|
207
|
+
const d = right[k] * mx + up[k] * my;
|
|
208
|
+
c.position[k] += d;
|
|
209
|
+
c.target[k] += d;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
zoom(factor) {
|
|
214
|
+
const c = this.camera;
|
|
215
|
+
if (c.type === 'orthographic') {
|
|
216
|
+
c.orthoHeight = Math.max(1e-4, c.orthoHeight * factor);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const off = [c.position[0] - c.target[0], c.position[1] - c.target[1], c.position[2] - c.target[2]];
|
|
220
|
+
const r = Math.hypot(...off);
|
|
221
|
+
const nr = Math.max(1e-4, r * factor);
|
|
222
|
+
for (let k = 0; k < 3; k++) c.position[k] = c.target[k] + (off[k] / (r || 1)) * nr;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Move the orbit centre to a point, keeping the viewing direction. */
|
|
226
|
+
focus(point) {
|
|
227
|
+
const c = this.camera;
|
|
228
|
+
const prev = this.snapshot();
|
|
229
|
+
const d = [point[0] - c.target[0], point[1] - c.target[1], point[2] - c.target[2]];
|
|
230
|
+
for (let k = 0; k < 3; k++) {
|
|
231
|
+
c.target[k] += d[k];
|
|
232
|
+
c.position[k] += d[k];
|
|
233
|
+
}
|
|
234
|
+
this.constrain(prev);
|
|
235
|
+
this.changed();
|
|
236
|
+
}
|
|
237
|
+
}
|