@weasel-js/kernel3d 1.10.1 → 1.11.0

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