trid3d 0.0.0-stage → 0.1.0
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 +21 -0
- package/README.md +102 -2
- package/package.json +41 -4
- package/trid/index.js +53 -0
- package/trid/loaders/obj.js +206 -0
- package/trid/loaders/texture.js +56 -0
- package/trid/math/projection.js +49 -0
- package/trid/math/transform.js +302 -0
- package/trid/render/canvasRenderer.js +634 -0
- package/trid/render/color.js +22 -0
- package/trid/render/draw.js +146 -0
- package/trid/render/effects.js +265 -0
- package/trid/render/loop.js +42 -0
- package/trid/render/normals.js +90 -0
- package/trid/render/shapes.js +230 -0
- package/trid/render/webglRenderer.js +613 -0
- package/trid/scene.js +167 -0
- package/trid/tools/dragObject.js +89 -0
- package/trid/tools/flyCamera.js +111 -0
- package/trid/tools/fpsMeter.js +51 -0
- package/trid/tools/helpers.js +85 -0
- package/trid/tools/hoverHighlight.js +62 -0
- package/trid/tools/index.js +8 -0
- package/trid/tools/manager.js +241 -0
- package/trid/tools/objUploader.js +153 -0
- package/trid/tools/orbitCamera.js +132 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tool plugs into a Scene to add interaction or extra drawing.
|
|
3
|
+
* Every method is optional.
|
|
4
|
+
*
|
|
5
|
+
* @typedef {Object} Tool
|
|
6
|
+
* @property {(scene:Scene) => void} [attach] - Called when the tool is added
|
|
7
|
+
* @property {(scene:Scene) => void} [detach] - Called when the tool is removed
|
|
8
|
+
* @property {(e:ToolPointerEvent, scene:Scene) => boolean} [pointerDown]
|
|
9
|
+
* Return true to take the drag: the following moves and the release
|
|
10
|
+
* go only to this tool
|
|
11
|
+
* @property {(e:ToolPointerEvent, scene:Scene) => void} [pointerMove]
|
|
12
|
+
* Called for the tool holding the drag, or for every tool while hovering
|
|
13
|
+
* @property {(e:ToolPointerEvent, scene:Scene) => void} [pointerUp] - Drag released
|
|
14
|
+
* @property {(scene:Scene) => void} [pointerLeave] - Pointer left the canvas (not dragging)
|
|
15
|
+
* @property {(e:ToolWheelEvent, scene:Scene) => boolean} [wheel]
|
|
16
|
+
* Return true to use the scroll (stops the page from scrolling)
|
|
17
|
+
* @property {(dt:number, scene:Scene) => void} [update] - Called every frame before the scene's update
|
|
18
|
+
* @property {(ctx:CanvasRenderingContext2D, scene:Scene) => void} [draw] - Called after the objects are drawn
|
|
19
|
+
* @property {boolean} [blockContextMenu] - Disable the right-click menu on the canvas
|
|
20
|
+
*
|
|
21
|
+
* @typedef {Object} ToolPointerEvent
|
|
22
|
+
* @property {number} x - Pointer X in canvas pixels
|
|
23
|
+
* @property {number} y - Pointer Y in canvas pixels
|
|
24
|
+
* @property {number} dx - X movement since the last event, in canvas pixels
|
|
25
|
+
* @property {number} dy - Y movement since the last event, in canvas pixels
|
|
26
|
+
* @property {number} button - Button that changed (0 left, 1 middle, 2 right)
|
|
27
|
+
* @property {boolean} shiftKey
|
|
28
|
+
* @property {boolean} ctrlKey
|
|
29
|
+
* @property {boolean} altKey
|
|
30
|
+
* @property {PointerEvent} original - The DOM event
|
|
31
|
+
*
|
|
32
|
+
* @typedef {Object} ToolWheelEvent
|
|
33
|
+
* @property {number} x - Pointer X in canvas pixels
|
|
34
|
+
* @property {number} y - Pointer Y in canvas pixels
|
|
35
|
+
* @property {number} deltaY - Scroll amount (> 0 scrolls down)
|
|
36
|
+
* @property {WheelEvent} original - The DOM event
|
|
37
|
+
*
|
|
38
|
+
* @typedef {import("../scene.js").Scene} Scene
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Keeps a scene's tools and sends them canvas pointer events.
|
|
43
|
+
*
|
|
44
|
+
* Tools added later get the first chance at a pointer down or a
|
|
45
|
+
* scroll, like layers stacked on top of each other. The first tool
|
|
46
|
+
* that returns true takes it.
|
|
47
|
+
*
|
|
48
|
+
* Pointer positions are converted to canvas pixels, so they match
|
|
49
|
+
* the drawing even when the canvas is resized with CSS.
|
|
50
|
+
*/
|
|
51
|
+
export class ToolManager {
|
|
52
|
+
/**
|
|
53
|
+
* @param {Scene} scene - Scene whose canvas receives the events
|
|
54
|
+
*/
|
|
55
|
+
constructor(scene) {
|
|
56
|
+
this.scene = scene;
|
|
57
|
+
/** @type {Tool[]} */
|
|
58
|
+
this.list = [];
|
|
59
|
+
|
|
60
|
+
this._active = null;
|
|
61
|
+
this._pointerId = null;
|
|
62
|
+
this._last = { x: 0, y: 0 };
|
|
63
|
+
this._listening = false;
|
|
64
|
+
|
|
65
|
+
this._onDown = this._onDown.bind(this);
|
|
66
|
+
this._onMove = this._onMove.bind(this);
|
|
67
|
+
this._onUp = this._onUp.bind(this);
|
|
68
|
+
this._onLeave = this._onLeave.bind(this);
|
|
69
|
+
this._onWheel = this._onWheel.bind(this);
|
|
70
|
+
this._onContextMenu = this._onContextMenu.bind(this);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Adds a tool and calls its `attach`. Adding the same tool twice does nothing.
|
|
75
|
+
*
|
|
76
|
+
* @param {Tool} tool
|
|
77
|
+
*/
|
|
78
|
+
add(tool) {
|
|
79
|
+
if (this.list.includes(tool)) return;
|
|
80
|
+
this.list.push(tool);
|
|
81
|
+
tool.attach?.(this.scene);
|
|
82
|
+
this._listen();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Removes a tool and calls its `detach`. Does nothing if it isn't added.
|
|
87
|
+
*
|
|
88
|
+
* @param {Tool} tool
|
|
89
|
+
*/
|
|
90
|
+
remove(tool) {
|
|
91
|
+
const i = this.list.indexOf(tool);
|
|
92
|
+
if (i === -1) return;
|
|
93
|
+
if (this._active === tool) this._release();
|
|
94
|
+
this.list.splice(i, 1);
|
|
95
|
+
tool.detach?.(this.scene);
|
|
96
|
+
if (this.list.length === 0) this._unlisten();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Removes every tool. */
|
|
100
|
+
clear() {
|
|
101
|
+
for (const tool of [...this.list]) this.remove(tool);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Calls every tool's `update`.
|
|
106
|
+
*
|
|
107
|
+
* @param {number} dt - Seconds since the last frame
|
|
108
|
+
*/
|
|
109
|
+
update(dt) {
|
|
110
|
+
for (const tool of this.list) tool.update?.(dt, this.scene);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Calls every tool's `draw`.
|
|
115
|
+
*
|
|
116
|
+
* @param {CanvasRenderingContext2D} ctx
|
|
117
|
+
*/
|
|
118
|
+
draw(ctx) {
|
|
119
|
+
for (const tool of this.list) tool.draw?.(ctx, this.scene);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
_listen() {
|
|
123
|
+
if (this._listening) return;
|
|
124
|
+
const c = this.scene.canvas;
|
|
125
|
+
c.addEventListener("pointerdown", this._onDown);
|
|
126
|
+
c.addEventListener("pointermove", this._onMove);
|
|
127
|
+
c.addEventListener("pointerup", this._onUp);
|
|
128
|
+
c.addEventListener("pointercancel", this._onUp);
|
|
129
|
+
c.addEventListener("pointerleave", this._onLeave);
|
|
130
|
+
c.addEventListener("wheel", this._onWheel, { passive: false });
|
|
131
|
+
c.addEventListener("contextmenu", this._onContextMenu);
|
|
132
|
+
// Let tools receive touch drags instead of the page scrolling
|
|
133
|
+
c.style.touchAction = "none";
|
|
134
|
+
this._listening = true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
_unlisten() {
|
|
138
|
+
if (!this._listening) return;
|
|
139
|
+
const c = this.scene.canvas;
|
|
140
|
+
c.removeEventListener("pointerdown", this._onDown);
|
|
141
|
+
c.removeEventListener("pointermove", this._onMove);
|
|
142
|
+
c.removeEventListener("pointerup", this._onUp);
|
|
143
|
+
c.removeEventListener("pointercancel", this._onUp);
|
|
144
|
+
c.removeEventListener("pointerleave", this._onLeave);
|
|
145
|
+
c.removeEventListener("wheel", this._onWheel);
|
|
146
|
+
c.removeEventListener("contextmenu", this._onContextMenu);
|
|
147
|
+
c.style.touchAction = "";
|
|
148
|
+
this._listening = false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Converts a DOM event position to canvas pixels. */
|
|
152
|
+
_toCanvas(e) {
|
|
153
|
+
const c = this.scene.canvas;
|
|
154
|
+
const rect = c.getBoundingClientRect();
|
|
155
|
+
return {
|
|
156
|
+
x: (e.clientX - rect.left) * (c.width / rect.width),
|
|
157
|
+
y: (e.clientY - rect.top) * (c.height / rect.height),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
_pointerEvent(e) {
|
|
162
|
+
const { x, y } = this._toCanvas(e);
|
|
163
|
+
const ev = {
|
|
164
|
+
x, y,
|
|
165
|
+
dx: x - this._last.x,
|
|
166
|
+
dy: y - this._last.y,
|
|
167
|
+
button: e.button,
|
|
168
|
+
shiftKey: e.shiftKey,
|
|
169
|
+
ctrlKey: e.ctrlKey,
|
|
170
|
+
altKey: e.altKey,
|
|
171
|
+
original: e,
|
|
172
|
+
};
|
|
173
|
+
this._last = { x, y };
|
|
174
|
+
return ev;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
_onDown(e) {
|
|
178
|
+
if (this._active) return;
|
|
179
|
+
const ev = this._pointerEvent(e);
|
|
180
|
+
ev.dx = ev.dy = 0;
|
|
181
|
+
|
|
182
|
+
for (let i = this.list.length - 1; i >= 0; i--) {
|
|
183
|
+
const tool = this.list[i];
|
|
184
|
+
if (tool.pointerDown?.(ev, this.scene) === true) {
|
|
185
|
+
this._active = tool;
|
|
186
|
+
this._pointerId = e.pointerId;
|
|
187
|
+
this.scene.canvas.setPointerCapture?.(e.pointerId);
|
|
188
|
+
e.preventDefault();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
_onMove(e) {
|
|
195
|
+
if (this._active && e.pointerId !== this._pointerId) return;
|
|
196
|
+
const ev = this._pointerEvent(e);
|
|
197
|
+
|
|
198
|
+
if (this._active) {
|
|
199
|
+
this._active.pointerMove?.(ev, this.scene);
|
|
200
|
+
} else {
|
|
201
|
+
for (const tool of this.list) tool.pointerMove?.(ev, this.scene);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
_onUp(e) {
|
|
206
|
+
if (!this._active || e.pointerId !== this._pointerId) return;
|
|
207
|
+
const ev = this._pointerEvent(e);
|
|
208
|
+
const tool = this._active;
|
|
209
|
+
this._release();
|
|
210
|
+
tool.pointerUp?.(ev, this.scene);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
_onLeave() {
|
|
214
|
+
if (this._active) return;
|
|
215
|
+
for (const tool of this.list) tool.pointerLeave?.(this.scene);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
_onWheel(e) {
|
|
219
|
+
const { x, y } = this._toCanvas(e);
|
|
220
|
+
const ev = { x, y, deltaY: e.deltaY, original: e };
|
|
221
|
+
|
|
222
|
+
for (let i = this.list.length - 1; i >= 0; i--) {
|
|
223
|
+
if (this.list[i].wheel?.(ev, this.scene) === true) {
|
|
224
|
+
e.preventDefault();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
_onContextMenu(e) {
|
|
231
|
+
if (this.list.some(tool => tool.blockContextMenu)) e.preventDefault();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
_release() {
|
|
235
|
+
if (this._pointerId !== null) {
|
|
236
|
+
this.scene.canvas.releasePointerCapture?.(this._pointerId);
|
|
237
|
+
}
|
|
238
|
+
this._active = null;
|
|
239
|
+
this._pointerId = null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { readOBJFile, pickOBJFile } from "../loaders/obj.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lets the user load .obj files into the scene.
|
|
5
|
+
*
|
|
6
|
+
* - Drag and drop a .obj file onto the canvas
|
|
7
|
+
* - Or call `open()` from a button click to show the file dialog
|
|
8
|
+
*
|
|
9
|
+
* By default the new model replaces the last one it loaded. Pass
|
|
10
|
+
* `onLoad` to decide yourself what happens with it.
|
|
11
|
+
*/
|
|
12
|
+
export class ObjUploader {
|
|
13
|
+
/**
|
|
14
|
+
* @param {Object} [opts]
|
|
15
|
+
* @param {number|null} [opts.fit=1] - Size of the model's largest side (null keeps the file's units)
|
|
16
|
+
* @param {string} [opts.color] - Color of loaded models (default: the scene's color)
|
|
17
|
+
* @param {boolean} [opts.replace=true] - Remove the previously loaded model first
|
|
18
|
+
* @param {string} [opts.hint="Drop an .obj file here"] - Text shown while nothing is loaded ("" for none)
|
|
19
|
+
* @param {(obj:Object, file:File, scene:Scene) => void} [opts.onLoad] - Called with the object after it's added
|
|
20
|
+
* @param {(err:Error, file:File) => void} [opts.onError] - Called when a file can't be read
|
|
21
|
+
*/
|
|
22
|
+
constructor({
|
|
23
|
+
fit = 1,
|
|
24
|
+
color,
|
|
25
|
+
replace = true,
|
|
26
|
+
hint = "Drop an .obj file here",
|
|
27
|
+
onLoad,
|
|
28
|
+
onError = (err) => console.error(err),
|
|
29
|
+
} = {}) {
|
|
30
|
+
this.fit = fit;
|
|
31
|
+
this.color = color;
|
|
32
|
+
this.replace = replace;
|
|
33
|
+
this.hint = hint;
|
|
34
|
+
this.onLoad = onLoad;
|
|
35
|
+
this.onError = onError;
|
|
36
|
+
|
|
37
|
+
/** The last object this tool added, or null. */
|
|
38
|
+
this.object = null;
|
|
39
|
+
|
|
40
|
+
this._scene = null;
|
|
41
|
+
this._dragging = false;
|
|
42
|
+
this._onDragOver = this._onDragOver.bind(this);
|
|
43
|
+
this._onDragLeave = this._onDragLeave.bind(this);
|
|
44
|
+
this._onDrop = this._onDrop.bind(this);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
attach(scene) {
|
|
48
|
+
this._scene = scene;
|
|
49
|
+
const c = scene.canvas;
|
|
50
|
+
c.addEventListener("dragover", this._onDragOver);
|
|
51
|
+
c.addEventListener("dragleave", this._onDragLeave);
|
|
52
|
+
c.addEventListener("drop", this._onDrop);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
detach(scene) {
|
|
56
|
+
const c = scene.canvas;
|
|
57
|
+
c.removeEventListener("dragover", this._onDragOver);
|
|
58
|
+
c.removeEventListener("dragleave", this._onDragLeave);
|
|
59
|
+
c.removeEventListener("drop", this._onDrop);
|
|
60
|
+
this._scene = null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Shows the file dialog and loads the chosen file.
|
|
65
|
+
* Call it from a user action, like a button click.
|
|
66
|
+
*
|
|
67
|
+
* @returns {Promise<Object|null>} The added object, or null if cancelled or failed
|
|
68
|
+
*/
|
|
69
|
+
async open() {
|
|
70
|
+
const file = await pickOBJFile();
|
|
71
|
+
return file ? this.load(file) : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Loads a .obj `File` (or `Blob`) into the scene.
|
|
76
|
+
*
|
|
77
|
+
* @param {File} file
|
|
78
|
+
* @returns {Promise<Object|null>} The added object, or null if it failed
|
|
79
|
+
*/
|
|
80
|
+
async load(file) {
|
|
81
|
+
const scene = this._scene;
|
|
82
|
+
if (!scene) return null;
|
|
83
|
+
|
|
84
|
+
let mesh;
|
|
85
|
+
try {
|
|
86
|
+
mesh = await readOBJFile(file, this.fit == null ? {} : { fit: this.fit });
|
|
87
|
+
} catch (err) {
|
|
88
|
+
this.onError(err, file);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
if (this._scene !== scene) return null;
|
|
92
|
+
|
|
93
|
+
if (this.replace && this.object) scene.remove(this.object);
|
|
94
|
+
const obj = { mesh };
|
|
95
|
+
if (this.color) obj.color = this.color;
|
|
96
|
+
this.object = scene.add(obj);
|
|
97
|
+
this.onLoad?.(this.object, file, scene);
|
|
98
|
+
this._redraw();
|
|
99
|
+
return this.object;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
draw(ctx, scene) {
|
|
103
|
+
const { width, height } = scene.canvas;
|
|
104
|
+
const showHint = this.hint && !this.object;
|
|
105
|
+
if (!this._dragging && !showHint) return;
|
|
106
|
+
|
|
107
|
+
ctx.save();
|
|
108
|
+
if (this._dragging) {
|
|
109
|
+
ctx.strokeStyle = "rgba(255, 255, 255, 0.6)";
|
|
110
|
+
ctx.lineWidth = 3;
|
|
111
|
+
ctx.setLineDash([10, 8]);
|
|
112
|
+
ctx.strokeRect(8, 8, width - 16, height - 16);
|
|
113
|
+
}
|
|
114
|
+
if (showHint || this._dragging) {
|
|
115
|
+
ctx.fillStyle = "rgba(255, 255, 255, 0.7)";
|
|
116
|
+
ctx.font = "16px sans-serif";
|
|
117
|
+
ctx.textAlign = "center";
|
|
118
|
+
ctx.textBaseline = "middle";
|
|
119
|
+
ctx.fillText(this._dragging ? "Drop to load" : this.hint, width / 2, height / 2);
|
|
120
|
+
}
|
|
121
|
+
ctx.restore();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// A running scene redraws every frame; a stopped one needs a nudge
|
|
125
|
+
_redraw() {
|
|
126
|
+
if (this._scene && !this._scene.running) this._scene.render();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
_onDragOver(e) {
|
|
130
|
+
if (!e.dataTransfer?.types.includes("Files")) return;
|
|
131
|
+
e.preventDefault(); // allows the drop
|
|
132
|
+
e.dataTransfer.dropEffect = "copy";
|
|
133
|
+
if (!this._dragging) {
|
|
134
|
+
this._dragging = true;
|
|
135
|
+
this._redraw();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
_onDragLeave() {
|
|
140
|
+
this._dragging = false;
|
|
141
|
+
this._redraw();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
_onDrop(e) {
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
this._dragging = false;
|
|
147
|
+
this._redraw();
|
|
148
|
+
const files = [...(e.dataTransfer?.files ?? [])];
|
|
149
|
+
const file = files.find((f) => f.name.toLowerCase().endsWith(".obj"));
|
|
150
|
+
if (file) this.load(file);
|
|
151
|
+
else if (files.length) this.onError(new Error("Not an .obj file"), files[0]);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { cameraForward, cameraRight, cameraUp } from "../math/transform.js";
|
|
2
|
+
import { worldPerPixel } from "./helpers.js";
|
|
3
|
+
|
|
4
|
+
const MAX_PITCH = Math.PI / 2 - 0.01;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Mouse camera controls that turn around a target point.
|
|
8
|
+
*
|
|
9
|
+
* - Left drag: orbit around the target
|
|
10
|
+
* - Right drag, middle drag or Shift + left drag: pan (moves the target)
|
|
11
|
+
* - Scroll: zoom in and out
|
|
12
|
+
*
|
|
13
|
+
* The tool drives `scene.camera` (x, y, z, yaw, pitch). Its own
|
|
14
|
+
* `target`, `distance`, `yaw` and `pitch` can be changed at any time.
|
|
15
|
+
* Don't use it together with another tool that moves the camera.
|
|
16
|
+
*/
|
|
17
|
+
export class OrbitCamera {
|
|
18
|
+
/**
|
|
19
|
+
* @param {Object} [opts]
|
|
20
|
+
* @param {{x:number,y:number,z:number}} [opts.target={x:0,y:0,z:0}] - Point to orbit around
|
|
21
|
+
* @param {number} [opts.distance] - Distance to the target (default: the camera's current distance)
|
|
22
|
+
* @param {number} [opts.yaw] - Starting yaw (default: the camera's yaw)
|
|
23
|
+
* @param {number} [opts.pitch] - Starting pitch (default: the camera's pitch)
|
|
24
|
+
* @param {number} [opts.rotateSpeed=0.005] - Radians per dragged pixel
|
|
25
|
+
* @param {number} [opts.zoomSpeed=0.001] - Zoom amount per scroll unit
|
|
26
|
+
* @param {number} [opts.minDistance=0.1] - Closest zoom
|
|
27
|
+
* @param {number} [opts.maxDistance=100] - Farthest zoom
|
|
28
|
+
* @param {boolean} [opts.rotate=true] - Allow orbiting
|
|
29
|
+
* @param {boolean} [opts.pan=true] - Allow panning
|
|
30
|
+
* @param {boolean} [opts.zoom=true] - Allow zooming
|
|
31
|
+
*/
|
|
32
|
+
constructor({
|
|
33
|
+
target = { x: 0, y: 0, z: 0 },
|
|
34
|
+
distance,
|
|
35
|
+
yaw,
|
|
36
|
+
pitch,
|
|
37
|
+
rotateSpeed = 0.005,
|
|
38
|
+
zoomSpeed = 0.001,
|
|
39
|
+
minDistance = 0.1,
|
|
40
|
+
maxDistance = 100,
|
|
41
|
+
rotate = true,
|
|
42
|
+
pan = true,
|
|
43
|
+
zoom = true,
|
|
44
|
+
} = {}) {
|
|
45
|
+
this.target = target;
|
|
46
|
+
this.distance = distance;
|
|
47
|
+
this.yaw = yaw;
|
|
48
|
+
this.pitch = pitch;
|
|
49
|
+
this.rotateSpeed = rotateSpeed;
|
|
50
|
+
this.zoomSpeed = zoomSpeed;
|
|
51
|
+
this.minDistance = minDistance;
|
|
52
|
+
this.maxDistance = maxDistance;
|
|
53
|
+
this.rotate = rotate;
|
|
54
|
+
this.pan = pan;
|
|
55
|
+
this.zoom = zoom;
|
|
56
|
+
|
|
57
|
+
this.blockContextMenu = true;
|
|
58
|
+
this._mode = null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
attach(scene) {
|
|
62
|
+
const cam = scene.camera;
|
|
63
|
+
const t = this.target;
|
|
64
|
+
this.yaw ??= cam.yaw ?? 0;
|
|
65
|
+
this.pitch ??= cam.pitch ?? 0;
|
|
66
|
+
this.distance ??= Math.hypot(
|
|
67
|
+
(cam.x ?? 0) - t.x, (cam.y ?? 0) - t.y, (cam.z ?? 0) - t.z
|
|
68
|
+
) || 1;
|
|
69
|
+
this.apply(cam);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Places the camera from the target, distance, yaw and pitch.
|
|
74
|
+
* Called automatically every frame and after each drag or scroll.
|
|
75
|
+
*
|
|
76
|
+
* @param {import("../math/transform.js").Camera} camera - Camera to move
|
|
77
|
+
*/
|
|
78
|
+
apply(camera) {
|
|
79
|
+
this.pitch = Math.max(-MAX_PITCH, Math.min(MAX_PITCH, this.pitch));
|
|
80
|
+
this.distance = Math.max(this.minDistance, Math.min(this.maxDistance, this.distance));
|
|
81
|
+
|
|
82
|
+
camera.yaw = this.yaw;
|
|
83
|
+
camera.pitch = this.pitch;
|
|
84
|
+
const f = cameraForward(camera);
|
|
85
|
+
camera.x = this.target.x - f.x * this.distance;
|
|
86
|
+
camera.y = this.target.y - f.y * this.distance;
|
|
87
|
+
camera.z = this.target.z - f.z * this.distance;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
pointerDown(e) {
|
|
91
|
+
const panButton = e.button === 1 || e.button === 2 || (e.button === 0 && e.shiftKey);
|
|
92
|
+
|
|
93
|
+
if (panButton && this.pan) this._mode = "pan";
|
|
94
|
+
else if (e.button === 0 && this.rotate) this._mode = "rotate";
|
|
95
|
+
else return false;
|
|
96
|
+
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
pointerMove(e, scene) {
|
|
101
|
+
if (this._mode === "rotate") {
|
|
102
|
+
this.yaw += e.dx * this.rotateSpeed;
|
|
103
|
+
this.pitch -= e.dy * this.rotateSpeed;
|
|
104
|
+
} else if (this._mode === "pan") {
|
|
105
|
+
// Move the target so the point under the cursor follows it
|
|
106
|
+
const k = worldPerPixel(scene, this.distance);
|
|
107
|
+
const r = cameraRight(scene.camera);
|
|
108
|
+
const u = cameraUp(scene.camera);
|
|
109
|
+
this.target.x += (-r.x * e.dx + u.x * e.dy) * k;
|
|
110
|
+
this.target.y += (-r.y * e.dx + u.y * e.dy) * k;
|
|
111
|
+
this.target.z += (-r.z * e.dx + u.z * e.dy) * k;
|
|
112
|
+
} else {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
this.apply(scene.camera);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
pointerUp() {
|
|
119
|
+
this._mode = null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
wheel(e, scene) {
|
|
123
|
+
if (!this.zoom) return false;
|
|
124
|
+
this.distance *= Math.exp(e.deltaY * this.zoomSpeed);
|
|
125
|
+
this.apply(scene.camera);
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
update(dt, scene) {
|
|
130
|
+
this.apply(scene.camera);
|
|
131
|
+
}
|
|
132
|
+
}
|