trid3d 0.2.0 → 0.3.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/trid/scene.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { CanvasRenderer, DEFAULT_LIGHT, DEFAULT_AMBIENT } from "./render/canvasRenderer.js";
2
2
  import { loop } from "./render/loop.js";
3
3
  import { ToolManager } from "./tools/manager.js";
4
+ import { linkTree } from "./sceneGraph.js";
4
5
  import { screenshot, record } from "./export/capture.js";
5
6
 
6
7
  /**
@@ -71,27 +72,37 @@ export class Scene {
71
72
  }
72
73
 
73
74
  /**
74
- * Adds an object to the scene, filling in missing transform fields.
75
+ * Adds an object to the scene, filling in missing transform fields
76
+ * (down its `children` too).
75
77
  *
76
- * @param {{ mesh:{ vertices:{x:number,y:number,z:number}[], edges:number[][] } }} obj - Mesh instance
78
+ * An object can be a group: give it `children`, objects placed relative
79
+ * to it, and leave out `mesh` if it's only there to hold them. Moving it
80
+ * moves them all. See `trid/sceneGraph.js`.
81
+ *
82
+ * @example
83
+ * const car = scene.add({ children: [{ mesh: body }, { mesh: wheel, pos: { x: 0.4, y: -0.2, z: 0 } }] });
84
+ * car.pos.x += 0.1; // the body and the wheel move together
85
+ *
86
+ * @param {Object} obj - An object: `mesh` and/or `children`, plus pos, rot, scale, material...
77
87
  * @returns The same object, so it can be kept and animated
78
88
  */
79
89
  add(obj) {
80
- obj.pos ??= { x: 0, y: 0, z: 0 };
81
- obj.rot ??= { x: 0, y: 0, z: 0 };
82
- obj.scale ??= { x: 1, y: 1, z: 1 };
90
+ linkTree(obj, null);
83
91
  this.objects.push(obj);
84
92
  return obj;
85
93
  }
86
94
 
87
95
  /**
88
- * Removes an object from the scene. Does nothing if it isn't in it.
96
+ * Removes an object: from the scene's top level, or from its group's
97
+ * `children`. Its own children go with it. Does nothing if it isn't there.
89
98
  *
90
- * @param {Object} obj - Object previously passed to `add`
99
+ * @param {Object} obj - Object previously added (or a child of one)
91
100
  */
92
101
  remove(obj) {
93
- const i = this.objects.indexOf(obj);
94
- if (i !== -1) this.objects.splice(i, 1);
102
+ const list = obj.parent ? obj.parent.children : this.objects;
103
+ const i = list?.indexOf(obj) ?? -1;
104
+ if (i !== -1) list.splice(i, 1);
105
+ if (obj.parent) obj.parent = null;
95
106
  }
96
107
 
97
108
  /**
@@ -0,0 +1,131 @@
1
+ import { mat4FromTransform, mulMat4, transformPoint } from "./math/matrix.js";
2
+
3
+ /**
4
+ * Groups: any object can have `children`, placed relative to it. An object
5
+ * without a `mesh` is just a group. Moving, turning or scaling an object
6
+ * moves its whole subtree.
7
+ *
8
+ * const sword = scene.add({ pos, children: [
9
+ * { mesh: blade },
10
+ * { name: "hilt", children: [{ mesh: guard }, { mesh: grip, pos: { x: 0, y: -0.12, z: 0 } }] },
11
+ * ] });
12
+ * sword.rot.y += dt; // everything turns
13
+ *
14
+ * `children` is a plain array, read every frame: push and splice freely.
15
+ * Each object gets two read-only-by-convention fields, kept up to date every
16
+ * frame (and set right away by `scene.add`):
17
+ * - `parent`: the object it's a child of, or null at the top
18
+ * - `world`: its world matrix (parent's world matrix × its own transform)
19
+ * Both are non-enumerable, so they don't show up in JSON or `{ ...obj }`.
20
+ *
21
+ * `visible: false` hides an object and everything under it.
22
+ */
23
+
24
+ /** Sets a field without making it enumerable (keeps objects clean for JSON and spreads). */
25
+ function hidden(obj, key, value) {
26
+ if (Object.prototype.hasOwnProperty.call(obj, key)) obj[key] = value;
27
+ else Object.defineProperty(obj, key, { value, writable: true, configurable: true, enumerable: false });
28
+ }
29
+
30
+ /** Fills missing transform fields, like `scene.add` does. */
31
+ function fill(obj) {
32
+ obj.pos ??= { x: 0, y: 0, z: 0 };
33
+ obj.rot ??= { x: 0, y: 0, z: 0 };
34
+ obj.scale ??= { x: 1, y: 1, z: 1 };
35
+ }
36
+
37
+ /**
38
+ * An object's world matrix, worked out now from its own transform and its
39
+ * parents' (following `obj.parent`). Use it between frames, after changing
40
+ * a transform; renderers keep `obj.world` up to date on their own.
41
+ *
42
+ * @param {Object} obj - A scene object
43
+ * @returns {number[]} 4×4 matrix (see `trid/math/matrix.js`)
44
+ */
45
+ export function worldMatrix(obj) {
46
+ const own = mat4FromTransform(obj);
47
+ return obj.parent ? mulMat4(worldMatrix(obj.parent), own) : own;
48
+ }
49
+
50
+ /**
51
+ * Where an object's origin is in the world, now (with its parents).
52
+ *
53
+ * @param {Object} obj - A scene object
54
+ * @returns {{x:number, y:number, z:number}}
55
+ */
56
+ export function worldPosition(obj) {
57
+ return transformPoint(worldMatrix(obj), { x: 0, y: 0, z: 0 });
58
+ }
59
+
60
+ /**
61
+ * Calls `fn(obj, world)` for every visible object under some roots, parents
62
+ * before children, with each one's world matrix. Changes nothing: objects
63
+ * don't need to be in a scene.
64
+ *
65
+ * @param {Object[]} roots - Top objects
66
+ * @param {(root:Object) => number[]|null} baseOf - Matrix a root's own transform goes under (null: none)
67
+ * @param {(obj:Object, world:number[], parent:Object|null) => void} fn
68
+ */
69
+ export function forEachPlaced(roots, baseOf, fn) {
70
+ const visit = (obj, parent, parentWorld) => {
71
+ if (!obj || typeof obj !== "object") return;
72
+ const world = parentWorld ? mulMat4(parentWorld, mat4FromTransform(obj)) : mat4FromTransform(obj);
73
+ if (obj.visible === false) return;
74
+ fn(obj, world, parent);
75
+ if (Array.isArray(obj.children)) for (const child of obj.children) visit(child, obj, world);
76
+ };
77
+ for (const root of roots) visit(root, root?.parent ?? null, baseOf(root));
78
+ }
79
+
80
+ /**
81
+ * Every visible object of a scene (or of a list of objects and their
82
+ * subtrees), parents before their children, with `parent` and `world` set
83
+ * and missing transforms filled in. Groups without a mesh are included;
84
+ * renderers skip them.
85
+ *
86
+ * Objects given in a list keep their place in the scene: an object that is
87
+ * a child of a group still gets its parents' transforms.
88
+ *
89
+ * @param {Object|Object[]} input - A Scene, a scene object or a list of them
90
+ * @returns {Object[]} The objects, flattened
91
+ */
92
+ export function worldObjects(input) {
93
+ const roots = Array.isArray(input) ? input : Array.isArray(input?.objects) ? input.objects : input ? [input] : [];
94
+ // The scene's own list is the top: those have no parent
95
+ if (roots === input?.objects) for (const obj of roots) if (obj && obj.parent) hidden(obj, "parent", null);
96
+ const out = [];
97
+ forEachPlaced(roots, (root) => (root.parent ? worldMatrix(root.parent) : null), (obj, world, parent) => {
98
+ fill(obj);
99
+ if (obj.parent !== parent) hidden(obj, "parent", parent);
100
+ hidden(obj, "world", world);
101
+ out.push(obj);
102
+ });
103
+ // Hidden objects still get their links (so a hidden group's children know their parent)
104
+ for (const root of roots) if (root?.visible === false) linkTree(root, root.parent ?? null);
105
+ return out;
106
+ }
107
+
108
+ /**
109
+ * Sets `parent` links and fills transforms down a subtree, right away.
110
+ * `scene.add` calls it, so `obj.parent` works before the first frame.
111
+ *
112
+ * @param {Object} obj
113
+ * @param {Object|null} [parent=null]
114
+ */
115
+ export function linkTree(obj, parent = null) {
116
+ fill(obj);
117
+ if (obj.parent !== parent) hidden(obj, "parent", parent);
118
+ if (Array.isArray(obj.children)) for (const child of obj.children) linkTree(child, obj);
119
+ }
120
+
121
+ /**
122
+ * The chain from an object up to the top of its tree: `[obj, parent, ..., root]`.
123
+ *
124
+ * @param {Object} obj
125
+ * @returns {Object[]}
126
+ */
127
+ export function ancestors(obj) {
128
+ const path = [];
129
+ for (let o = obj; o; o = o.parent) path.push(o);
130
+ return path;
131
+ }
@@ -1,5 +1,7 @@
1
1
  import { worldToCamera, cameraForward, cameraRight, cameraUp } from "../math/transform.js";
2
2
  import { pick, worldPerPixel } from "./helpers.js";
3
+ import { invertMat4, transformDirection } from "../math/matrix.js";
4
+ import { worldMatrix, worldPosition } from "../sceneGraph.js";
3
5
 
4
6
  /**
5
7
  * Click an object to pick it, then drag to move it.
@@ -7,9 +9,14 @@ import { pick, worldPerPixel } from "./helpers.js";
7
9
  * - Drag: moves the object parallel to the screen, following the cursor
8
10
  * - Shift + drag: moves it toward or away from the camera instead
9
11
  *
10
- * Objects are picked with `pick` (screen-space bounding box).
12
+ * Objects are picked with `pick`: by screen-space bounding box, or by their
13
+ * triangles with `exact: true`.
11
14
  * Set `obj.draggable = false` to make an object ignore this tool.
12
15
  *
16
+ * Inside a group, the part clicked moves within its group (it follows the
17
+ * cursor however the group is turned or scaled). With `root: true`, the
18
+ * whole group moves instead (the top-level object of what was clicked).
19
+ *
13
20
  * Add this tool after a camera tool so clicking an object drags it
14
21
  * and clicking empty space still moves the camera.
15
22
  */
@@ -19,6 +26,8 @@ export class DragObject {
19
26
  * @param {number} [opts.button=0] - Mouse button that drags (0 left, 1 middle, 2 right)
20
27
  * @param {(obj:Object) => boolean} [opts.filter] - Which objects can be dragged
21
28
  * (default: those without `draggable: false`)
29
+ * @param {boolean} [opts.exact=false] - Pick by triangles instead of bounding boxes (see `pickHit`)
30
+ * @param {boolean} [opts.root=false] - Drag the top-level object (the whole group) of what was clicked
22
31
  * @param {(obj:Object) => void} [opts.onPick] - Called when an object is picked
23
32
  * @param {(obj:Object) => void} [opts.onMove] - Called each time the object moves
24
33
  * @param {(obj:Object) => void} [opts.onDrop] - Called when the object is released
@@ -26,12 +35,16 @@ export class DragObject {
26
35
  constructor({
27
36
  button = 0,
28
37
  filter = obj => obj.draggable !== false,
38
+ exact = false,
39
+ root = false,
29
40
  onPick,
30
41
  onMove,
31
42
  onDrop,
32
43
  } = {}) {
33
44
  this.button = button;
34
45
  this.filter = filter;
46
+ this.exact = exact;
47
+ this.root = root;
35
48
  this.onPick = onPick;
36
49
  this.onMove = onMove;
37
50
  this.onDrop = onDrop;
@@ -43,8 +56,12 @@ export class DragObject {
43
56
  pointerDown(e, scene) {
44
57
  if (e.button !== this.button) return false;
45
58
 
46
- const obj = pick(scene, e.x, e.y, { filter: this.filter });
59
+ let obj = pick(scene, e.x, e.y, { filter: this.filter, exact: this.exact });
47
60
  if (!obj) return false;
61
+ if (this.root) {
62
+ while (obj.parent) obj = obj.parent;
63
+ if (obj.draggable === false) return false;
64
+ }
48
65
 
49
66
  this.dragging = obj;
50
67
  this.onPick?.(obj);
@@ -56,23 +73,30 @@ export class DragObject {
56
73
  if (!obj) return;
57
74
 
58
75
  const cam = scene.camera;
59
- const depth = worldToCamera(obj.pos, cam).z;
76
+ const depth = worldToCamera(worldPosition(obj), cam).z;
60
77
  const k = worldPerPixel(scene, depth);
61
78
 
79
+ // How far to move in the world
80
+ let move;
62
81
  if (e.shiftKey) {
63
82
  // Drag up pushes the object away, drag down pulls it closer
64
83
  const f = cameraForward(cam);
65
84
  const amount = -e.dy * k;
66
- obj.pos.x += f.x * amount;
67
- obj.pos.y += f.y * amount;
68
- obj.pos.z += f.z * amount;
85
+ move = { x: f.x * amount, y: f.y * amount, z: f.z * amount };
69
86
  } else {
70
87
  const r = cameraRight(cam);
71
88
  const u = cameraUp(cam);
72
- obj.pos.x += (r.x * e.dx - u.x * e.dy) * k;
73
- obj.pos.y += (r.y * e.dx - u.y * e.dy) * k;
74
- obj.pos.z += (r.z * e.dx - u.z * e.dy) * k;
89
+ move = {
90
+ x: (r.x * e.dx - u.x * e.dy) * k,
91
+ y: (r.y * e.dx - u.y * e.dy) * k,
92
+ z: (r.z * e.dx - u.z * e.dy) * k,
93
+ };
75
94
  }
95
+ // Inside a group, `pos` is in the group's space: turn the move into it
96
+ if (obj.parent) move = transformDirection(invertMat4(worldMatrix(obj.parent)), move);
97
+ obj.pos.x += move.x;
98
+ obj.pos.y += move.y;
99
+ obj.pos.z += move.z;
76
100
 
77
101
  this.onMove?.(obj);
78
102
  }
@@ -1,9 +1,14 @@
1
- import { localToWorld, worldToCamera, cameraToScreen, DEFAULT_FOV, DEFAULT_NEAR } from "../math/transform.js";
1
+ import { worldToCamera, cameraToScreen, DEFAULT_FOV, DEFAULT_NEAR } from "../math/transform.js";
2
+ import { transformPoint } from "../math/matrix.js";
3
+ import { worldMatrix, worldObjects, ancestors } from "../sceneGraph.js";
4
+ import { edgeSegments, wireEdges } from "../render/draw.js";
2
5
 
3
6
  /**
4
7
  * Screen-space bounding box of an object, in canvas pixels.
5
8
  * Only vertices in front of the camera's near plane are used.
6
9
  *
10
+ * Objects inside groups are measured where they're drawn.
11
+ *
7
12
  * @param {import("../scene.js").Scene} scene - Scene the object is in
8
13
  * @param {Object} obj - Mesh instance (mesh, pos, rot, scale)
9
14
  * @returns {{minX:number, minY:number, maxX:number, maxY:number} | null}
@@ -12,10 +17,11 @@ import { localToWorld, worldToCamera, cameraToScreen, DEFAULT_FOV, DEFAULT_NEAR
12
17
  export function screenBounds(scene, obj) {
13
18
  const { camera, canvas } = scene;
14
19
  const near = camera.near ?? DEFAULT_NEAR;
20
+ const world = worldMatrix(obj);
15
21
  let box = null;
16
22
 
17
23
  for (const v of obj.mesh.vertices) {
18
- const pCam = worldToCamera(localToWorld(v, obj), camera);
24
+ const pCam = worldToCamera(transformPoint(world, v), camera);
19
25
  if (pCam.z < near) continue;
20
26
 
21
27
  const p = cameraToScreen(pCam, camera, canvas.width, canvas.height);
@@ -35,27 +41,37 @@ export function screenBounds(scene, obj) {
35
41
  /**
36
42
  * Finds the object under a canvas pixel.
37
43
  *
38
- * An object is hit when the point is inside its screen-space
39
- * bounding box (plus `padding`). When several objects are hit,
40
- * the one whose center is closest to the camera wins.
44
+ * By default an object is hit when the point is inside its screen-space
45
+ * bounding box (plus `padding`), and when several are hit, the one whose
46
+ * center is closest to the camera wins. That's fast, but it can't tell apart
47
+ * objects that overlap on screen (a sword guard in front of its blade).
48
+ *
49
+ * With `exact: true` it tests the object's actual triangles (see `pickHit`),
50
+ * so the click always matches what's drawn.
51
+ *
52
+ * Objects inside groups are found too: the result is the object with the
53
+ * mesh that was hit (use `pickHit(...).path`, or `obj.parent`, for its groups).
41
54
  *
42
55
  * @param {import("../scene.js").Scene} scene - Scene to search
43
56
  * @param {number} x - Canvas X in pixels
44
57
  * @param {number} y - Canvas Y in pixels
45
58
  * @param {Object} [opts]
46
59
  * @param {(obj:Object) => boolean} [opts.filter] - Only consider objects that pass
47
- * @param {number} [opts.padding=4] - Extra pixels around each box
60
+ * @param {number} [opts.padding=4] - Extra pixels around each box (exact: how close to a line counts, for objects without faces)
61
+ * @param {boolean} [opts.exact=false] - Test triangles instead of bounding boxes
48
62
  * @returns {Object | null} The object, or null if nothing is hit
49
63
  */
50
- export function pick(scene, x, y, { filter = () => true, padding = 4 } = {}) {
64
+ export function pick(scene, x, y, { filter = () => true, padding = 4, exact = false } = {}) {
65
+ if (exact) return pickHit(scene, x, y, { filter, padding })?.object ?? null;
66
+
51
67
  const near = scene.camera.near ?? DEFAULT_NEAR;
52
68
  let best = null;
53
69
  let bestDepth = Infinity;
54
70
 
55
- for (const obj of scene.objects) {
56
- if (!filter(obj)) continue;
71
+ for (const obj of worldObjects(scene)) {
72
+ if (!obj.mesh || !filter(obj)) continue;
57
73
 
58
- const depth = worldToCamera(obj.pos, scene.camera).z;
74
+ const depth = worldToCamera(transformPoint(obj.world, { x: 0, y: 0, z: 0 }), scene.camera).z;
59
75
  if (depth < near || depth >= bestDepth) continue;
60
76
 
61
77
  const box = screenBounds(scene, obj);
@@ -71,6 +87,89 @@ export function pick(scene, x, y, { filter = () => true, padding = 4 } = {}) {
71
87
  return best;
72
88
  }
73
89
 
90
+ /**
91
+ * Exact picking: the nearest object whose drawn surface is under a canvas
92
+ * pixel, with which face was hit and how far it is. Every face is projected
93
+ * with the same math as the renderers and tested triangle by triangle.
94
+ * Objects without faces (wireframes) are hit within `padding` pixels of
95
+ * one of their lines.
96
+ *
97
+ * Faces with a corner behind the camera's near plane are skipped. The cost
98
+ * is one pass over the objects' vertices per call: fine for clicks and hover
99
+ * on models of a few thousand faces.
100
+ *
101
+ * @example
102
+ * canvas.addEventListener("click", (e) => {
103
+ * const hit = pickHit(scene, e.offsetX * canvas.width / canvas.clientWidth, e.offsetY * canvas.height / canvas.clientHeight);
104
+ * if (hit) console.log(hit.object.name, "face", hit.face);
105
+ * });
106
+ *
107
+ * @param {import("../scene.js").Scene} scene - Scene to search
108
+ * @param {number} x - Canvas X in pixels
109
+ * @param {number} y - Canvas Y in pixels
110
+ * @param {Object} [opts]
111
+ * @param {(obj:Object) => boolean} [opts.filter] - Only consider objects that pass
112
+ * @param {number} [opts.padding=4] - How many pixels from a line count as a hit (objects without faces)
113
+ * @param {Object[]} [opts.objects=scene.objects] - Objects to test (with their children)
114
+ * @returns {{ object:Object, face:number, depth:number, path:Object[] } | null}
115
+ * `object` has the mesh that was hit, `face` is the index in its
116
+ * `mesh.faces` (-1 for a line), `depth` the distance in front of the
117
+ * camera, `path` the object then its groups up to the top
118
+ * (`[part, group, ..., root]`); null if nothing is hit
119
+ */
120
+ export function pickHit(scene, x, y, { filter = () => true, padding = 4, objects = scene.objects } = {}) {
121
+ const { camera, canvas } = scene;
122
+ const w = canvas.width, h = canvas.height;
123
+ const near = camera.near ?? DEFAULT_NEAR;
124
+ let best = null;
125
+
126
+ for (const object of worldObjects(objects)) {
127
+ if (!object.mesh?.vertices || !filter(object)) continue;
128
+ const mesh = object.mesh;
129
+ const cam = mesh.vertices.map((v) => worldToCamera(transformPoint(object.world, v), camera));
130
+ const scr = cam.map((p) => (p.z > near ? cameraToScreen(p, camera, w, h) : null));
131
+ const closer = (depth) => !best || depth < best.depth;
132
+
133
+ if (mesh.faces?.length) {
134
+ mesh.faces.forEach((f, face) => {
135
+ // Polygons as triangle fans
136
+ for (let i = 1; i < f.length - 1; i++) {
137
+ const ia = f[0], ib = f[i], ic = f[i + 1];
138
+ const a = scr[ia], b = scr[ib], c = scr[ic];
139
+ if (!a || !b || !c) continue;
140
+ const bary = barycentric(x, y, a, b, c);
141
+ if (!bary) continue;
142
+ // Depth blended by 1/z, like perspective does (exact on tilted faces)
143
+ const depth = 1 / (bary[0] / cam[ia].z + bary[1] / cam[ib].z + bary[2] / cam[ic].z);
144
+ if (closer(depth)) best = { object, face, depth };
145
+ }
146
+ });
147
+ } else {
148
+ for (const [i, j] of edgeSegments(wireEdges(mesh))) {
149
+ const a = scr[i], b = scr[j];
150
+ if (!a || !b) continue;
151
+ const dx = b.x - a.x, dy = b.y - a.y;
152
+ const t = Math.max(0, Math.min(1, ((x - a.x) * dx + (y - a.y) * dy) / (dx * dx + dy * dy || 1)));
153
+ if (Math.hypot(x - (a.x + t * dx), y - (a.y + t * dy)) > padding) continue;
154
+ const depth = 1 / ((1 - t) / cam[i].z + t / cam[j].z);
155
+ if (closer(depth)) best = { object, face: -1, depth };
156
+ }
157
+ }
158
+ }
159
+ if (best) best.path = ancestors(best.object);
160
+ return best;
161
+ }
162
+
163
+ /** Barycentric weights of (x, y) in triangle abc, or null if outside (or degenerate). */
164
+ function barycentric(x, y, a, b, c) {
165
+ const d = (b.y - c.y) * (a.x - c.x) + (c.x - b.x) * (a.y - c.y);
166
+ if (Math.abs(d) < 1e-9) return null;
167
+ const u = ((b.y - c.y) * (x - c.x) + (c.x - b.x) * (y - c.y)) / d;
168
+ const v = ((c.y - a.y) * (x - c.x) + (a.x - c.x) * (y - c.y)) / d;
169
+ const t = 1 - u - v;
170
+ return u >= 0 && v >= 0 && t >= 0 ? [u, v, t] : null;
171
+ }
172
+
74
173
  /**
75
174
  * How many world units one canvas pixel covers at a given distance
76
175
  * from the camera. Useful to make things follow the cursor exactly.
@@ -14,11 +14,13 @@ export class HoverHighlight {
14
14
  * @param {string} [opts.color="#ffffff"] - Highlight color
15
15
  * @param {(obj:Object) => boolean} [opts.filter] - Which objects can be highlighted
16
16
  * (default: those without `hoverable: false`)
17
+ * @param {boolean} [opts.exact=false] - Pick by triangles instead of bounding boxes (see `pickHit`)
17
18
  * @param {(obj:Object|null) => void} [opts.onChange] - Called when the hovered object changes
18
19
  */
19
- constructor({ color = "#ffffff", filter = obj => obj.hoverable !== false, onChange } = {}) {
20
+ constructor({ color = "#ffffff", filter = obj => obj.hoverable !== false, exact = false, onChange } = {}) {
20
21
  this.color = color;
21
22
  this.filter = filter;
23
+ this.exact = exact;
22
24
  this.onChange = onChange;
23
25
 
24
26
  /** The object under the pointer, or null. */
@@ -28,7 +30,7 @@ export class HoverHighlight {
28
30
  }
29
31
 
30
32
  pointerMove(e, scene) {
31
- this._set(pick(scene, e.x, e.y, { filter: this.filter }));
33
+ this._set(pick(scene, e.x, e.y, { filter: this.filter, exact: this.exact }));
32
34
  }
33
35
 
34
36
  pointerLeave() {
@@ -1,8 +1,10 @@
1
1
  export { ToolManager } from "./manager.js";
2
- export { pick, screenBounds, worldPerPixel } from "./helpers.js";
2
+ export { pick, pickHit, screenBounds, worldPerPixel } from "./helpers.js";
3
3
  export { OrbitCamera } from "./orbitCamera.js";
4
4
  export { DragObject } from "./dragObject.js";
5
5
  export { FlyCamera } from "./flyCamera.js";
6
6
  export { HoverHighlight } from "./hoverHighlight.js";
7
7
  export { FpsMeter } from "./fpsMeter.js";
8
8
  export { ObjUploader } from "./objUploader.js";
9
+ export { ViewGizmo } from "./viewGizmo.js";
10
+ export { TransformGizmo } from "./transformGizmo.js";
@@ -100,6 +100,15 @@ export class OrbitCamera {
100
100
  camera.z = this.target.z - f.z * this.distance;
101
101
  }
102
102
 
103
+ /**
104
+ * Restarts the auto-rotate wait (`autoRotateDelay`), as if the user had
105
+ * just dragged. For other tools that turn the camera, so auto-rotate
106
+ * doesn't fight them.
107
+ */
108
+ holdAutoRotate() {
109
+ this._idle = 0;
110
+ }
111
+
103
112
  pointerDown(e) {
104
113
  const panButton = e.button === 1 || e.button === 2 || (e.button === 0 && e.shiftKey);
105
114