@weasel-js/kernel3d 1.10.0 → 1.11.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/README.md +12 -0
- package/dist/index.d.ts +212 -42
- package/dist/index.js +556 -105
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { lookAt, multiply, perspective, quatIdentity, compose, transformAabb, invert, transformPoint, normalize, sub, transformPoint4, intersectRayAabb,
|
|
2
|
-
import { resolveOverlays, defineTool } from '@weasel-js/core';
|
|
1
|
+
import { lookAt, multiply, perspective, add, scale, quatIdentity, compose, transformAabb, invert, transformPoint, normalize, sub, transformPoint4, intersectRayAabb, distance, dot, len, cross, intersectRayPlane, pointOnRay, aabbOfPoints } from '@weasel-js/geom/3d';
|
|
2
|
+
import { createSnapOutcomeStore, DEFAULT_SNAP_PX, overlaysOf, resolveOverlays, defineTool, snapSourcesFor, resolvedSnapMarks, SNAP_AXIS_STEP, gatherSnapCandidates } from '@weasel-js/core';
|
|
3
|
+
import { closestOnFlat, resolveSnap, stepLine } from '@weasel-js/geom/nd';
|
|
3
4
|
import { useMemo } from 'react';
|
|
5
|
+
import { pref } from '@weasel-js/prefs';
|
|
4
6
|
|
|
5
7
|
// src/camera.ts
|
|
6
8
|
var MAX_PITCH = Math.PI / 2 - 0.01;
|
|
7
|
-
var MIN_DISTANCE = 0.5;
|
|
8
|
-
var MAX_DISTANCE = 500;
|
|
9
9
|
var UP = { x: 0, y: 1, z: 0 };
|
|
10
10
|
function createCamera(init) {
|
|
11
11
|
return {
|
|
@@ -19,11 +19,11 @@ function createCamera(init) {
|
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
function cameraEye(camera) {
|
|
22
|
-
const { target, yaw, pitch, distance } = camera;
|
|
23
|
-
const horizontal = Math.cos(pitch) *
|
|
22
|
+
const { target, yaw, pitch, distance: distance2 } = camera;
|
|
23
|
+
const horizontal = Math.cos(pitch) * distance2;
|
|
24
24
|
return {
|
|
25
25
|
x: target.x + horizontal * Math.sin(yaw),
|
|
26
|
-
y: target.y + Math.sin(pitch) *
|
|
26
|
+
y: target.y + Math.sin(pitch) * distance2,
|
|
27
27
|
z: target.z + horizontal * Math.cos(yaw)
|
|
28
28
|
};
|
|
29
29
|
}
|
|
@@ -33,25 +33,59 @@ function cameraView(camera) {
|
|
|
33
33
|
function cameraViewProjection(camera, aspect) {
|
|
34
34
|
return multiply(perspective(camera.fov, aspect, camera.near, camera.far), cameraView(camera));
|
|
35
35
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
var DEFAULT_ORBIT_LIMITS = {
|
|
37
|
+
minDistance: 0.5,
|
|
38
|
+
maxDistance: 500,
|
|
39
|
+
minPitch: -MAX_PITCH,
|
|
40
|
+
maxPitch: MAX_PITCH
|
|
41
|
+
};
|
|
42
|
+
var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
43
|
+
function clampCamera(camera, limits = {}) {
|
|
44
|
+
const l = { ...DEFAULT_ORBIT_LIMITS, ...limits };
|
|
45
|
+
const pitch = clamp(camera.pitch, Math.max(-MAX_PITCH, l.minPitch), Math.min(MAX_PITCH, l.maxPitch));
|
|
46
|
+
const distance2 = clamp(camera.distance, l.minDistance, l.maxDistance);
|
|
47
|
+
if (pitch === camera.pitch && distance2 === camera.distance) return camera;
|
|
48
|
+
return { ...camera, pitch, distance: distance2 };
|
|
49
|
+
}
|
|
50
|
+
function orbitBy(camera, deltaYaw, deltaPitch, limits) {
|
|
51
|
+
return clampCamera(
|
|
52
|
+
{ ...camera, yaw: camera.yaw + deltaYaw, pitch: camera.pitch + deltaPitch },
|
|
53
|
+
limits
|
|
54
|
+
);
|
|
42
55
|
}
|
|
43
|
-
function dollyBy(camera, factor) {
|
|
44
|
-
return {
|
|
45
|
-
|
|
46
|
-
|
|
56
|
+
function dollyBy(camera, factor, limits) {
|
|
57
|
+
return clampCamera({ ...camera, distance: camera.distance * factor }, limits);
|
|
58
|
+
}
|
|
59
|
+
function panBy(camera, dx, dy, paneHeight) {
|
|
60
|
+
if (!(paneHeight > 0)) return camera;
|
|
61
|
+
const perPixel = 2 * camera.distance * Math.tan(camera.fov / 2) / paneHeight;
|
|
62
|
+
const { yaw, pitch } = camera;
|
|
63
|
+
const right = { x: Math.cos(yaw), y: 0, z: -Math.sin(yaw) };
|
|
64
|
+
const up = {
|
|
65
|
+
x: -Math.sin(yaw) * Math.sin(pitch),
|
|
66
|
+
y: Math.cos(pitch),
|
|
67
|
+
z: -Math.cos(yaw) * Math.sin(pitch)
|
|
47
68
|
};
|
|
69
|
+
const shift = add(scale(right, -dx * perPixel), scale(up, dy * perPixel));
|
|
70
|
+
return { ...camera, target: add(camera.target, shift) };
|
|
71
|
+
}
|
|
72
|
+
function dollyToward(camera, factor, at, pane, limits) {
|
|
73
|
+
const dollied = dollyBy(camera, factor, limits);
|
|
74
|
+
const kept = 1 - dollied.distance / camera.distance;
|
|
75
|
+
const moved = panBy(
|
|
76
|
+
camera,
|
|
77
|
+
-kept * (at.x - pane.width / 2),
|
|
78
|
+
-kept * (at.y - pane.height / 2),
|
|
79
|
+
pane.height
|
|
80
|
+
);
|
|
81
|
+
return { ...dollied, target: moved.target };
|
|
48
82
|
}
|
|
49
83
|
var UNIT_CUBE = {
|
|
50
84
|
min: { x: -0.5, y: -0.5, z: -0.5 },
|
|
51
85
|
max: { x: 0.5, y: 0.5, z: 0.5 }
|
|
52
86
|
};
|
|
53
|
-
function pose3(position,
|
|
54
|
-
return { position, rotation, scale };
|
|
87
|
+
function pose3(position, scale5 = { x: 1, y: 1, z: 1 }, rotation = quatIdentity()) {
|
|
88
|
+
return { position, rotation, scale: scale5 };
|
|
55
89
|
}
|
|
56
90
|
function poseMatrix(pose) {
|
|
57
91
|
return compose(pose.position, pose.rotation, pose.scale);
|
|
@@ -134,10 +168,6 @@ function projectAabbToScreen(box, viewProjection, rect) {
|
|
|
134
168
|
if (!any) return null;
|
|
135
169
|
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
136
170
|
}
|
|
137
|
-
var DEFAULT_UP = { x: 0, y: 1, z: 0 };
|
|
138
|
-
function boundsOfNode(world, node) {
|
|
139
|
-
return world.bounds ? world.bounds(node) : aabbOfPose(node.pose);
|
|
140
|
-
}
|
|
141
171
|
function rectOf(viewport) {
|
|
142
172
|
return {
|
|
143
173
|
x: viewport.originX ?? 0,
|
|
@@ -157,6 +187,187 @@ function rayAt(viewport, point) {
|
|
|
157
187
|
cameraEye(viewport.camera)
|
|
158
188
|
);
|
|
159
189
|
}
|
|
190
|
+
function projectPoint(p, viewport) {
|
|
191
|
+
const [x, y, z, w] = transformPoint4(viewProjectionOf(viewport), p);
|
|
192
|
+
if (!(w > 0) || z < -w) return null;
|
|
193
|
+
const screen = ndcToScreen({ x: x / w, y: y / w }, rectOf(viewport));
|
|
194
|
+
return Number.isFinite(screen.x) && Number.isFinite(screen.y) ? screen : null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/world.ts
|
|
198
|
+
var DEFAULT_UP = { x: 0, y: 1, z: 0 };
|
|
199
|
+
function boundsOfNode(world, node) {
|
|
200
|
+
return world.bounds ? world.bounds(node) : aabbOfPose(node.pose);
|
|
201
|
+
}
|
|
202
|
+
var ZERO = { x: 0, y: 0, z: 0 };
|
|
203
|
+
var tuple = (p) => [p.x, p.y, p.z];
|
|
204
|
+
var vec3Of = (t) => ({ x: t[0], y: t[1], z: t[2] });
|
|
205
|
+
function planeBasis(up) {
|
|
206
|
+
const n = normalize(up);
|
|
207
|
+
const e = Math.abs(n.x) < 0.9 ? { x: 1, y: 0, z: 0 } : { x: 0, y: 0, z: 1 };
|
|
208
|
+
const u = normalize(sub(e, scale(n, dot(n, e))));
|
|
209
|
+
return [u, cross(u, n)];
|
|
210
|
+
}
|
|
211
|
+
function inputFlat3d(c, held, basis, delta0, about) {
|
|
212
|
+
const [u, v] = basis;
|
|
213
|
+
if (!held || c === void 0 || c === "aspect") return { at: [0, 0, 0], dirs: [tuple(u), tuple(v)] };
|
|
214
|
+
const step = c === "axis" ? SNAP_AXIS_STEP : c.step;
|
|
215
|
+
return stepLine(tuple(c === "axis" ? ZERO : about), tuple(delta0), [tuple(u), tuple(v)], step);
|
|
216
|
+
}
|
|
217
|
+
function subjectOf(world, ids, up) {
|
|
218
|
+
let box = null;
|
|
219
|
+
let anchor = null;
|
|
220
|
+
for (const id of ids) {
|
|
221
|
+
const node = world.scene.get(id);
|
|
222
|
+
if (!node) continue;
|
|
223
|
+
const b = boundsOfNode(world, node);
|
|
224
|
+
anchor ??= { box: b, height: dot(up, node.pose.position) };
|
|
225
|
+
box = box ? union(box, b) : b;
|
|
226
|
+
}
|
|
227
|
+
return box && anchor ? { box, anchor } : null;
|
|
228
|
+
}
|
|
229
|
+
function union(a, b) {
|
|
230
|
+
return {
|
|
231
|
+
min: { x: Math.min(a.min.x, b.min.x), y: Math.min(a.min.y, b.min.y), z: Math.min(a.min.z, b.min.z) },
|
|
232
|
+
max: { x: Math.max(a.max.x, b.max.x), y: Math.max(a.max.y, b.max.y), z: Math.max(a.max.z, b.max.z) }
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function boxCorner(box, i) {
|
|
236
|
+
return {
|
|
237
|
+
x: i & 1 ? box.max.x : box.min.x,
|
|
238
|
+
y: i & 2 ? box.max.y : box.min.y,
|
|
239
|
+
z: i & 4 ? box.max.z : box.min.z
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function boxProbes(box) {
|
|
243
|
+
const center = scale(add(box.min, box.max), 0.5);
|
|
244
|
+
const out = [];
|
|
245
|
+
for (let i = 0; i < 8; i++) {
|
|
246
|
+
out.push({ id: `corner:${i}`, at: boxCorner(box, i), role: "geometry" });
|
|
247
|
+
}
|
|
248
|
+
out.push(
|
|
249
|
+
{ id: "center", at: center, role: "geometry" },
|
|
250
|
+
{ id: "box.min", at: box.min, role: "box" },
|
|
251
|
+
{ id: "box.center", at: center, role: "box" },
|
|
252
|
+
{ id: "box.max", at: box.max, role: "box" }
|
|
253
|
+
);
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// src/snap/session3d.ts
|
|
258
|
+
var ZERO2 = { x: 0, y: 0, z: 0 };
|
|
259
|
+
function createSession3d(inputs) {
|
|
260
|
+
const { req, world, store } = inputs;
|
|
261
|
+
const up = normalize(world.up ?? DEFAULT_UP);
|
|
262
|
+
const basis = planeBasis(up);
|
|
263
|
+
const frame = frameOf(req, world, up);
|
|
264
|
+
const sources = snapSourcesFor(inputs.sources, req.gesture);
|
|
265
|
+
const unsnapped = (p) => {
|
|
266
|
+
store.set([]);
|
|
267
|
+
return { point: p, held: false, marks: [] };
|
|
268
|
+
};
|
|
269
|
+
return {
|
|
270
|
+
at(p, mods) {
|
|
271
|
+
if (!frame) return unsnapped(p);
|
|
272
|
+
const vp = world.viewport();
|
|
273
|
+
const screen = frame.anchor(vp);
|
|
274
|
+
const anchor = screen && castOnto(vp, screen, up, frame.offset);
|
|
275
|
+
const hit = screen && castOnto(vp, { x: screen.x + p.x - req.press.x, y: screen.y + p.y - req.press.y }, up, frame.offset);
|
|
276
|
+
if (!screen || !anchor || !hit) return unsnapped(p);
|
|
277
|
+
const delta0 = sub(hit, anchor);
|
|
278
|
+
const origin = req.origin ? castOnto(vp, req.origin, up, frame.offset) : null;
|
|
279
|
+
const input = inputFlat3d(req.constrain, mods.constrain, basis, delta0, origin ? sub(origin, anchor) : ZERO2);
|
|
280
|
+
let delta;
|
|
281
|
+
let held = false;
|
|
282
|
+
if (mods.suspend) {
|
|
283
|
+
delta = vec3Of(closestOnFlat(input, tuple(delta0)));
|
|
284
|
+
store.set([]);
|
|
285
|
+
} else {
|
|
286
|
+
const probes = frame.probes(anchor);
|
|
287
|
+
const res = resolveSnap({
|
|
288
|
+
probes: probes.map((q) => ({ id: q.id, at: tuple(q.at), role: q.role })),
|
|
289
|
+
input,
|
|
290
|
+
delta0: tuple(delta0),
|
|
291
|
+
candidates: candidatesOf(inputs, sources, probes, delta0, vp, origin, up),
|
|
292
|
+
toScreen: (w) => {
|
|
293
|
+
const s = projectPoint(vec3Of(w), vp);
|
|
294
|
+
return s ? [s.x, s.y] : [NaN, NaN];
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
delta = vec3Of(res.delta);
|
|
298
|
+
held = res.members.length > 0;
|
|
299
|
+
store.set(resolvedSnapMarks(res.satisfied, delta));
|
|
300
|
+
}
|
|
301
|
+
const landed = projectPoint(add(anchor, delta), vp);
|
|
302
|
+
if (!landed) return unsnapped(p);
|
|
303
|
+
return {
|
|
304
|
+
point: { x: req.press.x + landed.x - screen.x, y: req.press.y + landed.y - screen.y },
|
|
305
|
+
held,
|
|
306
|
+
marks: store.get()
|
|
307
|
+
};
|
|
308
|
+
},
|
|
309
|
+
angle: (theta) => ({ theta, marks: [] }),
|
|
310
|
+
end() {
|
|
311
|
+
store.set([]);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function frameOf(req, world, up) {
|
|
316
|
+
if (req.gesture === "point") {
|
|
317
|
+
return {
|
|
318
|
+
offset: 0,
|
|
319
|
+
anchor: () => req.press,
|
|
320
|
+
probes: (at) => [{ id: "pointer", at, role: "pointer" }]
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
if (req.gesture !== "move") return null;
|
|
324
|
+
const subject = subjectOf(world, req.ids ?? [], up);
|
|
325
|
+
if (!subject) return null;
|
|
326
|
+
const probes = boxProbes(subject.box);
|
|
327
|
+
return {
|
|
328
|
+
offset: subject.anchor.height,
|
|
329
|
+
anchor(vp) {
|
|
330
|
+
const box = projectAabbToScreen(subject.anchor.box, viewProjectionOf(vp), rectOf(vp));
|
|
331
|
+
return box ? { x: box.x + box.width / 2, y: box.y + box.height / 2 } : null;
|
|
332
|
+
},
|
|
333
|
+
probes: () => probes
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function castOnto(vp, s, up, offset) {
|
|
337
|
+
const ray = rayAt(vp, s);
|
|
338
|
+
const t = ray && intersectRayPlane(ray, up, offset);
|
|
339
|
+
return ray && t !== null ? pointOnRay(ray, t) : null;
|
|
340
|
+
}
|
|
341
|
+
function candidatesOf(inputs, sources, probes, delta0, vp, origin, up) {
|
|
342
|
+
const { req } = inputs;
|
|
343
|
+
const shifted = probes.map((q) => ({ ...q, at: add(q.at, delta0) }));
|
|
344
|
+
const perPx = worldPerPx(vp, shifted);
|
|
345
|
+
const found = gatherSnapCandidates(sources, shifted, (_source, offered, reachPx) => ({
|
|
346
|
+
gesture: req.gesture,
|
|
347
|
+
probes: offered,
|
|
348
|
+
delta0,
|
|
349
|
+
exclude: req.ids ?? [],
|
|
350
|
+
...origin ? { origin } : {},
|
|
351
|
+
reachWorld: (px) => px * perPx,
|
|
352
|
+
reachPx
|
|
353
|
+
}), { toVec: tuple, reachPx: inputs.reachPx, ...origin ? { origin } : {} });
|
|
354
|
+
return found.filter((c) => !level(c.locus, up));
|
|
355
|
+
}
|
|
356
|
+
function level(locus, up) {
|
|
357
|
+
if (locus.dirs.length !== 2) return false;
|
|
358
|
+
const n = cross(vec3Of(locus.dirs[0]), vec3Of(locus.dirs[1]));
|
|
359
|
+
return Math.abs(dot(n, up)) >= (1 - 1e-9) * len(n);
|
|
360
|
+
}
|
|
361
|
+
function worldPerPx(vp, probes) {
|
|
362
|
+
const eye = cameraEye(vp.camera);
|
|
363
|
+
const forward = normalize(sub(vp.camera.target, eye));
|
|
364
|
+
const k = 2 * Math.tan(vp.camera.fov / 2) / Math.max(1, vp.height);
|
|
365
|
+
let depth = 0;
|
|
366
|
+
for (const q of probes) depth = Math.max(depth, dot(sub(q.at, eye), forward));
|
|
367
|
+
return depth * k;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/deps.ts
|
|
160
371
|
function viewPlaneAt(camera, p) {
|
|
161
372
|
const normal = normalize(sub(camera.target, cameraEye(camera)));
|
|
162
373
|
return { normal, offset: dot(normal, p) };
|
|
@@ -167,24 +378,6 @@ function onPlane(vp, plane, point) {
|
|
|
167
378
|
const t = intersectRayPlane(ray, plane.normal, plane.offset);
|
|
168
379
|
return t === null ? null : pointOnRay(ray, t);
|
|
169
380
|
}
|
|
170
|
-
function aabbOfPoints(points) {
|
|
171
|
-
const min = { x: Infinity, y: Infinity, z: Infinity };
|
|
172
|
-
const max = { x: -Infinity, y: -Infinity, z: -Infinity };
|
|
173
|
-
for (const p of points) {
|
|
174
|
-
for (const axis of ["x", "y", "z"]) {
|
|
175
|
-
if (p[axis] < min[axis]) min[axis] = p[axis];
|
|
176
|
-
if (p[axis] > max[axis]) max[axis] = p[axis];
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
return { min, max };
|
|
180
|
-
}
|
|
181
|
-
function pointOnRay(ray, t) {
|
|
182
|
-
return {
|
|
183
|
-
x: ray.origin.x + ray.direction.x * t,
|
|
184
|
-
y: ray.origin.y + ray.direction.y * t,
|
|
185
|
-
z: ray.origin.z + ray.direction.z * t
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
381
|
function overlaps(a, b) {
|
|
189
382
|
return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
|
|
190
383
|
}
|
|
@@ -232,50 +425,12 @@ function createAreaSelect(world, selection) {
|
|
|
232
425
|
setSelection: (ids) => selection.set([...ids])
|
|
233
426
|
};
|
|
234
427
|
}
|
|
235
|
-
function createSnap() {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const up = opts.up ?? DEFAULT_UP;
|
|
240
|
-
const minExtent = opts.minExtent ?? 0.25;
|
|
241
|
-
const axes = up.y !== 0 ? ["x", "z"] : up.x !== 0 ? ["y", "z"] : ["x", "y"];
|
|
428
|
+
function createSnap(world, opts = {}) {
|
|
429
|
+
const store = opts.store ?? createSnapOutcomeStore();
|
|
430
|
+
const sources = opts.sources ?? [];
|
|
431
|
+
const reachPx = opts.reachPx ?? DEFAULT_SNAP_PX;
|
|
242
432
|
return {
|
|
243
|
-
|
|
244
|
-
const vp = opts.viewport();
|
|
245
|
-
const corners = [
|
|
246
|
-
{ x: bounds.x, y: bounds.y },
|
|
247
|
-
{ x: bounds.x + bounds.width, y: bounds.y },
|
|
248
|
-
{ x: bounds.x, y: bounds.y + bounds.height },
|
|
249
|
-
{ x: bounds.x + bounds.width, y: bounds.y + bounds.height }
|
|
250
|
-
];
|
|
251
|
-
const hits = [];
|
|
252
|
-
for (const corner of corners) {
|
|
253
|
-
const ray = rayAt(vp, corner);
|
|
254
|
-
if (!ray) continue;
|
|
255
|
-
const t = intersectRayPlane(ray, up, 0);
|
|
256
|
-
if (t !== null) hits.push(pointOnRay(ray, t));
|
|
257
|
-
}
|
|
258
|
-
if (hits.length === 0) return null;
|
|
259
|
-
const [u, v] = axes;
|
|
260
|
-
let minU = Infinity, maxU = -Infinity, minV = Infinity, maxV = -Infinity;
|
|
261
|
-
for (const h of hits) {
|
|
262
|
-
minU = Math.min(minU, h[u]);
|
|
263
|
-
maxU = Math.max(maxU, h[u]);
|
|
264
|
-
minV = Math.min(minV, h[v]);
|
|
265
|
-
maxV = Math.max(maxV, h[v]);
|
|
266
|
-
}
|
|
267
|
-
const center = { x: 0, y: 0, z: 0 };
|
|
268
|
-
center[u] = (minU + maxU) / 2;
|
|
269
|
-
center[v] = (minV + maxV) / 2;
|
|
270
|
-
return opts.mint(
|
|
271
|
-
{
|
|
272
|
-
center,
|
|
273
|
-
width: Math.max(minExtent, maxU - minU),
|
|
274
|
-
depth: Math.max(minExtent, maxV - minV)
|
|
275
|
-
},
|
|
276
|
-
extras
|
|
277
|
-
);
|
|
278
|
-
}
|
|
433
|
+
begin: (req) => createSession3d({ req, world, sources, reachPx, store })
|
|
279
434
|
};
|
|
280
435
|
}
|
|
281
436
|
function createPoseDescriptor(world) {
|
|
@@ -387,7 +542,7 @@ function createPoseDescriptor(world) {
|
|
|
387
542
|
if (!box) return pose;
|
|
388
543
|
const from = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
|
389
544
|
const to = { x: from.x + dx, y: from.y + dy };
|
|
390
|
-
const offset = pose.position
|
|
545
|
+
const offset = dot(up, pose.position);
|
|
391
546
|
const ray = rayAt(vp, to);
|
|
392
547
|
if (!ray) return pose;
|
|
393
548
|
const t = intersectRayPlane(ray, up, offset);
|
|
@@ -413,12 +568,223 @@ function createPoseDescriptor(world) {
|
|
|
413
568
|
}
|
|
414
569
|
return build((pose) => aabbOfPose(pose));
|
|
415
570
|
}
|
|
571
|
+
function footprintPoints(bounds, ends, cast) {
|
|
572
|
+
if (ends) {
|
|
573
|
+
const start = cast(ends.start);
|
|
574
|
+
const end = cast(ends.end);
|
|
575
|
+
if (!start || !end) return [];
|
|
576
|
+
return ends.fromCenter ? [end, sub(scale(start, 2), end)] : [start, end];
|
|
577
|
+
}
|
|
578
|
+
const corners = [
|
|
579
|
+
{ x: bounds.x, y: bounds.y },
|
|
580
|
+
{ x: bounds.x + bounds.width, y: bounds.y },
|
|
581
|
+
{ x: bounds.x, y: bounds.y + bounds.height },
|
|
582
|
+
{ x: bounds.x + bounds.width, y: bounds.y + bounds.height }
|
|
583
|
+
];
|
|
584
|
+
return corners.map(cast).filter((h) => h !== null);
|
|
585
|
+
}
|
|
586
|
+
function createInsert(opts) {
|
|
587
|
+
const up = opts.up ?? DEFAULT_UP;
|
|
588
|
+
const minExtent = opts.minExtent ?? 0.25;
|
|
589
|
+
const axes = up.y !== 0 ? ["x", "z"] : up.x !== 0 ? ["y", "z"] : ["x", "y"];
|
|
590
|
+
return {
|
|
591
|
+
commit(bounds, extras, ends) {
|
|
592
|
+
const vp = opts.viewport();
|
|
593
|
+
const cast = (p) => {
|
|
594
|
+
const ray = rayAt(vp, p);
|
|
595
|
+
const t = ray && intersectRayPlane(ray, up, 0);
|
|
596
|
+
return ray && t !== null ? pointOnRay(ray, t) : null;
|
|
597
|
+
};
|
|
598
|
+
const hits = footprintPoints(bounds, ends, cast);
|
|
599
|
+
if (hits.length === 0) return null;
|
|
600
|
+
const [u, v] = axes;
|
|
601
|
+
let minU = Infinity, maxU = -Infinity, minV = Infinity, maxV = -Infinity;
|
|
602
|
+
for (const h of hits) {
|
|
603
|
+
minU = Math.min(minU, h[u]);
|
|
604
|
+
maxU = Math.max(maxU, h[u]);
|
|
605
|
+
minV = Math.min(minV, h[v]);
|
|
606
|
+
maxV = Math.max(maxV, h[v]);
|
|
607
|
+
}
|
|
608
|
+
const center = { x: 0, y: 0, z: 0 };
|
|
609
|
+
center[u] = (minU + maxU) / 2;
|
|
610
|
+
center[v] = (minV + maxV) / 2;
|
|
611
|
+
return opts.mint(
|
|
612
|
+
{
|
|
613
|
+
center,
|
|
614
|
+
width: Math.max(minExtent, maxU - minU),
|
|
615
|
+
depth: Math.max(minExtent, maxV - minV)
|
|
616
|
+
},
|
|
617
|
+
extras
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
var AXES = ["x", "y", "z"];
|
|
623
|
+
var BITS = [1, 2, 4];
|
|
624
|
+
var UNIT = { x: { x: 1, y: 0, z: 0 }, y: { x: 0, y: 1, z: 0 }, z: { x: 0, y: 0, z: 1 } };
|
|
625
|
+
var ACROSS = { x: [UNIT.y, UNIT.z], y: [UNIT.x, UNIT.z], z: [UNIT.x, UNIT.y] };
|
|
626
|
+
function boxesOf(world, q) {
|
|
627
|
+
const excluded = new Set(q.exclude);
|
|
628
|
+
const out = [];
|
|
629
|
+
for (const node of world.scene.renderOrderNodes()) {
|
|
630
|
+
if (!excluded.has(node.id)) out.push(boundsOfNode(world, node));
|
|
631
|
+
}
|
|
632
|
+
return out;
|
|
633
|
+
}
|
|
634
|
+
function distanceToBox(p, box) {
|
|
635
|
+
const d = (a) => Math.max(box.min[a] - p[a], 0, p[a] - box.max[a]);
|
|
636
|
+
return Math.hypot(d("x"), d("y"), d("z"));
|
|
637
|
+
}
|
|
638
|
+
var centerOf = (box) => scale(add(box.min, box.max), 0.5);
|
|
639
|
+
var isZero = (v) => v.x === 0 && v.y === 0 && v.z === 0;
|
|
640
|
+
function boxSnapSource(world) {
|
|
641
|
+
return {
|
|
642
|
+
id: "box",
|
|
643
|
+
roles: ["geometry", "pointer"],
|
|
644
|
+
*collect(q) {
|
|
645
|
+
const r = q.reachWorld(q.reachPx);
|
|
646
|
+
for (const box of boxesOf(world, q)) {
|
|
647
|
+
for (const p of q.probes) {
|
|
648
|
+
if (distanceToBox(p.at, box) <= r) yield* boxLoci(p.id, p.at, box, r);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
function* boxLoci(probe, at, box, r) {
|
|
655
|
+
for (let i = 0; i < 8; i++) {
|
|
656
|
+
const c = boxCorner(box, i);
|
|
657
|
+
if (distance(at, c) <= r) yield { probe, locus: { at: c, dirs: [] }, mark: { kind: "point", at: c, glyph: "corner" } };
|
|
658
|
+
}
|
|
659
|
+
const center = centerOf(box);
|
|
660
|
+
if (distance(at, center) <= r) {
|
|
661
|
+
yield { probe, locus: { at: center, dirs: [] }, mark: { kind: "point", at: center, glyph: "center" } };
|
|
662
|
+
}
|
|
663
|
+
for (let i = 0; i < 8; i++) {
|
|
664
|
+
for (const bit of BITS) {
|
|
665
|
+
if (i & bit) continue;
|
|
666
|
+
const a = boxCorner(box, i);
|
|
667
|
+
const b = boxCorner(box, i | bit);
|
|
668
|
+
const dir = sub(b, a);
|
|
669
|
+
if (isZero(dir) || distanceToBox(at, { min: a, max: b }) > r) continue;
|
|
670
|
+
yield { probe, locus: { at: a, dirs: [dir], span: [[0, 1]] }, mark: { kind: "line", from: a, to: b } };
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
for (const bit of BITS) {
|
|
674
|
+
const [b1, b2] = BITS.filter((b) => b !== bit);
|
|
675
|
+
for (const base of [0, bit]) {
|
|
676
|
+
const o = boxCorner(box, base);
|
|
677
|
+
const far = boxCorner(box, base | b1 | b2);
|
|
678
|
+
const d1 = sub(boxCorner(box, base | b1), o);
|
|
679
|
+
const d2 = sub(boxCorner(box, base | b2), o);
|
|
680
|
+
if (isZero(d1) || isZero(d2) || distanceToBox(at, { min: o, max: far }) > r) continue;
|
|
681
|
+
yield {
|
|
682
|
+
probe,
|
|
683
|
+
locus: { at: o, dirs: [d1, d2], span: [[0, 1], [0, 1]] },
|
|
684
|
+
mark: () => outline([o, boxCorner(box, base | b1), far, boxCorner(box, base | b2)])
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
function outline(ring) {
|
|
690
|
+
return ring.map((from, i) => ({ kind: "line", from, to: ring[(i + 1) % ring.length] }));
|
|
691
|
+
}
|
|
692
|
+
function alignPlaneSource(world) {
|
|
693
|
+
return {
|
|
694
|
+
id: "align-plane",
|
|
695
|
+
roles: ["box", "pointer"],
|
|
696
|
+
*collect(q) {
|
|
697
|
+
const r = q.reachWorld(q.reachPx);
|
|
698
|
+
for (const [axis, planes] of planesOf(boxesOf(world, q))) {
|
|
699
|
+
for (const [value, ends] of planes) {
|
|
700
|
+
for (const p of q.probes) {
|
|
701
|
+
if (Math.abs(p.at[axis] - value) > r) continue;
|
|
702
|
+
yield {
|
|
703
|
+
probe: p.id,
|
|
704
|
+
locus: { at: ends[0], dirs: ACROSS[axis] },
|
|
705
|
+
mark: (delta) => {
|
|
706
|
+
const from = add(sub(p.at, q.delta0), delta);
|
|
707
|
+
return ends.map((to) => ({ kind: "line", from, to }));
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
function planesOf(boxes) {
|
|
717
|
+
const out = new Map(AXES.map((a) => [a, /* @__PURE__ */ new Map()]));
|
|
718
|
+
for (const box of boxes) {
|
|
719
|
+
const center = centerOf(box);
|
|
720
|
+
for (const axis of AXES) {
|
|
721
|
+
const planes = out.get(axis);
|
|
722
|
+
for (const value of /* @__PURE__ */ new Set([box.min[axis], center[axis], box.max[axis]])) {
|
|
723
|
+
const on = { ...center, [axis]: value };
|
|
724
|
+
const ends = planes.get(value);
|
|
725
|
+
if (ends) ends.push(on);
|
|
726
|
+
else planes.set(value, [on]);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return out;
|
|
731
|
+
}
|
|
732
|
+
function groundGridSource(opts) {
|
|
733
|
+
const up = normalize(opts.up ?? DEFAULT_UP);
|
|
734
|
+
const [u, v] = planeBasis(up);
|
|
735
|
+
const lines = (c) => {
|
|
736
|
+
const near = Math.round(c / opts.step) * opts.step;
|
|
737
|
+
return [near, near + (c >= near ? opts.step : -opts.step)];
|
|
738
|
+
};
|
|
739
|
+
return {
|
|
740
|
+
id: "ground-grid",
|
|
741
|
+
tier: "fallback",
|
|
742
|
+
reachPx: Infinity,
|
|
743
|
+
roles: ["pointer", "geometry"],
|
|
744
|
+
*collect(q) {
|
|
745
|
+
for (const p of q.probes) {
|
|
746
|
+
if (Math.abs(dot(p.at, up)) > 1e-6 * Math.max(1, len(p.at))) continue;
|
|
747
|
+
const a = dot(p.at, u);
|
|
748
|
+
const b = dot(p.at, v);
|
|
749
|
+
for (const at of lines(a)) yield { probe: p.id, locus: { at: add(scale(u, at), scale(v, b)), dirs: [v] } };
|
|
750
|
+
for (const at of lines(b)) yield { probe: p.id, locus: { at: add(scale(u, a), scale(v, at)), dirs: [u] } };
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// src/snap/marks.ts
|
|
757
|
+
function projectSnapMarks(marks, viewport) {
|
|
758
|
+
const lines = [];
|
|
759
|
+
const points = [];
|
|
760
|
+
for (const m of marks) {
|
|
761
|
+
if (m.kind === "point") {
|
|
762
|
+
const at = projectPoint(m.at, viewport);
|
|
763
|
+
if (at) {
|
|
764
|
+
points.push({
|
|
765
|
+
at,
|
|
766
|
+
glyph: m.glyph,
|
|
767
|
+
...m.label !== void 0 ? { label: m.label } : {},
|
|
768
|
+
...m.color ? { color: m.color } : {}
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
const from = projectPoint(m.from, viewport);
|
|
774
|
+
const to = projectPoint(m.to, viewport);
|
|
775
|
+
if (!from || !to) continue;
|
|
776
|
+
lines.push({
|
|
777
|
+
from,
|
|
778
|
+
to,
|
|
779
|
+
..."value" in m ? { value: m.value } : {},
|
|
780
|
+
...m.color ? { color: m.color } : {}
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
return { lines, points };
|
|
784
|
+
}
|
|
416
785
|
function collectOverlayBoxes(handles, origin) {
|
|
417
786
|
const overlays = [];
|
|
418
|
-
for (const handle of handles)
|
|
419
|
-
const ov = handle.overlay?.();
|
|
420
|
-
if (ov) overlays.push(ov);
|
|
421
|
-
}
|
|
787
|
+
for (const handle of handles) overlays.push(...overlaysOf(handle));
|
|
422
788
|
const boxes = [];
|
|
423
789
|
for (const resolved of resolveOverlays(overlays)) {
|
|
424
790
|
if (resolved.kind !== "marquee" && resolved.kind !== "insertPreview") continue;
|
|
@@ -429,41 +795,117 @@ function collectOverlayBoxes(handles, origin) {
|
|
|
429
795
|
}
|
|
430
796
|
var ORBIT_RATE = 6e-3;
|
|
431
797
|
var DOLLY_RATE = 15e-4;
|
|
798
|
+
function screenDelta(move) {
|
|
799
|
+
return move.drag?.screenDelta ?? move.drag?.delta;
|
|
800
|
+
}
|
|
801
|
+
function dragCamera(ctx, kind, step) {
|
|
802
|
+
const dep = ctx.deps.camera3d;
|
|
803
|
+
const origin = dep?.get();
|
|
804
|
+
return {
|
|
805
|
+
kind,
|
|
806
|
+
onMove(move) {
|
|
807
|
+
const delta = screenDelta(move);
|
|
808
|
+
if (!dep || !origin || !delta) return;
|
|
809
|
+
dep.set(step(origin, delta, dep));
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
}
|
|
432
813
|
var orbitAction = {
|
|
433
814
|
id: "camera.orbit",
|
|
434
815
|
label: "Orbit",
|
|
816
|
+
group: "viewport",
|
|
817
|
+
cursor: "grab",
|
|
818
|
+
activeCursor: "grabbing",
|
|
819
|
+
requires: ["camera3d", "kitSettings"],
|
|
820
|
+
invoker: {
|
|
821
|
+
timing: "ongoing",
|
|
822
|
+
start(ctx) {
|
|
823
|
+
const rate = ctx.deps.kitSettings?.get("orbit3d.orbitRate") ?? ORBIT_RATE;
|
|
824
|
+
return dragCamera(ctx, "orbit", (origin, d, dep) => orbitBy(origin, -d.x * rate, d.y * rate, dep.limits?.()));
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
var panAction = {
|
|
829
|
+
id: "camera.pan",
|
|
830
|
+
label: "Pan",
|
|
831
|
+
group: "viewport",
|
|
832
|
+
cursor: "move",
|
|
833
|
+
requires: ["camera3d"],
|
|
834
|
+
invoker: {
|
|
835
|
+
timing: "ongoing",
|
|
836
|
+
start(ctx) {
|
|
837
|
+
return dragCamera(ctx, "pan", (origin, d, dep) => panBy(origin, d.x, d.y, dep.size().height));
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
function anchorOf(params, pane) {
|
|
842
|
+
const x = params?.clientX;
|
|
843
|
+
const y = params?.clientY;
|
|
844
|
+
return typeof x === "number" && typeof y === "number" ? { x, y } : { x: pane.width / 2, y: pane.height / 2 };
|
|
845
|
+
}
|
|
846
|
+
var dollyAction = {
|
|
847
|
+
id: "camera.dolly",
|
|
848
|
+
label: "Dolly",
|
|
849
|
+
group: "viewport",
|
|
850
|
+
defaultBinding: [
|
|
851
|
+
{ spec: { kind: "wheel" }, opts: {} },
|
|
852
|
+
// Unbound, a trackpad pinch zooms the page instead.
|
|
853
|
+
{ spec: { kind: "wheel", mods: { ctrl: true } }, opts: {} },
|
|
854
|
+
{ spec: { kind: "pinch" }, opts: {} }
|
|
855
|
+
],
|
|
856
|
+
requires: ["camera3d", "kitSettings"],
|
|
857
|
+
invoker: {
|
|
858
|
+
timing: "immediate",
|
|
859
|
+
run(deps, params) {
|
|
860
|
+
const dep = deps.camera3d;
|
|
861
|
+
if (!dep) return;
|
|
862
|
+
const factor = typeof params?.scale === "number" ? 1 / params.scale : Math.exp(
|
|
863
|
+
(typeof params?.deltaY === "number" ? params.deltaY : 0) * (deps.kitSettings?.get("orbit3d.dollyRate") ?? DOLLY_RATE)
|
|
864
|
+
);
|
|
865
|
+
if (!(factor > 0) || !Number.isFinite(factor)) return;
|
|
866
|
+
const pane = dep.size();
|
|
867
|
+
dep.set(dollyToward(dep.get(), factor, anchorOf(params, pane), pane, dep.limits?.()));
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
var pinchDollyAction = {
|
|
872
|
+
id: "camera.pinchDolly",
|
|
873
|
+
label: "Pinch Dolly",
|
|
874
|
+
group: "viewport",
|
|
875
|
+
defaultBinding: { kind: "multiTouch", fingers: 2 },
|
|
435
876
|
requires: ["camera3d"],
|
|
436
877
|
invoker: {
|
|
437
878
|
timing: "ongoing",
|
|
438
879
|
start(ctx) {
|
|
439
880
|
const dep = ctx.deps.camera3d;
|
|
440
881
|
const origin = dep?.get();
|
|
882
|
+
const startSpread = ctx.multiTouch?.spread ?? 0;
|
|
883
|
+
const at = ctx.multiTouch?.centroid;
|
|
441
884
|
return {
|
|
442
|
-
kind: "
|
|
885
|
+
kind: "pinch",
|
|
443
886
|
onMove(move) {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
887
|
+
const spread = move.multiTouch?.pinch?.currentSpread ?? 0;
|
|
888
|
+
if (!dep || !origin || !(startSpread > 0) || !(spread > 0)) return;
|
|
889
|
+
const pane = dep.size();
|
|
890
|
+
const anchor = at ?? { x: pane.width / 2, y: pane.height / 2 };
|
|
891
|
+
dep.set(dollyToward(origin, startSpread / spread, anchor, pane, dep.limits?.()));
|
|
448
892
|
}
|
|
449
893
|
};
|
|
450
894
|
}
|
|
451
895
|
}
|
|
452
896
|
};
|
|
453
|
-
var
|
|
454
|
-
id: "camera.
|
|
455
|
-
label: "
|
|
897
|
+
var homeAction = {
|
|
898
|
+
id: "camera.home",
|
|
899
|
+
label: "Reset Camera",
|
|
456
900
|
group: "viewport",
|
|
457
|
-
|
|
458
|
-
defaultBinding: [{ spec: { kind: "wheel" }, opts: {} }],
|
|
901
|
+
defaultBinding: { kind: "doubleClick" },
|
|
459
902
|
requires: ["camera3d"],
|
|
460
903
|
invoker: {
|
|
461
904
|
timing: "immediate",
|
|
462
|
-
run(deps
|
|
905
|
+
run(deps) {
|
|
463
906
|
const dep = deps.camera3d;
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
dep.set(dollyBy(dep.get(), Math.exp(amount * DOLLY_RATE)));
|
|
907
|
+
const home = dep?.home?.();
|
|
908
|
+
if (dep && home) dep.set(home);
|
|
467
909
|
}
|
|
468
910
|
}
|
|
469
911
|
};
|
|
@@ -473,10 +915,19 @@ function useOrbitTool() {
|
|
|
473
915
|
hookName: "useOrbitTool",
|
|
474
916
|
cursor: "grab",
|
|
475
917
|
presentation: { label: "Orbit", group: "viewport" },
|
|
476
|
-
bindings: [
|
|
918
|
+
bindings: [
|
|
919
|
+
{ spec: { kind: "drag" }, actionId: "camera.orbit" },
|
|
920
|
+
{ spec: { kind: "drag", mods: { shift: true } }, actionId: "camera.pan" }
|
|
921
|
+
]
|
|
477
922
|
}), []);
|
|
478
923
|
}
|
|
924
|
+
var kernel3dKitSettingsSchema = pref.group("3D camera", {
|
|
925
|
+
orbit3d: pref.group("Orbit", {
|
|
926
|
+
orbitRate: pref.number("Orbit speed", ORBIT_RATE, { min: 1e-3, max: 0.03, step: 1e-3 }),
|
|
927
|
+
dollyRate: pref.number("Dolly speed", DOLLY_RATE, { min: 2e-4, max: 0.01, step: 1e-4 })
|
|
928
|
+
})
|
|
929
|
+
});
|
|
479
930
|
|
|
480
|
-
export { MAX_PITCH, UNIT_CUBE, aabbOfPose, cameraEye, cameraView, cameraViewProjection, collectOverlayBoxes, createAreaSelect, createCamera, createInsert, createNodeAtPoint, createPoseDescriptor, createSnap, dollyAction, dollyBy, ndcToScreen, orbitAction, orbitBy, pose3, poseMatrix, projectAabbToScreen, rayThroughScreenPoint, screenBoxOf, screenToNdc, useOrbitTool };
|
|
931
|
+
export { DEFAULT_ORBIT_LIMITS, MAX_PITCH, UNIT_CUBE, aabbOfPose, alignPlaneSource, boxSnapSource, cameraEye, cameraView, cameraViewProjection, clampCamera, collectOverlayBoxes, createAreaSelect, createCamera, createInsert, createNodeAtPoint, createPoseDescriptor, createSnap, dollyAction, dollyBy, dollyToward, groundGridSource, homeAction, kernel3dKitSettingsSchema, ndcToScreen, orbitAction, orbitBy, panAction, panBy, pinchDollyAction, pose3, poseMatrix, projectAabbToScreen, projectPoint, projectSnapMarks, rayThroughScreenPoint, screenBoxOf, screenToNdc, useOrbitTool };
|
|
481
932
|
//# sourceMappingURL=index.js.map
|
|
482
933
|
//# sourceMappingURL=index.js.map
|