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/README.md +38 -18
- package/package.json +1 -1
- package/trid/export/mtl.js +119 -0
- package/trid/export/obj.js +107 -21
- package/trid/index.d.ts +419 -8
- package/trid/index.js +19 -3
- package/trid/loaders/mtl.js +63 -0
- package/trid/loaders/obj.js +58 -6
- package/trid/loaders/texture.js +47 -6
- package/trid/math/matrix.js +200 -0
- package/trid/math/noise.js +162 -0
- package/trid/math/rotation.js +152 -0
- package/trid/render/canvasRenderer.js +40 -23
- package/trid/render/draw.js +6 -2
- package/trid/render/meshUtils.js +257 -0
- package/trid/render/proceduralTextures.js +211 -0
- package/trid/render/shapes.js +95 -27
- package/trid/render/webglRenderer.js +98 -27
- package/trid/scene.js +20 -9
- package/trid/sceneGraph.js +131 -0
- package/trid/tools/dragObject.js +33 -9
- package/trid/tools/helpers.js +109 -10
- package/trid/tools/hoverHighlight.js +4 -2
- package/trid/tools/index.js +3 -1
- package/trid/tools/orbitCamera.js +9 -0
- package/trid/tools/transformGizmo.js +316 -0
- package/trid/tools/viewGizmo.js +178 -0
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
99
|
+
* @param {Object} obj - Object previously added (or a child of one)
|
|
91
100
|
*/
|
|
92
101
|
remove(obj) {
|
|
93
|
-
const
|
|
94
|
-
|
|
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
|
+
}
|
package/trid/tools/dragObject.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
}
|
package/trid/tools/helpers.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import {
|
|
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(
|
|
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
|
-
*
|
|
39
|
-
* bounding box (plus `padding`)
|
|
40
|
-
*
|
|
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
|
|
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.
|
|
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() {
|
package/trid/tools/index.js
CHANGED
|
@@ -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
|
|