@weasel-js/kernel3d 1.18.2 → 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
  /**
@@ -219,8 +221,8 @@ type Point$1 = {
219
221
  x: number;
220
222
  y: number;
221
223
  };
222
- /** Where `p` draws in the viewport, or null when it is behind the near plane
223
- * 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. */
224
226
  declare function projectPoint(p: Vec3, viewport: Viewport3d): Point$1 | null;
225
227
 
226
228
  /** The 3D world every dep is built from: core's scene with a `Pose3`, the
@@ -368,6 +370,8 @@ interface Footprint {
368
370
  */
369
371
  declare function createInsert(opts: {
370
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;
371
375
  up?: Vec3;
372
376
  /** Smallest footprint a click-sized drag produces. Default 0.25. */
373
377
  minExtent?: number;
@@ -439,8 +443,9 @@ interface GroundGridSourceOptions {
439
443
  /**
440
444
  * A grid on the ground plane as snap targets: for each pointer or geometry
441
445
  * probe on the ground, the grid line on either side of it along each of the
442
- * ground's two axes. Its reach is unlimited and it is a fallback, so it
443
- * 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.
444
449
  */
445
450
  declare function groundGridSource(opts: GroundGridSourceOptions): SnapSource<Vec3>;
446
451
 
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 });
@@ -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));
@@ -724,8 +749,8 @@ function createPoseDescriptor(world, viewId) {
724
749
  const ky = src.height === 0 ? 1 : dst.height / src.height;
725
750
  const from = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
726
751
  const to = {
727
- x: dst.x + (from.x - src.x) * kx,
728
- 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)
729
754
  };
730
755
  const plane = viewPlaneAt(vp.camera, pose.position);
731
756
  const before = onPlane(vp, plane, from);
@@ -829,7 +854,7 @@ function createInsert(opts) {
829
854
  const { u, v } = groundAxes(up);
830
855
  return {
831
856
  commit(bounds, extras, ends, select) {
832
- const vp = opts.viewport();
857
+ const vp = opts.viewport(opts.viewId);
833
858
  const hits = footprintPoints(bounds, ends, (p) => groundHit(vp, p, up));
834
859
  if (hits.length === 0) return null;
835
860
  let minU = Infinity, maxU = -Infinity, minV = Infinity, maxV = -Infinity;
@@ -945,7 +970,7 @@ function aimSpace3d(viewport, viewId, opts = {}) {
945
970
  },
946
971
  unproject(px) {
947
972
  const hit = groundHit(viewport, { x: px.x + ox, y: px.y + oy }, up);
948
- return hit && toAimPoint(hit, axes);
973
+ return hit && { ...toAimPoint(hit, axes), z: 0 };
949
974
  }
950
975
  };
951
976
  }
@@ -954,8 +979,8 @@ function cameraOfAim3d(aim) {
954
979
  }
955
980
  var AXES = ["x", "y", "z"];
956
981
  var BITS = [1, 2, 4];
957
- var UNIT = { x: { x: 1, y: 0, z: 0 }, y: { x: 0, y: 1, z: 0 }, z: { x: 0, y: 0, z: 1 } };
958
- 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] };
959
984
  function boxesNear(world, q, r) {
960
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])));
961
986
  }
@@ -1072,7 +1097,9 @@ function groundGridSource(opts) {
1072
1097
  const up = normalize(opts.up ?? DEFAULT_UP);
1073
1098
  const [u, v] = planeBasis(up);
1074
1099
  const lines = (c) => {
1075
- 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;
1076
1103
  return [near, near + (c >= near ? opts.step : -opts.step)];
1077
1104
  };
1078
1105
  return {
@@ -1139,9 +1166,10 @@ var HEAD = { lenRatio: 8, maxShare: 0.5, aspect: 7 / 16 };
1139
1166
  var HEAD_OVERDRAW = 0.1;
1140
1167
  function headFor(shaftHalf, linePx = Number.POSITIVE_INFINITY, head = HEAD) {
1141
1168
  const wanted = shaftHalf * head.lenRatio;
1142
- 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));
1143
1171
  const headHalf = headLen * head.aspect;
1144
- const shrunk = wanted > 0 ? shaftHalf * headLen / wanted : shaftHalf;
1172
+ const shrunk = wanted > 0 ? shaftHalf * (headLen / wanted) : shaftHalf;
1145
1173
  return { headLen, headHalf, shaftHalf: wanted > 0 ? Math.min(shrunk, headHalf) : shrunk };
1146
1174
  }
1147
1175
  function layoutLine(shaftHalf, length, pxPerUnit, head) {
@@ -1319,7 +1347,7 @@ void main() {
1319
1347
  if (wanted > 0.0 && density > 0.0) {
1320
1348
  headPx = max(0.0, min(wanted, a_shape.z * density * total));
1321
1349
  headHalf = headPx * a_shape.w;
1322
- shaft = min(shaftHalf * headPx / wanted, headHalf);
1350
+ shaft = min(shaftHalf * (headPx / wanted), headHalf);
1323
1351
  shaftEnd = total - headPx / density * (1.0 - OVERDRAW);
1324
1352
  }
1325
1353
 
@@ -1479,10 +1507,11 @@ function cameraStepper(dep, zero, relative, step) {
1479
1507
  let reached = zero;
1480
1508
  let written = origin;
1481
1509
  let prior = origin;
1510
+ let caughtUp = false;
1482
1511
  return (at) => {
1483
1512
  if (!dep || !origin) return;
1484
1513
  const now = dep.get();
1485
- if (now !== written && now !== prior) {
1514
+ if (now !== written && (caughtUp || now !== prior)) {
1486
1515
  origin = now;
1487
1516
  from = reached;
1488
1517
  }
@@ -1490,6 +1519,7 @@ function cameraStepper(dep, zero, relative, step) {
1490
1519
  written = step(origin, relative(at, from), dep);
1491
1520
  reached = at;
1492
1521
  dep.set(written);
1522
+ caughtUp = dep.get() === written;
1493
1523
  };
1494
1524
  }
1495
1525
  function dragCamera(ctx, kind, step) {