@weasel-js/kernel3d 1.18.1 → 1.19.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.d.ts CHANGED
@@ -53,14 +53,16 @@ declare const DEFAULT_ORBIT_LIMITS: OrbitLimits;
53
53
  * itself when it already holds. */
54
54
  declare function clampCamera(camera: Camera3d, limits?: Partial<OrbitLimits>): Camera3d;
55
55
  /** The camera turned about its target by the given radians, pitch held inside
56
- * `limits`. Yaw is not wrapped. */
56
+ * `limits`. Yaw is not wrapped. A turn that is not finite leaves it as it is. */
57
57
  declare function orbitBy(camera: Camera3d, deltaYaw: number, deltaPitch: number, limits?: Partial<OrbitLimits>): Camera3d;
58
- /** The camera with its distance multiplied by `factor`, held inside `limits`. */
58
+ /** The camera with its distance multiplied by `factor`, held inside `limits`.
59
+ * A `NaN` factor leaves it as it is. */
59
60
  declare function dollyBy(camera: Camera3d, factor: number, limits?: Partial<OrbitLimits>): Camera3d;
60
61
  /**
61
62
  * The camera with its target slid across the screen plane, so the point at the
62
63
  * target follows a pointer that moved `dx`, `dy` CSS pixels in a pane
63
- * `paneHeight` pixels tall. Neither angle nor distance changes.
64
+ * `paneHeight` pixels tall. Neither angle nor distance changes. A pan that
65
+ * carries the target past the double range leaves the camera as it is.
64
66
  */
65
67
  declare function panBy(camera: Camera3d, dx: number, dy: number, paneHeight: number): Camera3d;
66
68
  /**
@@ -209,14 +211,18 @@ interface Viewport3d {
209
211
  originX?: number;
210
212
  originY?: number;
211
213
  }
212
- /** Read on every dep call, so a dep always sees the current camera and pane. */
213
- type ViewportSource = () => Viewport3d;
214
+ /**
215
+ * The viewport the view `viewId` looks through, read on every dep call so a
216
+ * dep always sees the current camera and pane. `viewId` is a `<CanvasView>`'s
217
+ * id; absent or `null`, the surface's own camera.
218
+ */
219
+ type ViewportSource = (viewId?: string | null) => Viewport3d;
214
220
  type Point$1 = {
215
221
  x: number;
216
222
  y: number;
217
223
  };
218
- /** Where `p` draws in the viewport, or null when it is behind the near plane
219
- * or projects to nothing finite. */
224
+ /** Where `p` draws in the viewport, or null when it is behind the near plane,
225
+ * the pane has no area, or it projects to nothing finite. */
220
226
  declare function projectPoint(p: Vec3, viewport: Viewport3d): Point$1 | null;
221
227
 
222
228
  /** The 3D world every dep is built from: core's scene with a `Pose3`, the
@@ -273,7 +279,8 @@ declare function screenBoxOf<TData, TLayer extends string>(world: World3d<TData,
273
279
  /**
274
280
  * The kit's deps, implemented against a 3D world.
275
281
  *
276
- * Every one of these takes its camera from a thunk rather than an argument:
282
+ * Every one of these takes its camera from a thunk rather than an argument,
283
+ * `world.viewport(viewId)` for the view it was built for:
277
284
  * the dispatcher keeps handing actions two numbers, and what makes them mean
278
285
  * something in 3D is the camera each dep already closes over.
279
286
  */
@@ -283,14 +290,27 @@ declare function screenBoxOf<TData, TLayer extends string>(world: World3d<TData,
283
290
  *
284
291
  * The kit calls this with what it believes is a world point; a 3D host feeds
285
292
  * the dispatcher an identity `clientToWorld`, so it is the pane-relative screen
286
- * point, which is exactly what a ray needs.
293
+ * point, which is exactly what a ray needs. *
294
+ * `viewId` names the view whose camera casts the ray; a surface with several
295
+ * 3D views gives each `<CanvasView deps>` its own. The same goes for every
296
+ * factory here that takes one.
297
+ */
298
+ declare function createNodeAtPoint<TData, TLayer extends string>(world: World3d<TData, TLayer>, viewId?: string | null): NodeAtPointDep;
299
+ /**
300
+ * `<SceneCanvas geometry={{ pickEvery }}>` over a 3D world: the node along the
301
+ * ray the point names, cast from the camera of the view the pick is asked for.
287
302
  */
288
- declare function createNodeAtPoint<TData, TLayer extends string>(world: World3d<TData, TLayer>): NodeAtPointDep;
303
+ declare function createPickEvery<TData, TLayer extends string>(world: World3d<TData, TLayer>): (point: {
304
+ x: number;
305
+ y: number;
306
+ }, view?: {
307
+ viewId?: string | null;
308
+ }) => NodeId | null;
289
309
  /** Marquee: the nodes whose projected box the screen rectangle touches, in render order. */
290
310
  declare function createAreaSelect<TData, TLayer extends string>(world: World3d<TData, TLayer>, selection: {
291
311
  get(): readonly NodeId[];
292
312
  set(ids: NodeId[]): void;
293
- }): AreaSelectDep;
313
+ }, viewId?: string | null): AreaSelectDep;
294
314
  /** Options for `createSnap`. */
295
315
  interface Snap3dOptions {
296
316
  /** What a gesture snaps to, in order: earlier wins a full tie. Two sharing
@@ -305,6 +325,8 @@ interface Snap3dOptions {
305
325
  /** Where the marks of the snap in flight are written, for a host to draw
306
326
  * with `projectSnapMarks`. */
307
327
  store?: SnapOutcomeStore<Vec3>;
328
+ /** The view whose camera a gesture is cast through. Default the surface's. */
329
+ viewId?: string | null;
308
330
  }
309
331
  /**
310
332
  * The `snap` dep over a 3D world. A gesture's `omit` is honored; its 2D
@@ -327,7 +349,7 @@ declare function createSnap<TData, TLayer extends string>(world: World3d<TData,
327
349
  * through the pose they were handed, facing the camera, so nothing they
328
350
  * produce changes depth.
329
351
  */
330
- declare function createPoseDescriptor<TData, TLayer extends string>(world: World3d<TData, TLayer>): PoseDescriptor<Pose3, Node3d<TData, TLayer>>;
352
+ declare function createPoseDescriptor<TData, TLayer extends string>(world: World3d<TData, TLayer>, viewId?: string | null): PoseDescriptor<Pose3, Node3d<TData, TLayer>>;
331
353
 
332
354
  /** The `insert` dep over a 3D world: a drag becomes a footprint on the ground. */
333
355
 
@@ -348,6 +370,8 @@ interface Footprint {
348
370
  */
349
371
  declare function createInsert(opts: {
350
372
  viewport: ViewportSource;
373
+ /** The view whose camera a drag is cast through, for a `<CanvasView deps>` of its own. Default the surface's. */
374
+ viewId?: string | null;
351
375
  up?: Vec3;
352
376
  /** Smallest footprint a click-sized drag produces. Default 0.25. */
353
377
  minExtent?: number;
@@ -419,8 +443,9 @@ interface GroundGridSourceOptions {
419
443
  /**
420
444
  * A grid on the ground plane as snap targets: for each pointer or geometry
421
445
  * probe on the ground, the grid line on either side of it along each of the
422
- * ground's two axes. Its reach is unlimited and it is a fallback, so it
423
- * holds every axis no target claims and never displaces one.
446
+ * ground's two axes, none along an axis where the probe lies so far out that
447
+ * lines a step apart are no longer two numbers. Its reach is unlimited and it
448
+ * is a fallback, so it holds every axis no target claims and never displaces one.
424
449
  */
425
450
  declare function groundGridSource(opts: GroundGridSourceOptions): SnapSource<Vec3>;
426
451
 
@@ -709,5 +734,5 @@ declare const kernel3dKitSettingsSchema: Omit<_weasel_js_prefs.PrefGroup, "child
709
734
  };
710
735
  };
711
736
 
712
- export { DEFAULT_ORBIT_LIMITS, HEAD, HEAD_OVERDRAW, LINE_FLOATS, MAX_PITCH, POSE3_COMPOSITION, UNIT_CUBE, aabbOfPose, aimOf3d, aimSpace3d, alignPlaneSource, boxSnapSource, cameraEye, cameraOfAim3d, cameraView, cameraViewProjection, clampCamera, collectOverlayBoxes, createAreaSelect, createCamera, createInsert, createLinePass, createNodeAtPoint, createPoseDescriptor, createSnap, dollyAction, dollyBy, dollyToward, groundGridSource, headFor, homeAction, kernel3dKitSettingsSchema, layoutLine, ndcToScreen, orbitAction, orbitBy, packLines, panAction, panBy, pinchDollyAction, pose3, poseMatrix, projectAabbToScreen, projectPoint, projectSnapMarks, rayThroughScreenPoint, screenBoxOf, screenToNdc, useOrbitTool, visibleRuns, worldPoseOf };
737
+ export { DEFAULT_ORBIT_LIMITS, HEAD, HEAD_OVERDRAW, LINE_FLOATS, MAX_PITCH, POSE3_COMPOSITION, UNIT_CUBE, aabbOfPose, aimOf3d, aimSpace3d, alignPlaneSource, boxSnapSource, cameraEye, cameraOfAim3d, cameraView, cameraViewProjection, clampCamera, collectOverlayBoxes, createAreaSelect, createCamera, createInsert, createLinePass, createNodeAtPoint, createPickEvery, createPoseDescriptor, createSnap, dollyAction, dollyBy, dollyToward, groundGridSource, headFor, homeAction, kernel3dKitSettingsSchema, layoutLine, ndcToScreen, orbitAction, orbitBy, packLines, panAction, panBy, pinchDollyAction, pose3, poseMatrix, projectAabbToScreen, projectPoint, projectSnapMarks, rayThroughScreenPoint, screenBoxOf, screenToNdc, useOrbitTool, visibleRuns, worldPoseOf };
713
738
  export type { Aim3d, Aim3dOptions, Camera3d, Camera3dDep, ChromeBox, Footprint, Frustum3d, GroundGridSourceOptions, HeadShape, HeadSize, Line3, LineDrawOptions, LineLayout, LinePass, LineRun3, Node3d, NodeBounds, Orbit3dKitSettings, OrbitLimits, Pose3, Scene3d, ScreenBox, ScreenLine, ScreenPoint, Snap3dOptions, Viewport3d, ViewportSource, VisibleRun3, World3d };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { lookAt, multiply, perspective, add, scale, quatIdentity, compose, transformAabb, quatMultiply, quatConjugate, quatRotate, sub, invert, transformPoint, normalize, transformPoint4, distance, dot, len, aabbOfPoints, intersectRayPlane, pointOnRay, cross, intersectRayAabb } from '@weasel-js/geom/3d';
1
+ import { lookAt, multiply, perspective, add, scale, quatIdentity, compose, transformAabb, quatMultiply, quatConjugate, quatRotate, sub, invert, transformPoint, normalize, cross, dot, transformPoint4, distance, len, aabbOfPoints, intersectRayPlane, pointOnRay, intersectRayAabb } from '@weasel-js/geom/3d';
2
2
  import { clamp, lengthTable, curveSpan, rectsOverlap } from '@weasel-js/geom';
3
- import { scenePoseFrame, createSnapOutcomeStore, assertSnapSourceIds, DEFAULT_SNAP_PX, resolveOverlays, snapSourcesFor, resolvedSnapMarks, SNAP_AXIS_STEP, gatherSnapCandidates, scenePoseAdapter, composeWorldPose } from '@weasel-js/core/engine';
3
+ import { scenePoseFrame, createSnapOutcomeStore, assertSnapSourceIds, DEFAULT_SNAP_PX, resolveOverlays, snapSourcesFor, resolveHolding, resolvedSnapMarks, SNAP_AXIS_STEP, gatherSnapCandidates, scenePoseAdapter, composeWorldPose } from '@weasel-js/core/engine';
4
4
  import { isDev } from '@weasel-js/react';
5
5
  import { closestOnFlat, resolveSnap, createBvh, stepLine, createSettledCache } from '@weasel-js/geom/nd';
6
6
  import { overlaysOf, defineTool } from '@weasel-js/routing';
@@ -45,19 +45,23 @@ var DEFAULT_ORBIT_LIMITS = {
45
45
  };
46
46
  function clampCamera(camera, limits = {}) {
47
47
  const l = { ...DEFAULT_ORBIT_LIMITS, ...limits };
48
- const pitch = clamp(camera.pitch, Math.max(-MAX_PITCH, l.minPitch), Math.min(MAX_PITCH, l.maxPitch));
48
+ const pitch = clamp(camera.pitch, clamp(l.minPitch, -MAX_PITCH, MAX_PITCH), clamp(l.maxPitch, -MAX_PITCH, MAX_PITCH));
49
49
  const distance2 = clamp(camera.distance, l.minDistance, l.maxDistance);
50
50
  if (pitch === camera.pitch && distance2 === camera.distance) return camera;
51
51
  return { ...camera, pitch, distance: distance2 };
52
52
  }
53
+ function heldFinite(camera, moved) {
54
+ const { target: t, yaw, pitch, distance: distance2 } = moved;
55
+ return Number.isFinite(t.x + t.y + t.z + yaw + pitch + distance2) ? moved : camera;
56
+ }
53
57
  function orbitBy(camera, deltaYaw, deltaPitch, limits) {
54
- return clampCamera(
58
+ return heldFinite(camera, clampCamera(
55
59
  { ...camera, yaw: camera.yaw + deltaYaw, pitch: camera.pitch + deltaPitch },
56
60
  limits
57
- );
61
+ ));
58
62
  }
59
63
  function dollyBy(camera, factor, limits) {
60
- return clampCamera({ ...camera, distance: camera.distance * factor }, limits);
64
+ return heldFinite(camera, clampCamera({ ...camera, distance: camera.distance * factor }, limits));
61
65
  }
62
66
  function panBy(camera, dx, dy, paneHeight) {
63
67
  if (!(paneHeight > 0)) return camera;
@@ -70,7 +74,7 @@ function panBy(camera, dx, dy, paneHeight) {
70
74
  z: -Math.cos(yaw) * Math.sin(pitch)
71
75
  };
72
76
  const shift = add(scale(right, -dx * perPixel), scale(up, dy * perPixel));
73
- return { ...camera, target: add(camera.target, shift) };
77
+ return heldFinite(camera, { ...camera, target: add(camera.target, shift) });
74
78
  }
75
79
  function dollyToward(camera, factor, at, pane, limits) {
76
80
  const dollied = dollyBy(camera, factor, limits);
@@ -81,14 +85,14 @@ function dollyToward(camera, factor, at, pane, limits) {
81
85
  -kept * (at.y - pane.height / 2),
82
86
  pane.height
83
87
  );
84
- return { ...dollied, target: moved.target };
88
+ return heldFinite(camera, { ...dollied, target: moved.target });
85
89
  }
86
90
  var UNIT_CUBE = {
87
91
  min: { x: -0.5, y: -0.5, z: -0.5 },
88
92
  max: { x: 0.5, y: 0.5, z: 0.5 }
89
93
  };
90
- function pose3(position, scale6 = { x: 1, y: 1, z: 1 }, rotation = quatIdentity()) {
91
- return { position, rotation, scale: scale6 };
94
+ function pose3(position, scale7 = { x: 1, y: 1, z: 1 }, rotation = quatIdentity()) {
95
+ return { position, rotation, scale: scale7 };
92
96
  }
93
97
  function poseMatrix(pose) {
94
98
  return compose(pose.position, pose.rotation, pose.scale);
@@ -97,10 +101,14 @@ function aabbOfPose(pose, local = UNIT_CUBE) {
97
101
  return transformAabb(poseMatrix(pose), local);
98
102
  }
99
103
  var times = (a, b) => ({ x: a.x * b.x, y: a.y * b.y, z: a.z * b.z });
104
+ var overAxis = (a, s, zero) => {
105
+ const q = a / s;
106
+ return s === 0 || !Number.isFinite(q) ? zero : q;
107
+ };
100
108
  var over = (a, s, zero) => ({
101
- x: s.x === 0 ? zero.x : a.x / s.x,
102
- y: s.y === 0 ? zero.y : a.y / s.y,
103
- z: s.z === 0 ? zero.z : a.z / s.z
109
+ x: overAxis(a.x, s.x, zero.x),
110
+ y: overAxis(a.y, s.y, zero.y),
111
+ z: overAxis(a.z, s.z, zero.z)
104
112
  });
105
113
  var ORIGIN = { x: 0, y: 0, z: 0 };
106
114
  var POSE3_COMPOSITION = {
@@ -129,7 +137,8 @@ function ndcToScreen(ndc, rect) {
129
137
  };
130
138
  }
131
139
  function rayThroughScreenPoint(point, rect, viewProjection) {
132
- const inv = invert(viewProjection);
140
+ const eye = eyeOf(viewProjection) ?? ORIGIN2;
141
+ const inv = invert(multiply(viewProjection, compose(eye, quatIdentity(), UNIT)));
133
142
  if (!inv) return null;
134
143
  const ndc = screenToNdc(point, rect);
135
144
  const near = transformPoint(inv, { x: ndc.x, y: ndc.y, z: -1 });
@@ -137,7 +146,19 @@ function rayThroughScreenPoint(point, rect, viewProjection) {
137
146
  const direction = sub(far, near);
138
147
  const unit = normalize(direction);
139
148
  if (unit.x === 0 && unit.y === 0 && unit.z === 0) return null;
140
- return { origin: near, direction };
149
+ return { origin: add(eye, near), direction };
150
+ }
151
+ var ORIGIN2 = { x: 0, y: 0, z: 0 };
152
+ var UNIT = { x: 1, y: 1, z: 1 };
153
+ function eyeOf(m) {
154
+ const row = (r) => ({ x: m[r], y: m[4 + r], z: m[8 + r] });
155
+ const [a, b, c] = [row(0), row(1), row(3)];
156
+ const bc = cross(b, c);
157
+ const ca = cross(c, a);
158
+ const ab = cross(a, b);
159
+ const det = dot(a, bc);
160
+ const eye = scale(add(add(scale(bc, m[12]), scale(ca, m[13])), scale(ab, m[15])), -1 / det);
161
+ return Number.isFinite(eye.x + eye.y + eye.z) ? eye : null;
141
162
  }
142
163
  var CLIP_SIDES = [
143
164
  (p) => p[2] + p[3],
@@ -153,6 +174,7 @@ function lerpClip(a, b, t) {
153
174
  }
154
175
  var EDGE_BITS = [1, 2, 4];
155
176
  function projectAabbToScreen(box, viewProjection, rect) {
177
+ if (!(rect.width > 0 && rect.height > 0)) return null;
156
178
  const corners = [];
157
179
  for (let i = 0; i < 8; i++) {
158
180
  const corner = {
@@ -186,7 +208,7 @@ function projectAabbToScreen(box, viewProjection, rect) {
186
208
  for (const side of CLIP_SIDES) {
187
209
  const da = side(a);
188
210
  const db = side(b);
189
- if (da < 0 && db < 0) {
211
+ if (Number.isNaN(da) || Number.isNaN(db) || da < 0 && db < 0) {
190
212
  t0 = 1;
191
213
  t1 = 0;
192
214
  break;
@@ -200,7 +222,8 @@ function projectAabbToScreen(box, viewProjection, rect) {
200
222
  }
201
223
  }
202
224
  if (!any) return null;
203
- return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
225
+ const out = { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
226
+ return Number.isFinite(out.x + out.y + out.width + out.height) ? out : null;
204
227
  }
205
228
  function rectOf(viewport) {
206
229
  return {
@@ -217,8 +240,9 @@ function rayAt(viewport, point) {
217
240
  return rayThroughScreenPoint(point, rectOf(viewport), viewProjectionOf(viewport));
218
241
  }
219
242
  function projectPoint(p, viewport) {
243
+ if (!(viewport.width > 0 && viewport.height > 0)) return null;
220
244
  const [x, y, z, w] = transformPoint4(viewProjectionOf(viewport), p);
221
- if (!(w > 0) || z < -w) return null;
245
+ if (!(w > 0) || !(z >= -w)) return null;
222
246
  const screen = ndcToScreen({ x: x / w, y: y / w }, rectOf(viewport));
223
247
  return Number.isFinite(screen.x) && Number.isFinite(screen.y) ? screen : null;
224
248
  }
@@ -257,8 +281,8 @@ function worldPoses(scene, reading) {
257
281
  return (id) => frame.world(id);
258
282
  }
259
283
  const adapter = scenePoseAdapter(untyped);
260
- const compose2 = scene.composition.compose;
261
- return (id) => composeWorldPose(adapter, id, compose2);
284
+ const compose3 = scene.composition.compose;
285
+ return (id) => composeWorldPose(adapter, id, compose3);
262
286
  }
263
287
  function boxAt(world, node, pose) {
264
288
  if (world.bounds) return world.bounds(pose === node.pose ? node : { ...node, pose });
@@ -517,7 +541,7 @@ function createSession3d(inputs) {
517
541
  return {
518
542
  at(p, mods) {
519
543
  if (!frame) return unsnapped(p);
520
- const vp = world.viewport();
544
+ const vp = world.viewport(inputs.viewId);
521
545
  const screen = frame.anchor(vp);
522
546
  const anchor = screen && castOnto(vp, screen, up, frame.offset);
523
547
  const hit = screen && castOnto(vp, { x: screen.x + p.x - req.press.x, y: screen.y + p.y - req.press.y }, up, frame.offset);
@@ -532,16 +556,17 @@ function createSession3d(inputs) {
532
556
  store.set([]);
533
557
  } else {
534
558
  const probes = frame.probes(anchor);
535
- const res = resolveSnap({
536
- probes: probes.map((q) => ({ id: q.id, at: tuple(q.at), role: q.role })),
559
+ const at = probes.map((q) => ({ id: q.id, at: tuple(q.at), role: q.role }));
560
+ const res = resolveHolding(candidatesOf(inputs, sources, probes, delta0, vp, origin, up), (kept) => resolveSnap({
561
+ probes: at,
537
562
  input,
538
563
  delta0: tuple(delta0),
539
- candidates: candidatesOf(inputs, sources, probes, delta0, vp, origin, up),
564
+ candidates: kept,
540
565
  toScreen: (w) => {
541
566
  const s = projectPoint(vec3Of(w), vp);
542
567
  return s ? [s.x, s.y] : [NaN, NaN];
543
568
  }
544
- });
569
+ }), vec3Of);
545
570
  delta = vec3Of(res.delta);
546
571
  held = res.members.length > 0;
547
572
  store.set(resolvedSnapMarks(res.satisfied, delta));
@@ -632,16 +657,20 @@ function movedOnLevel(pose, delta, upAxis) {
632
657
  const position = { ...add(pose.position, delta), [upAxis]: pose.position[upAxis] };
633
658
  return { ...pose, position };
634
659
  }
635
- function createNodeAtPoint(world) {
636
- return (point, exclude) => {
637
- const ray = rayAt(world.viewport(), point);
638
- if (!ray) return null;
639
- return worldPickIndex(world).nearestOnRay(ray, exclude ? new Set(exclude) : null, 1);
640
- };
660
+ function createNodeAtPoint(world, viewId) {
661
+ return (point, exclude) => pickOnRay(world, viewId, point, exclude);
662
+ }
663
+ function pickOnRay(world, viewId, point, exclude) {
664
+ const ray = rayAt(world.viewport(viewId), point);
665
+ if (!ray) return null;
666
+ return worldPickIndex(world).nearestOnRay(ray, exclude ? new Set(exclude) : null, 1);
667
+ }
668
+ function createPickEvery(world) {
669
+ return (point, view) => pickOnRay(world, view?.viewId, point);
641
670
  }
642
- function createAreaSelect(world, selection) {
671
+ function createAreaSelect(world, selection, viewId) {
643
672
  return {
644
- hitTestArea: (bounds) => worldPickIndex(world).inScreenRect(world.viewport(), bounds),
673
+ hitTestArea: (bounds) => worldPickIndex(world).inScreenRect(world.viewport(viewId), bounds),
645
674
  getSelection: () => [...selection.get()],
646
675
  setSelection: (ids) => selection.set([...ids])
647
676
  };
@@ -668,25 +697,26 @@ function createSnap(world, opts = {}) {
668
697
  world,
669
698
  sources,
670
699
  store,
700
+ viewId: opts.viewId,
671
701
  reachPx: opts.reachPx ?? opts.kitSettings?.get("targets.snapPx") ?? DEFAULT_SNAP_PX
672
702
  });
673
703
  }
674
704
  };
675
705
  }
676
- function createPoseDescriptor(world) {
706
+ function createPoseDescriptor(world, viewId) {
677
707
  const up = world.up ?? DEFAULT_UP;
678
708
  const upAxis = groundAxes(up).up;
679
709
  const specialized = /* @__PURE__ */ new WeakMap();
680
710
  let lastCastable = null;
681
711
  function castableViewport() {
682
- const vp = world.viewport();
712
+ const vp = world.viewport(viewId);
683
713
  const rect = rectOf(vp);
684
714
  if (rayAt(vp, { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 })) lastCastable = vp;
685
715
  return lastCastable;
686
716
  }
687
717
  function build3(worldBox) {
688
718
  const boundsIn = (vp, pose) => projectAabbToScreen(worldBox(pose), viewProjectionOf(vp), rectOf(vp));
689
- const boundsOf = (pose) => boundsIn(world.viewport(), pose);
719
+ const boundsOf = (pose) => boundsIn(world.viewport(viewId), pose);
690
720
  const descriptor = {
691
721
  forNode(node) {
692
722
  let d = specialized.get(node);
@@ -719,8 +749,8 @@ function createPoseDescriptor(world) {
719
749
  const ky = src.height === 0 ? 1 : dst.height / src.height;
720
750
  const from = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
721
751
  const to = {
722
- x: dst.x + (from.x - src.x) * kx,
723
- y: dst.y + (from.y - src.y) * ky
752
+ x: from.x + (dst.x - src.x) + (from.x - src.x) * (kx - 1),
753
+ y: from.y + (dst.y - src.y) + (from.y - src.y) * (ky - 1)
724
754
  };
725
755
  const plane = viewPlaneAt(vp.camera, pose.position);
726
756
  const before = onPlane(vp, plane, from);
@@ -824,7 +854,7 @@ function createInsert(opts) {
824
854
  const { u, v } = groundAxes(up);
825
855
  return {
826
856
  commit(bounds, extras, ends, select) {
827
- const vp = opts.viewport();
857
+ const vp = opts.viewport(opts.viewId);
828
858
  const hits = footprintPoints(bounds, ends, (p) => groundHit(vp, p, up));
829
859
  if (hits.length === 0) return null;
830
860
  let minU = Infinity, maxU = -Infinity, minV = Infinity, maxV = -Infinity;
@@ -940,7 +970,7 @@ function aimSpace3d(viewport, viewId, opts = {}) {
940
970
  },
941
971
  unproject(px) {
942
972
  const hit = groundHit(viewport, { x: px.x + ox, y: px.y + oy }, up);
943
- return hit && toAimPoint(hit, axes);
973
+ return hit && { ...toAimPoint(hit, axes), z: 0 };
944
974
  }
945
975
  };
946
976
  }
@@ -949,8 +979,8 @@ function cameraOfAim3d(aim) {
949
979
  }
950
980
  var AXES = ["x", "y", "z"];
951
981
  var BITS = [1, 2, 4];
952
- var UNIT = { x: { x: 1, y: 0, z: 0 }, y: { x: 0, y: 1, z: 0 }, z: { x: 0, y: 0, z: 1 } };
953
- var ACROSS = { x: [UNIT.y, UNIT.z], y: [UNIT.x, UNIT.z], z: [UNIT.x, UNIT.y] };
982
+ var UNIT2 = { x: { x: 1, y: 0, z: 0 }, y: { x: 0, y: 1, z: 0 }, z: { x: 0, y: 0, z: 1 } };
983
+ var ACROSS = { x: [UNIT2.y, UNIT2.z], y: [UNIT2.x, UNIT2.z], z: [UNIT2.x, UNIT2.y] };
954
984
  function boxesNear(world, q, r) {
955
985
  return snapBoxes(world, q.exclude, q.probes.map(({ at: p }) => padQuery([p.x - r, p.y - r, p.z - r, p.x + r, p.y + r, p.z + r])));
956
986
  }
@@ -1067,7 +1097,9 @@ function groundGridSource(opts) {
1067
1097
  const up = normalize(opts.up ?? DEFAULT_UP);
1068
1098
  const [u, v] = planeBasis(up);
1069
1099
  const lines = (c) => {
1070
- const near = Math.round(c / opts.step) * opts.step;
1100
+ const k = Math.round(c / opts.step);
1101
+ if (!Number.isSafeInteger(k)) return [];
1102
+ const near = k * opts.step;
1071
1103
  return [near, near + (c >= near ? opts.step : -opts.step)];
1072
1104
  };
1073
1105
  return {
@@ -1134,9 +1166,10 @@ var HEAD = { lenRatio: 8, maxShare: 0.5, aspect: 7 / 16 };
1134
1166
  var HEAD_OVERDRAW = 0.1;
1135
1167
  function headFor(shaftHalf, linePx = Number.POSITIVE_INFINITY, head = HEAD) {
1136
1168
  const wanted = shaftHalf * head.lenRatio;
1137
- const headLen = Math.max(0, Math.min(wanted, head.maxShare * linePx));
1169
+ const cap = head.maxShare === 0 ? 0 : head.maxShare * linePx;
1170
+ const headLen = Math.max(0, Math.min(wanted, cap));
1138
1171
  const headHalf = headLen * head.aspect;
1139
- const shrunk = wanted > 0 ? shaftHalf * headLen / wanted : shaftHalf;
1172
+ const shrunk = wanted > 0 ? shaftHalf * (headLen / wanted) : shaftHalf;
1140
1173
  return { headLen, headHalf, shaftHalf: wanted > 0 ? Math.min(shrunk, headHalf) : shrunk };
1141
1174
  }
1142
1175
  function layoutLine(shaftHalf, length, pxPerUnit, head) {
@@ -1314,7 +1347,7 @@ void main() {
1314
1347
  if (wanted > 0.0 && density > 0.0) {
1315
1348
  headPx = max(0.0, min(wanted, a_shape.z * density * total));
1316
1349
  headHalf = headPx * a_shape.w;
1317
- shaft = min(shaftHalf * headPx / wanted, headHalf);
1350
+ shaft = min(shaftHalf * (headPx / wanted), headHalf);
1318
1351
  shaftEnd = total - headPx / density * (1.0 - OVERDRAW);
1319
1352
  }
1320
1353
 
@@ -1474,10 +1507,11 @@ function cameraStepper(dep, zero, relative, step) {
1474
1507
  let reached = zero;
1475
1508
  let written = origin;
1476
1509
  let prior = origin;
1510
+ let caughtUp = false;
1477
1511
  return (at) => {
1478
1512
  if (!dep || !origin) return;
1479
1513
  const now = dep.get();
1480
- if (now !== written && now !== prior) {
1514
+ if (now !== written && (caughtUp || now !== prior)) {
1481
1515
  origin = now;
1482
1516
  from = reached;
1483
1517
  }
@@ -1485,9 +1519,11 @@ function cameraStepper(dep, zero, relative, step) {
1485
1519
  written = step(origin, relative(at, from), dep);
1486
1520
  reached = at;
1487
1521
  dep.set(written);
1522
+ caughtUp = dep.get() === written;
1488
1523
  };
1489
1524
  }
1490
1525
  function dragCamera(ctx, kind, step) {
1526
+ if (!ctx.deps.camera3d) return {};
1491
1527
  const apply = cameraStepper(ctx.deps.camera3d, { x: 0, y: 0 }, (a, b) => ({ x: a.x - b.x, y: a.y - b.y }), step);
1492
1528
  return {
1493
1529
  kind,
@@ -1550,13 +1586,14 @@ var dollyAction = {
1550
1586
  timing: "immediate",
1551
1587
  run(deps, params) {
1552
1588
  const dep = deps.camera3d;
1553
- if (!dep) return;
1589
+ if (!dep) return "pass";
1554
1590
  const factor = typeof params?.scale === "number" ? 1 / params.scale : Math.exp(
1555
1591
  (typeof params?.deltaY === "number" ? params.deltaY : 0) * (deps.kitSettings?.get("orbit3d.dollyRate") ?? DOLLY_RATE)
1556
1592
  );
1557
- if (!(factor > 0) || !Number.isFinite(factor)) return;
1593
+ if (!(factor > 0) || !Number.isFinite(factor) || factor === 1) return "pass";
1558
1594
  const pane = dep.size();
1559
1595
  dep.set(dollyToward(dep.get(), factor, paneAnchor(wheelPoint(params), dep, pane), pane, dep.limits?.()));
1596
+ return void 0;
1560
1597
  }
1561
1598
  }
1562
1599
  };
@@ -1570,6 +1607,7 @@ var pinchDollyAction = {
1570
1607
  timing: "ongoing",
1571
1608
  start(ctx) {
1572
1609
  const dep = ctx.deps.camera3d;
1610
+ if (!dep) return {};
1573
1611
  const startSpread = ctx.multiTouch?.spread ?? 0;
1574
1612
  const at = ctx.multiTouch?.centroid;
1575
1613
  const apply = cameraStepper(dep, 1, (a, b) => a / b, (origin, factor, d) => {
@@ -1598,7 +1636,9 @@ var homeAction = {
1598
1636
  run(deps) {
1599
1637
  const dep = deps.camera3d;
1600
1638
  const home = dep?.home?.();
1601
- if (dep && home) dep.set(home);
1639
+ if (!dep || !home) return "pass";
1640
+ dep.set(home);
1641
+ return void 0;
1602
1642
  }
1603
1643
  }
1604
1644
  };
@@ -1621,6 +1661,6 @@ var kernel3dKitSettingsSchema = pref.group("3D camera", {
1621
1661
  })
1622
1662
  });
1623
1663
 
1624
- export { DEFAULT_ORBIT_LIMITS, HEAD, HEAD_OVERDRAW, LINE_FLOATS, MAX_PITCH, POSE3_COMPOSITION, UNIT_CUBE, aabbOfPose, aimOf3d, aimSpace3d, alignPlaneSource, boxSnapSource, cameraEye, cameraOfAim3d, cameraView, cameraViewProjection, clampCamera, collectOverlayBoxes, createAreaSelect, createCamera, createInsert, createLinePass, createNodeAtPoint, createPoseDescriptor, createSnap, dollyAction, dollyBy, dollyToward, groundGridSource, headFor, homeAction, kernel3dKitSettingsSchema, layoutLine, ndcToScreen, orbitAction, orbitBy, packLines, panAction, panBy, pinchDollyAction, pose3, poseMatrix, projectAabbToScreen, projectPoint, projectSnapMarks, rayThroughScreenPoint, screenBoxOf, screenToNdc, useOrbitTool, visibleRuns, worldPoseOf };
1664
+ export { DEFAULT_ORBIT_LIMITS, HEAD, HEAD_OVERDRAW, LINE_FLOATS, MAX_PITCH, POSE3_COMPOSITION, UNIT_CUBE, aabbOfPose, aimOf3d, aimSpace3d, alignPlaneSource, boxSnapSource, cameraEye, cameraOfAim3d, cameraView, cameraViewProjection, clampCamera, collectOverlayBoxes, createAreaSelect, createCamera, createInsert, createLinePass, createNodeAtPoint, createPickEvery, createPoseDescriptor, createSnap, dollyAction, dollyBy, dollyToward, groundGridSource, headFor, homeAction, kernel3dKitSettingsSchema, layoutLine, ndcToScreen, orbitAction, orbitBy, packLines, panAction, panBy, pinchDollyAction, pose3, poseMatrix, projectAabbToScreen, projectPoint, projectSnapMarks, rayThroughScreenPoint, screenBoxOf, screenToNdc, useOrbitTool, visibleRuns, worldPoseOf };
1625
1665
  //# sourceMappingURL=index.js.map
1626
1666
  //# sourceMappingURL=index.js.map