@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/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
- import { lookAt, multiply, perspective, quatIdentity, compose, transformAabb, invert, transformPoint, normalize, sub, transformPoint4, intersectRayAabb, intersectRayPlane, len, add, scale, dot } from '@weasel-js/geom/3d';
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) * distance;
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) * distance,
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
- function orbitBy(camera, deltaYaw, deltaPitch) {
37
- return {
38
- ...camera,
39
- yaw: camera.yaw + deltaYaw,
40
- pitch: Math.max(-MAX_PITCH, Math.min(MAX_PITCH, camera.pitch + deltaPitch))
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
- ...camera,
46
- distance: Math.max(MIN_DISTANCE, Math.min(MAX_DISTANCE, camera.distance * factor))
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, scale = { x: 1, y: 1, z: 1 }, rotation = quatIdentity()) {
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
- return { point: (p) => p };
237
- }
238
- function createInsert(opts) {
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
- commit(bounds, extras) {
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[upAxis];
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: "orbit",
885
+ kind: "pinch",
443
886
  onMove(move) {
444
- if (!dep || !origin) return;
445
- const delta = move.drag?.screenDelta ?? move.drag?.delta;
446
- if (!delta) return;
447
- dep.set(orbitBy(origin, -delta.x * ORBIT_RATE, delta.y * ORBIT_RATE));
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 dollyAction = {
454
- id: "camera.dolly",
455
- label: "Dolly",
897
+ var homeAction = {
898
+ id: "camera.home",
899
+ label: "Reset Camera",
456
900
  group: "viewport",
457
- // Wheel, in any tool — the dispatcher merges the event's deltas into params.
458
- defaultBinding: [{ spec: { kind: "wheel" }, opts: {} }],
901
+ defaultBinding: { kind: "doubleClick" },
459
902
  requires: ["camera3d"],
460
903
  invoker: {
461
904
  timing: "immediate",
462
- run(deps, params) {
905
+ run(deps) {
463
906
  const dep = deps.camera3d;
464
- if (!dep) return;
465
- const amount = typeof params?.deltaY === "number" ? params.deltaY : 0;
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: [{ spec: { kind: "drag" }, actionId: "camera.orbit" }]
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