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