@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/README.md CHANGED
@@ -36,6 +36,18 @@ One thing does not:
36
36
  - **`ViewApi` has no orientation**, so an orbit camera cannot travel through
37
37
  the kit's `view`. The kernel declares a `camera3d` dep of its own.
38
38
 
39
+ ## The camera's actions
40
+
41
+ `camera.orbit` turns the `camera3d` dep on a drag, `camera.pan` slides its
42
+ target across the screen plane, `camera.dolly` answers the wheel and both
43
+ trackpad pinches, `camera.pinchDolly` two fingers, and `camera.home` returns it
44
+ to the dep's `home()`. Both dollies go through `dollyToward`, which keeps the
45
+ point in the target's plane under the pointer still, as a 2D wheel zoom does,
46
+ measured against the dep's `size()`. Each holds the camera inside the dep's `limits()` — an
47
+ `OrbitLimits` in the scene's own units, defaulting to `DEFAULT_ORBIT_LIMITS` —
48
+ through `clampCamera`, which `orbitBy` and `dollyBy` take as well. labkit's
49
+ `orbit` capability is these actions on a dep the trial owns.
50
+
39
51
  ## The depth a screen rectangle does not name
40
52
 
41
53
  `PoseDescriptor` reads `Bounds` as the screen box a solid covers. `getBounds`
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { Vec3, Mat4, Quat, Aabb, Ray } from '@weasel-js/geom/3d';
2
- import { SceneNode, Scene, NodeId, AreaSelectDep, InsertExtras, InsertDep, NodeAtPointDep, PoseDescriptor, SnapDep, OngoingHandle, Action, Tool } from '@weasel-js/core';
2
+ import { SceneNode, Scene, SnapSource, SnapOutcomeStore, NodeId, AreaSelectDep, NodeAtPointDep, PoseDescriptor, SnapDep, InsertExtras, InsertDep, SnapMark, OngoingHandle, Action, Tool } from '@weasel-js/core';
3
+ import { KitSettingsStore } from '@weasel-js/react';
4
+ import * as _weasel_js_prefs from '@weasel-js/prefs';
3
5
 
4
6
  /**
5
7
  * An orbit camera: a target, two angles and a distance. Immutable — every
@@ -34,11 +36,45 @@ declare function cameraEye(camera: Camera3d): Vec3;
34
36
  declare function cameraView(camera: Camera3d): Mat4;
35
37
  /** World-to-clip matrix for a pane of the given width/height `aspect`. */
36
38
  declare function cameraViewProjection(camera: Camera3d, aspect: number): Mat4;
37
- /** The camera turned about its target by the given radians. Pitch is clamped
38
- * to ±`MAX_PITCH`; yaw is not wrapped. */
39
- declare function orbitBy(camera: Camera3d, deltaYaw: number, deltaPitch: number): Camera3d;
40
- /** The camera with its distance multiplied by `factor`, clamped to 0.5..500. */
41
- declare function dollyBy(camera: Camera3d, factor: number): Camera3d;
39
+ /** How far an orbit camera may turn and travel. Distances are in world units,
40
+ * so a scene measured in millimeters and one in kilometers want different ones. */
41
+ interface OrbitLimits {
42
+ minDistance: number;
43
+ maxDistance: number;
44
+ /** Radians. Held inside ±`MAX_PITCH` whatever is asked for. */
45
+ minPitch: number;
46
+ maxPitch: number;
47
+ }
48
+ /** Pitch short of the poles, distance 0.5..500. */
49
+ declare const DEFAULT_ORBIT_LIMITS: OrbitLimits;
50
+ /** The camera with its pitch and distance inside `limits`. Returns `camera`
51
+ * itself when it already holds. */
52
+ declare function clampCamera(camera: Camera3d, limits?: Partial<OrbitLimits>): Camera3d;
53
+ /** The camera turned about its target by the given radians, pitch held inside
54
+ * `limits`. Yaw is not wrapped. */
55
+ declare function orbitBy(camera: Camera3d, deltaYaw: number, deltaPitch: number, limits?: Partial<OrbitLimits>): Camera3d;
56
+ /** The camera with its distance multiplied by `factor`, held inside `limits`. */
57
+ declare function dollyBy(camera: Camera3d, factor: number, limits?: Partial<OrbitLimits>): Camera3d;
58
+ /**
59
+ * The camera with its target slid across the screen plane, so the point at the
60
+ * target follows a pointer that moved `dx`, `dy` CSS pixels in a pane
61
+ * `paneHeight` pixels tall. Neither angle nor distance changes.
62
+ */
63
+ declare function panBy(camera: Camera3d, dx: number, dy: number, paneHeight: number): Camera3d;
64
+ /**
65
+ * `dollyBy` about a point on the pane rather than its middle: the point in the
66
+ * target's screen-facing plane under `at` — CSS pixels from the pane's
67
+ * top-left, in a pane of `pane` size — stays under it, as a 2D camera's wheel
68
+ * zoom keeps the point under the pointer. The target slides toward that point
69
+ * by as much as the distance shrank, so the next turn pivots nearer it.
70
+ */
71
+ declare function dollyToward(camera: Camera3d, factor: number, at: {
72
+ x: number;
73
+ y: number;
74
+ }, pane: {
75
+ width: number;
76
+ height: number;
77
+ }, limits?: Partial<OrbitLimits>): Camera3d;
42
78
 
43
79
  /**
44
80
  * A 3D pose, and the two things every consumer of one needs: its matrix, and
@@ -137,14 +173,6 @@ declare function rayThroughScreenPoint(point: {
137
173
  */
138
174
  declare function projectAabbToScreen(box: Aabb, viewProjection: Mat4, rect: ScreenBox): ScreenBox | null;
139
175
 
140
- /**
141
- * The kit's deps, implemented against a 3D world.
142
- *
143
- * Every one of these takes its camera from a thunk rather than an argument:
144
- * the dispatcher keeps handing actions two numbers, and what makes them mean
145
- * something in 3D is the camera each dep already closes over.
146
- */
147
-
148
176
  /** The camera and the pane it draws into; `width`/`height` are the pane's
149
177
  * size in the same units as the pointer points. */
150
178
  interface Viewport3d {
@@ -161,6 +189,17 @@ interface Viewport3d {
161
189
  }
162
190
  /** Read on every dep call, so a dep always sees the current camera and pane. */
163
191
  type ViewportSource = () => Viewport3d;
192
+ type Point$1 = {
193
+ x: number;
194
+ y: number;
195
+ };
196
+ /** Where `p` draws in the viewport, or null when it is behind the near plane
197
+ * or projects to nothing finite. */
198
+ declare function projectPoint(p: Vec3, viewport: Viewport3d): Point$1 | null;
199
+
200
+ /** The 3D world every dep is built from: core's scene with a `Pose3`, the
201
+ * viewport it is seen through, and how wide each node is. */
202
+
164
203
  /** A 3D scene is core's scene with a `Pose3`. Nothing wraps it. */
165
204
  type Scene3d<TData, TLayer extends string> = Scene<TData, TLayer, Pose3>;
166
205
  /** A node of a `Scene3d`. */
@@ -182,6 +221,15 @@ interface World3d<TData, TLayer extends string> {
182
221
  /** The axis gestures resolve against. Default `+y`. */
183
222
  up?: Vec3;
184
223
  }
224
+
225
+ /**
226
+ * The kit's deps, implemented against a 3D world.
227
+ *
228
+ * Every one of these takes its camera from a thunk rather than an argument:
229
+ * the dispatcher keeps handing actions two numbers, and what makes them mean
230
+ * something in 3D is the camera each dep already closes over.
231
+ */
232
+
185
233
  /** The screen box a node covers, or null when it projects to nothing visible. */
186
234
  declare function screenBoxOf<TData, TLayer extends string>(world: World3d<TData, TLayer>, id: NodeId, viewport: Viewport3d): ScreenBox | null;
187
235
  /**
@@ -197,8 +245,46 @@ declare function createAreaSelect<TData, TLayer extends string>(world: World3d<T
197
245
  get(): readonly NodeId[];
198
246
  set(ids: NodeId[]): void;
199
247
  }): AreaSelectDep;
200
- /** No grid in 3D yet; the identity keeps `insertAction` happy without lying. */
201
- declare function createSnap(): SnapDep;
248
+ /** Options for `createSnap`. */
249
+ interface Snap3dOptions {
250
+ /** What a gesture snaps to, in order: earlier wins a full tie. Two sharing
251
+ * an id throw. Default none, which leaves only the gesture's own constraint. */
252
+ sources?: readonly SnapSource<Vec3>[];
253
+ /** The reach of a source that sets none, in screen px. Overrides the kit
254
+ * setting `targets.snapPx`. */
255
+ reachPx?: number;
256
+ /** Where `targets.snapPx` is read, when each gesture begins. Absent, and
257
+ * with no `reachPx`, reach is `DEFAULT_SNAP_PX`. */
258
+ kitSettings?: KitSettingsStore;
259
+ /** Where the marks of the snap in flight are written, for a host to draw
260
+ * with `projectSnapMarks`. */
261
+ store?: SnapOutcomeStore<Vec3>;
262
+ }
263
+ /**
264
+ * The `snap` dep over a 3D world. A gesture's `omit` is honored; its 2D
265
+ * `sources` are not asked. A move snaps its subject's world box on the
266
+ * plane it already moves on; an insert snaps the pointer on the ground. Both
267
+ * answer in screen points, which `translate` and `createInsert` cast back
268
+ * onto the same plane, landing exactly where the snap put them.
269
+ */
270
+ declare function createSnap<TData, TLayer extends string>(world: World3d<TData, TLayer>, opts?: Snap3dOptions): SnapDep;
271
+ /**
272
+ * `PoseDescriptor` over a 3D pose, with `Bounds` read as the screen box the
273
+ * node covers.
274
+ *
275
+ * `forNode` is what lets a sphere bound itself as a sphere: the shape lives
276
+ * on `node.data`. Unspecialized — the kit calling with only a pose in hand —
277
+ * it answers with the default local box, which is the looser of the two.
278
+ *
279
+ * `remapBounds` and `fromBounds` take a screen rectangle, which names a pose
280
+ * only once something supplies a depth. Both resolve it against the plane
281
+ * through the pose they were handed, facing the camera, so nothing they
282
+ * produce changes depth.
283
+ */
284
+ declare function createPoseDescriptor<TData, TLayer extends string>(world: World3d<TData, TLayer>): PoseDescriptor<Pose3, Node3d<TData, TLayer>>;
285
+
286
+ /** The `insert` dep over a 3D world: a drag becomes a footprint on the ground. */
287
+
202
288
  /** The ground-plane rectangle a drag swept out, in world units. */
203
289
  interface Footprint {
204
290
  /** Centre of the swept rectangle, on the ground plane. */
@@ -208,14 +294,11 @@ interface Footprint {
208
294
  depth: number;
209
295
  }
210
296
  /**
211
- * A drag rectangle becomes a footprint on the ground plane: its four corners
212
- * are cast onto the plane and the hit points give the extent. What node that
213
- * implies is the consumer's answer — the kernel does not know what it is
214
- * inserting.
215
- *
216
- * The kit's `InsertDep.commit` hands over a screen-space AABB and nothing else:
217
- * the depth the 2D contract cannot express comes from the plane, not from the
218
- * caller.
297
+ * A drag becomes a footprint on the ground plane: its ends, or without them
298
+ * its box's four corners, are cast onto the plane and the hit points give the
299
+ * extent. What node that implies is the consumer's answer — the kernel does
300
+ * not know what it is inserting. The depth the 2D contract cannot express
301
+ * comes from the plane, not from the caller.
219
302
  */
220
303
  declare function createInsert(opts: {
221
304
  viewport: ViewportSource;
@@ -224,20 +307,67 @@ declare function createInsert(opts: {
224
307
  minExtent?: number;
225
308
  mint(footprint: Footprint, extras: InsertExtras): NodeId | null;
226
309
  }): InsertDep;
310
+
311
+ /** What a 3D scene offers a snap session: its boxes' corners, edges, faces
312
+ * and centers, the planes through their sides and centers, and a grid on
313
+ * the ground. */
314
+
227
315
  /**
228
- * `PoseDescriptor` over a 3D pose, with `Bounds` read as the screen box the
229
- * node covers.
230
- *
231
- * `forNode` is what lets a sphere bound itself as a sphere: the shape lives
232
- * on `node.data`. Unspecialized — the kit calling with only a pose in hand —
233
- * it answers with the default local box, which is the looser of the two.
234
- *
235
- * `remapBounds` and `fromBounds` take a screen rectangle, which names a pose
236
- * only once something supplies a depth. Both resolve it against the plane
237
- * through the pose they were handed, facing the camera, so nothing they
238
- * produce changes depth.
316
+ * Each node's world box as targets for geometry and pointer probes: its 8
317
+ * corners and its center as points, its 12 edges as segments, and its 6
318
+ * faces as bounded planes. Targets past the reach are not offered.
239
319
  */
240
- declare function createPoseDescriptor<TData, TLayer extends string>(world: World3d<TData, TLayer>): PoseDescriptor<Pose3, Node3d<TData, TLayer>>;
320
+ declare function boxSnapSource<TData, TLayer extends string>(world: World3d<TData, TLayer>): SnapSource<Vec3>;
321
+ /**
322
+ * The planes through each node's world box at its min, center and max on
323
+ * every axis, as targets for box and pointer probes: alignment in depth as
324
+ * well as across. Boxes sharing a plane offer it once, and its mark reaches
325
+ * each of them.
326
+ */
327
+ declare function alignPlaneSource<TData, TLayer extends string>(world: World3d<TData, TLayer>): SnapSource<Vec3>;
328
+ /** Options for `groundGridSource`. */
329
+ interface GroundGridSourceOptions {
330
+ /** The grid's pitch, in world units. */
331
+ step: number;
332
+ /** The ground's normal; the grid lies on `up · p = 0`. Default `+y`. */
333
+ up?: Vec3;
334
+ }
335
+ /**
336
+ * A grid on the ground plane as snap targets: for each pointer or geometry
337
+ * probe on the ground, the grid line on either side of it along each of the
338
+ * ground's two axes. Its reach is unlimited and it is a fallback, so it
339
+ * holds every axis no target claims and never displaces one.
340
+ */
341
+ declare function groundGridSource(opts: GroundGridSourceOptions): SnapSource<Vec3>;
342
+
343
+ type Point = {
344
+ x: number;
345
+ y: number;
346
+ };
347
+ /** A snap mark's segment on screen: a line, a gap, or a distance. */
348
+ interface ScreenLine {
349
+ from: Point;
350
+ to: Point;
351
+ /** A gap's or distance's value, in world units. */
352
+ value?: number;
353
+ color?: string;
354
+ }
355
+ /** A snap mark's point on screen. */
356
+ interface ScreenPoint {
357
+ at: Point;
358
+ glyph: string;
359
+ label?: string;
360
+ color?: string;
361
+ }
362
+ /**
363
+ * The marks a 3D snap session draws, in the viewport's screen space, for a
364
+ * host to draw through its own renderer. A mark with any point that does not
365
+ * project — behind the camera — is dropped whole.
366
+ */
367
+ declare function projectSnapMarks(marks: readonly SnapMark<Vec3>[], viewport: Viewport3d): {
368
+ lines: ScreenLine[];
369
+ points: ScreenPoint[];
370
+ };
241
371
 
242
372
  /**
243
373
  * What a host paints for a gesture that proposes chrome rather than a pose.
@@ -269,13 +399,26 @@ declare function collectOverlayBoxes(handles: Iterable<OngoingHandle>, origin: {
269
399
  */
270
400
 
271
401
  /** `camera.orbit`: an ongoing drag that turns the `camera3d` dep about its
272
- * target, relative to the camera as it was when the drag began. */
402
+ * target. */
273
403
  declare const orbitAction: Action;
274
- /** `camera.dolly`: moves the `camera3d` dep toward or away from its target by
275
- * the wheel's `deltaY`. Bound to the wheel by default, in every tool. */
404
+ /** `camera.pan`: an ongoing drag that slides the `camera3d` dep's target
405
+ * across the screen plane, so the point at the target follows the pointer. */
406
+ declare const panAction: Action;
407
+ /** `camera.dolly`: moves the `camera3d` dep in or out about the point under
408
+ * the pointer, as a 2D camera's wheel zooms about it. A wheel's `deltaY`, and
409
+ * a trackpad pinch delivered as ctrl+wheel, dolly by `exp(deltaY × rate)` —
410
+ * one rate for both, as a 2D camera zooms by both. Safari's pinch arrives as
411
+ * a `scale` and divides the distance by it. Bound to all three by default, in
412
+ * every tool. */
276
413
  declare const dollyAction: Action;
277
- /** A tool that binds drag to `camera.orbit`. Register `orbitAction` alongside
278
- * it; the tool only declares the binding. */
414
+ /** `camera.pinchDolly`: two fingers spreading dolly the `camera3d` dep in and
415
+ * closing dolly it out, by the ratio of their spread, about where they began. */
416
+ declare const pinchDollyAction: Action;
417
+ /** `camera.home`: returns the `camera3d` dep to its `home`. Bound to a double
418
+ * click by default. */
419
+ declare const homeAction: Action;
420
+ /** A tool that binds a drag to `camera.orbit` and Shift+drag to `camera.pan`.
421
+ * Register both actions alongside it; the tool only declares the bindings. */
279
422
  declare function useOrbitTool(): Tool;
280
423
 
281
424
  /**
@@ -297,6 +440,11 @@ interface Camera3dDep {
297
440
  width: number;
298
441
  height: number;
299
442
  };
443
+ /** How far the camera actions may turn and dolly it. Omitted,
444
+ * `DEFAULT_ORBIT_LIMITS`. */
445
+ limits?(): Partial<OrbitLimits>;
446
+ /** Where `camera.home` returns it. Omitted, `camera.home` does nothing. */
447
+ home?(): Camera3d;
300
448
  }
301
449
  declare module '@weasel-js/core' {
302
450
  interface DepSchema {
@@ -304,4 +452,32 @@ declare module '@weasel-js/core' {
304
452
  }
305
453
  }
306
454
 
307
- export { type Camera3d, type Camera3dDep, type ChromeBox, type Footprint, MAX_PITCH, type Node3d, type NodeBounds, type Pose3, type Scene3d, type ScreenBox, UNIT_CUBE, type Viewport3d, type ViewportSource, type World3d, aabbOfPose, cameraEye, cameraView, cameraViewProjection, collectOverlayBoxes, createAreaSelect, createCamera, createInsert, createNodeAtPoint, createPoseDescriptor, createSnap, dollyAction, dollyBy, ndcToScreen, orbitAction, orbitBy, pose3, poseMatrix, projectAabbToScreen, rayThroughScreenPoint, screenBoxOf, screenToNdc, useOrbitTool };
455
+ /** The `orbit3d` kit settings area: how far the camera tools move per input. */
456
+ interface Orbit3dKitSettings {
457
+ /** Radians of orbit per CSS pixel of drag. */
458
+ orbitRate: number;
459
+ /** Dolly exponent per unit of wheel `deltaY`. */
460
+ dollyRate: number;
461
+ }
462
+ declare module '@weasel-js/react' {
463
+ interface KitSettings {
464
+ orbit3d: Orbit3dKitSettings;
465
+ }
466
+ }
467
+ /** The `orbit3d` area as a prefs group whose paths are the `KitSettings` paths. */
468
+ declare const kernel3dKitSettingsSchema: Omit<_weasel_js_prefs.PrefGroup, "children"> & {
469
+ children: {
470
+ readonly orbit3d: Omit<_weasel_js_prefs.PrefGroup, "children"> & {
471
+ children: {
472
+ readonly orbitRate: _weasel_js_prefs.PrefNumber & {
473
+ default: number;
474
+ };
475
+ readonly dollyRate: _weasel_js_prefs.PrefNumber & {
476
+ default: number;
477
+ };
478
+ };
479
+ };
480
+ };
481
+ };
482
+
483
+ export { type Camera3d, type Camera3dDep, type ChromeBox, DEFAULT_ORBIT_LIMITS, type Footprint, type GroundGridSourceOptions, MAX_PITCH, type Node3d, type NodeBounds, type Orbit3dKitSettings, type OrbitLimits, type Pose3, type Scene3d, type ScreenBox, type ScreenLine, type ScreenPoint, type Snap3dOptions, UNIT_CUBE, type Viewport3d, type ViewportSource, type World3d, 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 };