@nakednous/tree 0.0.27 → 0.0.29

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
@@ -550,8 +616,24 @@ projLeft projRight projTop projBottom
550
616
 
551
617
  **Pixel ratio:** `pixelRatio(proj, vpH, eyeZ, ndcZMin)` — world-units-per-pixel at a given depth, handles both perspective and orthographic.
552
618
 
619
+ **Viewport matrix:** `mat4Viewport(out, vp, ndcZMin)` — the matrix `W` taking NDC to screen coordinates, so world → screen is `(W · P · V · p) / w` through `mat4MulPoint` and screen → world the inverse of that composition
620
+
553
621
  **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
622
 
623
+ **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`.
624
+
625
+ **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.
626
+
627
+ **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`.
628
+
629
+ **Camera state** (`camera.js`):
630
+ ```
631
+ createCamera cameraCopy
632
+ cameraView cameraEye cameraProj cameraPlanes
633
+ cameraFromMat4 cameraFromPose cameraToPose
634
+ cameraOrbit cameraDolly cameraPan
635
+ ```
636
+
555
637
  ---
556
638
 
557
639
  ### Constants
@@ -574,6 +656,9 @@ INVISIBLE, VISIBLE, SEMIVISIBLE
574
656
  SPHERE, PLANE, AXIS, DIAL
575
657
  POINT, DIRECTION
576
658
 
659
+ // Pointer-hit shapes
660
+ CIRCLE, SQUARE
661
+
577
662
  // Basis vectors (frozen)
578
663
  ORIGIN, i, j, k, _i, _j, _k
579
664
  ```
@@ -604,11 +689,24 @@ mapLocation(out, px, py, pz, WORLD, SCREEN,
604
689
 
605
690
  ---
606
691
 
607
- ## Relationship to `p5.tree`
692
+ ## Golden vectors
693
+
694
+ `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).
695
+
696
+ ```bash
697
+ npm run golden # regenerate every fixture from src/
698
+ npm test # assert src/ against golden/, and that every export has a fixture
699
+ ```
700
+
701
+ The fixture format is specified in `tools/golden.js`. A function without a fixture is not exported.
702
+
703
+ ---
704
+
705
+ ## Relationship to the bridges
608
706
 
609
- [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.
707
+ The bridges are where rendering lives. [p5.tree](https://github.com/VisualComputing/p5.tree) reads live p5 renderer state (camera matrices, viewport, NDC convention) into the host's view bag and draws the gizmo arrays with p5's own strokes; `twgl.tree` installs the camera, uploads the declared transforms and draws the same arrays through a line pipe on raw WebGL2; `webgpu.tree` realizes the same surface on WebGPU. Between them sits [`@nakednous/host`](https://github.com/nakednous/host): the pointer, the players, the handles, helms and tracks, the device streams, labels and the orbit — DOM transport, no renderer — which every bridge drives and which computes only through this package.
610
708
 
611
- `@nakednous/tree` provides the algorithms. The bridge provides the wiring.
709
+ `@nakednous/tree` provides the algorithms, `@nakednous/host` the transport, `@nakednous/ui` the panels. The bridges provide the drawing.
612
710
 
613
711
  ---
614
712