@nakednous/tree 0.0.27 → 0.0.28

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
@@ -18,22 +18,24 @@ import * as tree from '@nakednous/tree'
18
18
 
19
19
  ## Architecture
20
20
 
21
- `@nakednous/tree` is the bottom layer of a three-package stack. It knows nothing about renderers, the DOM, or p5 — it operates on plain arrays and `Float32Array` buffers throughout.
21
+ `@nakednous/tree` is the bottom layer of a stack. It knows nothing about renderers, the DOM, or p5 — it operates on plain arrays and `Float32Array` buffers throughout.
22
22
 
23
23
  ```
24
24
  application
25
25
 
26
26
 
27
- p5.tree.js bridge: wires tree + ui into p5.js v2
27
+ twgl.tree · p5.tree · webgpu.tree bridges: draw, the GPU ceremony, a framework adapter
28
+
29
+ ├── @nakednous/host ← DOM transport: pointer, view, players, handles, devices, labels, orbit
28
30
 
29
31
  ├── @nakednous/ui ← DOM param panels, transport controls
30
32
 
31
- └── @nakednous/tree ← this package: math, spaces, animation, visibility
33
+ └── @nakednous/tree ← this package: math, spaces, animation, visibility, gizmo geometry
32
34
  ```
33
35
 
34
- The dependency direction is strict: `@nakednous/tree` never imports from the bridge or the DOM layer. This is what lets the same `PoseTrack` that drives a camera path also animate any object — headless, server-side, or in a future renderer.
36
+ The dependency direction is strict: `@nakednous/tree` never imports from the host, the bridges, or the DOM layer; `host` and `ui` depend on `tree` only; a bridge depends on `tree` and `host`, never on another bridge. `@nakednous/*` never renders — rendering lives in the `*.tree` bridges. This is what lets the same `PoseTrack` that drives a camera path also animate any object — headless, server-side, or in a future renderer — and the same gizmo arrays draw through twgl, p5, or WebGPU.
35
37
 
36
- Source is organised into six focused modules:
38
+ Source is organised into focused modules:
37
39
 
38
40
  ```
39
41
  form.js — you have specs, you want a matrix
@@ -43,6 +45,9 @@ track.js — spline math and keyframe animation state machines
43
45
  helm.js — 6-DOF rate-stream integrator — the Track family's live-input sibling
44
46
  filter.js — input conditioning: the 1€ filter + absolute→rate differencing
45
47
  handle.js — constraint solver + ray primitives for interactive manipulators
48
+ visibility.js — frustum planes and visibility tests
49
+ camera.js — camera state (the CameraTrack keyframe shape) ↔ matrices, planes, and orbit edits
50
+ gizmo.js — line generators in twgl's arrays shape: axes, grid, cross, bulls-eye, ring, frustum, hermite, path, helm rig, locus, pane
46
51
  ```
47
52
 
48
53
  ---
@@ -105,7 +110,7 @@ track.add({ pos:[300,0,0] }) // auto tangents
105
110
  ```js
106
111
  rot: [x,y,z,w] // raw quaternion
107
112
  rot: { axis:[x,y,z], angle } // axis-angle
108
- rot: { dir:[x,y,z], up?:[x,y,z] } // look direction (−Z forward)
113
+ rot: { dir:[x,y,z], up?:[x,y,z] } // look direction (−Z forward); up re-seeded when ∥ dir
109
114
  rot: { euler:[rx,ry,rz], order?:'YXZ' } // intrinsic Euler angles (radians)
110
115
  // orders: YXZ (default), XYZ, ZYX,
111
116
  // ZXY, XZY, YZX
@@ -340,7 +345,7 @@ helm.profile = {
340
345
  **Frame — `from`.** `helm.from` names the space fed rates are interpreted in — a declaration the host reads to resolve the per-step `basis` (the core stays camera-agnostic):
341
346
 
342
347
  ```
343
- WORLD world axes — the identity basis (step's basis is null)
348
+ WORLD the world-aligned eye frame — the identity basis (forward −Z; step's basis is null)
344
349
  EYE a viewing camera's frame — screen-relative (default)
345
350
  SELF the helm's OWN evolving pose — body-relative
346
351
  <mat4> an explicit fixed frame
@@ -476,6 +481,45 @@ Three-state result: `VISIBLE` (fully inside), `SEMIVISIBLE` (intersecting), `INV
476
481
 
477
482
  ---
478
483
 
484
+ ### Camera state
485
+
486
+ The camera is plain data — the `CameraTrack` keyframe shape — with pure functions between it and the matrices a draw uploads (`camera.js`). There is no camera object: a track evaluates into the state, a pose drives it, an orbit gesture edits it, and a renderer installs the matrices built from it.
487
+
488
+ ```js
489
+ import { createCamera, cameraView, cameraEye, cameraProj, cameraPlanes, WEBGL } from '@nakednous/tree'
490
+
491
+ const cam = createCamera({ eye: [0, 0, 500], center: [0, 0, 0], fov: Math.PI / 3, near: 0.1, far: 1000 })
492
+ // { eye, center, up, fov | halfHeight, near, far } — fov xor halfHeight (perspective xor orthographic)
493
+
494
+ // per-frame — zero allocation; aspect belongs to the viewport, never to the state
495
+ cameraView(V, cam) // world → eye
496
+ cameraEye(E, cam) // eye → world
497
+ cameraProj(P, cam, width / height, WEBGL) // mat4Persp from fov or mat4Ortho from halfHeight; null when both are null
498
+ cameraPlanes(planes, cam, width / height) // the six frustum planes — visibility straight from the state
499
+ track.eval(cam) // a CameraTrack writes the state directly
500
+ ```
501
+
502
+ Decomposers read the state back:
503
+
504
+ ```js
505
+ cameraFromMat4(cam, E, P, WEBGL) // eye, up, forward from E; the lens from P; the gaze distance |center − eye| is kept
506
+ cameraFromPose(cam, pose) // { pos, rot } → lookat at constant gaze distance; the lens untouched
507
+ cameraToPose(pose, cam) // lookat → { pos, rot } — the rotation of cameraEye, a helm's seed
508
+ cameraCopy(out, cam) // one state into another
509
+ ```
510
+
511
+ Edits are in place and chainable — the arithmetic behind an orbit gesture, callable from a script just the same:
512
+
513
+ ```js
514
+ cameraOrbit(cam, dAz, dEl, { maxEl }) // azimuth about the up hint, elevation clamped short of the pole; never rolls
515
+ cameraDolly(cam, factor, { min, max }) // scales the gaze distance — or halfHeight under orthographic
516
+ cameraPan(cam, dx, dy) // along the eye's right and up, world units (pixelRatio converts pixels)
517
+ ```
518
+
519
+ Every function that needs the camera frame derives it through `mat4Eye`, so planes, poses and edits agree with `cameraEye` exactly — including the up re-seed when the view direction is parallel to the hint.
520
+
521
+ ---
522
+
479
523
  ### Manipulator constraints
480
524
 
481
525
  `handle.js` is the renderer-agnostic core of an interactive manipulator: ray-primitive intersections, az/el utilities, and a `Constraint` state machine. The `p5.tree` bridge wraps these into a draggable handle; this package supplies the math and the **contract** that makes the handle extensible.
@@ -483,6 +527,7 @@ Three-state result: `VISIBLE` (fully inside), `SEMIVISIBLE` (intersecting), `INV
483
527
  ```js
484
528
  import { createConstraint, SPHERE, PLANE, AXIS, DIAL, POINT, DIRECTION,
485
529
  raySphere, rayPlane, rayClosestPointOnAxis,
530
+ rayHitSphere, rayHitCapsule, rayHitRing,
486
531
  dirFromAzEl, azElFromDir } from '@nakednous/tree'
487
532
 
488
533
  const c = createConstraint(SPHERE, { radius: 1 }) // or PLANE / AXIS / DIAL
@@ -493,7 +538,28 @@ c.value(out, DIRECTION) // write the reported value into out(3)
493
538
 
494
539
  `SPHERE` stores a unit direction (gimbal-free); `PLANE` / `AXIS` store a constrained point; `DIAL` stores an accumulated angle θ (multi-turn winding preserved). `value` reports a `DIRECTION` (unit) or a `POINT` per kind. `aim(ax,ay,az[, zx,zy,zz])` re-aims the constraint basis in the working space — `PLANE` takes a new normal (point re-projected), `AXIS` a new direction (`t` preserved), `DIAL` a new plane normal plus optional θ=0 reference (θ preserved) — the seam the `p5.tree` bridge's deferred `from` frame drives. Ray primitives are out-first and assume a unit ray direction; `rayPlane` returns `Infinity` when the ray is parallel.
495
540
 
496
- **Constraint contract (extension seam).** A constraint is any object exposing `kind`, `solve(ox,oy,oz, dx,dy,dz)`, `value(out, report)`, `seed(x,y,z)`, and optionally `scalar()` / `azEl(out2)` / `aim(ax,ay,az[, zx,zy,zz])`. The handle controller drives any conforming constraint, so a new kind rotation, 6-DOF, or app-specificimplements this contract (portable, draw-free) plus a bridge-side locus draw, rather than forking the controller. The built-in `Constraint` is the reference implementation. Full design: [`handle-design.md`](./handle-design.md).
541
+ **Hit tests the analytic pick.** Beside the solve primitives, which always write a point, three tests write nothing and return the ray parameter `t` of the nearest hit with `t ≥ 0`, or `Infinity`: `rayHitSphere(o, d, c, r)`, `rayHitCapsule(o, d, a, b, r)` (the segment `a→b` swept by `r`) and `rayHitRing(o, d, c, u, R, r, detail = 32)` (the circle of radius `R` about `c` in the plane `u`, swept by tube radius `r`, as a capsule chain of `detail` links chordal error `R · (1 − cos(π / detail))`, never degenerate edge-on). A ray starting inside hits at its exit, so a press from inside a proxy still grabs. These are what a host's controller picks with instead of a tagged render pass: unproject the pointer, convert the grab size to working units through `pixelRatio`, test every candidate, nearest `t` wins.
542
+
543
+ **Constraint contract (extension seam).** A constraint is any object exposing `kind`, `solve(ox,oy,oz, dx,dy,dz)`, `value(out, report)`, `seed(x,y,z)`, and optionally `scalar()` / `azEl(out2)` / `aim(ax,ay,az[, zx,zy,zz])` / `proxy(ox,oy,oz, dx,dy,dz, radius)` — the analytic pick: `t` or `Infinity` for a ray against the grab proxy of `radius` working units (built-in kinds: a sphere at the reported `POINT`; `DIAL`: the ring at the anchor with tube `radius`; a kind without one gets the sphere). The handle controller drives any conforming constraint, so a new kind — rotation, 6-DOF, or app-specific — implements this contract (portable, draw-free, its hit test included) plus a bridge-side locus draw, rather than forking the controller. The built-in `Constraint` is the reference implementation. Full design: [`handle-design.md`](./handle-design.md).
544
+
545
+ ---
546
+
547
+ ### Gizmo geometry
548
+
549
+ `gizmo.js` generates the vertices a gizmo is made of — renderer-free, into a caller-owned arrays object in twgl's `arrays` shape, line lists (and one triangle list) that `createBufferInfoFromArrays` uploads as they are and a WebGPU vertex buffer is filled from. Drawing, colour state, HUD mode, textures and text stay in the bridges.
550
+
551
+ ```js
552
+ import { createArrays, growArrays, capacityOf,
553
+ axesLines, gridLines, crossLines, bullsEyeLines, ringLines,
554
+ frustumLines, frustumCorners, hermiteLines,
555
+ pathLines, helmRigLines, locusLines, paneTris } from '@nakednous/tree'
556
+
557
+ const out = createArrays(64, { color: true }) // { position, color, count }, the one allocating call
558
+ let n = axesLines(out, { size: 100 }) // → the vertex count needed; writes min(n, capacity)
559
+ if (n > capacityOf(out)) { growArrays(out, n); axesLines(out, { size: 100 }) }
560
+ ```
561
+
562
+ Every generator is snprintf-style: it returns the count it needs, writes what fits and sets `out.count`, and states its count formula so a caller can pre-size exactly. With an `out.color` array, `axesLines` and `helmRigLines` write the semantic palette (`COLOR_X` · `COLOR_Y` · `COLOR_Z`, `COLOR_DIM` alpha for a dimmed stroke) and every other generator writes `opts.color`. The bit namespaces (`X` … `LABELS`, `NEAR` … `APEX`, `PATH` … `HANDLES`, `TRANSLATE` · `ROTATE`, `HANDLE` … `RING`) are gizmo-local. `frustumCorners` writes a camera's eight world-space corners (near face counter-clockwise from bottom-left, then far) from a camera state or a matrix-captured `{ mat4Eye, mat4Proj, ndcZMin }`; `pathLines` walks a track's own samplers; `helmRigLines` reads a helm's profile and activity; `locusLines` dispatches on a constraint's kind (or its own `locus(out, opts)`); `paneTris` is the textured quad with one upright uv orientation. Full design: [`gizmo-design.md`](./gizmo-design.md).
497
563
 
498
564
  ---
499
565
 
@@ -514,7 +580,7 @@ qToAxisAngle
514
580
 
515
581
  **Mat4 arithmetic** (`query.js`):
516
582
  ```
517
- mat4Mul mat4Invert mat4Transpose mat4MulPoint mat4MulDir
583
+ mat4Mul mat4Invert mat4MulPoint mat4MulDir
518
584
  mat3NormalFromMat4 mat4Location mat3Direction
519
585
  mat4PV mat4MV
520
586
  ```
@@ -524,8 +590,8 @@ mat4PV mat4MV
524
590
  **Matrix construction from specs** (`form.js`):
525
591
  ```
526
592
  mat4FromBasis — rigid frame from orthonormal basis + translation
527
- mat4View — view matrix (world→eye) from lookat params
528
- mat4Eye — eye matrix (eye→world) from lookat params
593
+ mat4View — view matrix (world→eye) from lookat params (up re-seeded when ∥ view direction)
594
+ mat4Eye — eye matrix (eye→world) from lookat params (same rule)
529
595
  mat4FromTRS — column-major mat4 from flat TRS scalars
530
596
  mat4FromTranslation — translation-only mat4
531
597
  mat4FromScale — scale-only mat4
@@ -552,6 +618,20 @@ projLeft projRight projTop projBottom
552
618
 
553
619
  **Pick matrix:** `mat4Pick(proj, px, py, vp)` — mutates a projection matrix in-place so that the pixel at `(px, py)` maps to the full NDC square, making a 1×1 FBO render contain exactly that pixel. Takes the same signed viewport `vp` as `mapLocation` — the y-convention is preserved automatically.
554
620
 
621
+ **Pointer ray:** `unproject(outO, outD, sx, sy, m, vp, ndcZMin)` — a screen point as a world ray: origin on the near plane, unit direction toward the far plane. Same bag and signed viewport as `mapLocation` (`mat4PVInv` filled by the caller); `null` when the bag has no inverse. The point-at-depth form stays `mapLocation(SCREEN → WORLD)` with a depth in `z`.
622
+
623
+ **Pointer hit:** `pointerHit(px, py, x, y, z, radius, m, vp, ndcZMin, shape = CIRCLE)` — is the pointer within `radius` px of the projected world point? `CIRCLE` (Euclidean) or `SQUARE` (Chebyshev), boundary inclusive; a point whose screen depth falls outside `[0, 1]` never hits.
624
+
625
+ **Pick-id codec:** `idToRgba(out, id)` packs a 24-bit id into `[r, g, b, 1]` normalised floats, R the low byte; `rgbaToId(r, g, b)` decodes the bytes of a readback. Id `0` is the background; ids run `1 … 2²⁴ − 1`.
626
+
627
+ **Camera state** (`camera.js`):
628
+ ```
629
+ createCamera cameraCopy
630
+ cameraView cameraEye cameraProj cameraPlanes
631
+ cameraFromMat4 cameraFromPose cameraToPose
632
+ cameraOrbit cameraDolly cameraPan
633
+ ```
634
+
555
635
  ---
556
636
 
557
637
  ### Constants
@@ -574,6 +654,9 @@ INVISIBLE, VISIBLE, SEMIVISIBLE
574
654
  SPHERE, PLANE, AXIS, DIAL
575
655
  POINT, DIRECTION
576
656
 
657
+ // Pointer-hit shapes
658
+ CIRCLE, SQUARE
659
+
577
660
  // Basis vectors (frozen)
578
661
  ORIGIN, i, j, k, _i, _j, _k
579
662
  ```
@@ -604,6 +687,19 @@ mapLocation(out, px, py, pz, WORLD, SCREEN,
604
687
 
605
688
  ---
606
689
 
690
+ ## Golden vectors
691
+
692
+ `golden/` holds one JSON fixture per source module — `{ args, out }` cases for every exported function and `{ call, args, expect }` transcripts for the stateful classes — generated from this core and committed with the repo (not shipped in the package). They are both the regression suite and the port contract: a port is conformant when it reproduces them within the stated tolerances (exact for ints and enums, `1e-6` for `f64` state, `1e-5` for `f32` matrices).
693
+
694
+ ```bash
695
+ npm run golden # regenerate every fixture from src/
696
+ npm test # assert src/ against golden/, and that every export has a fixture
697
+ ```
698
+
699
+ The fixture format is specified in `tools/golden.js`. A function without a fixture is not exported.
700
+
701
+ ---
702
+
607
703
  ## Relationship to `p5.tree`
608
704
 
609
705
  [p5.tree](https://github.com/VisualComputing/p5.tree) is the bridge layer. It reads live renderer state (camera matrices, viewport dimensions, NDC convention) and passes it to `@nakednous/tree` functions. It wires `PoseTrack` and `CameraTrack` to the p5 draw loop, exposes `createPoseTrack` / `createCameraTrack` / `getCamera`, and provides `createPanel` for transport and parameter UIs.