@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 +107 -11
- package/dist/index.js +1489 -72
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
**
|
|
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
|
|
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.
|