@nakednous/tree 0.0.26 → 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 +150 -12
- package/dist/index.js +1717 -70
- 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
|
|
@@ -41,7 +43,11 @@ query.js — you have a matrix, you want information
|
|
|
41
43
|
quat.js — quaternion algebra and mat4/mat3 conversions
|
|
42
44
|
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
|
|
46
|
+
filter.js — input conditioning: the 1€ filter + absolute→rate differencing
|
|
44
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
|
|
45
51
|
```
|
|
46
52
|
|
|
47
53
|
---
|
|
@@ -104,7 +110,7 @@ track.add({ pos:[300,0,0] }) // auto tangents
|
|
|
104
110
|
```js
|
|
105
111
|
rot: [x,y,z,w] // raw quaternion
|
|
106
112
|
rot: { axis:[x,y,z], angle } // axis-angle
|
|
107
|
-
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
|
|
108
114
|
rot: { euler:[rx,ry,rz], order?:'YXZ' } // intrinsic Euler angles (radians)
|
|
109
115
|
// orders: YXZ (default), XYZ, ZYX,
|
|
110
116
|
// ZXY, XZY, YZX
|
|
@@ -339,7 +345,7 @@ helm.profile = {
|
|
|
339
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):
|
|
340
346
|
|
|
341
347
|
```
|
|
342
|
-
WORLD world
|
|
348
|
+
WORLD the world-aligned eye frame — the identity basis (forward −Z; step's basis is null)
|
|
343
349
|
EYE a viewing camera's frame — screen-relative (default)
|
|
344
350
|
SELF the helm's OWN evolving pose — body-relative
|
|
345
351
|
<mat4> an explicit fixed frame
|
|
@@ -351,14 +357,55 @@ SELF the helm's OWN evolving pose — body-relative
|
|
|
351
357
|
|
|
352
358
|
```js
|
|
353
359
|
helm.deadzone = 8 // rest-drift floor — |rate| ≤ deadzone reads as 0
|
|
360
|
+
helm.filter = oneEuro({ minCutoff: 1, beta: 0.5 }) // optional input conditioner (filter → deadzone)
|
|
361
|
+
helm.fullScale = 500 // raw full-deflection magnitude a read-out divides by
|
|
354
362
|
helm.activity(out6) // six effective rates (post deadzone·sign·sens), channel order
|
|
355
|
-
helm.home([pose]) // re-home pos + rot (NOT reset — no keyframes); clears pending rate
|
|
363
|
+
helm.home([pose]) // re-home pos + rot (NOT reset — no keyframes); clears pending rate; resets filter
|
|
356
364
|
```
|
|
357
365
|
|
|
366
|
+
`filter` is an optional input conditioner (default `null`): when set, `step` runs it over the fed rate before the deadzone — filter then deadzone, the two orthogonal (the 1€ removes zero-mean jitter; the deadzone's exact zero is the only no-creep guarantee, since a low-pass passes DC). `fullScale` (default `500`) is the raw full-deflection magnitude a read-out divides by, so a transport on a different input scale declares its own and its meters read honestly. `activity()` reports the raw fed rate (pre-filter) by design.
|
|
367
|
+
|
|
358
368
|
The `p5.tree` bridge wraps this into `createCameraHelm` / `createPoseHelm` (transport, camera basis, draw-loop player) plus the `helmRig` gizmo and the `createPanel(helm)` profile editor.
|
|
359
369
|
|
|
360
370
|
---
|
|
361
371
|
|
|
372
|
+
### Input conditioning — `oneEuro` · `poseDelta`
|
|
373
|
+
|
|
374
|
+
Two helpers for the rate stream a helm feeds on — flat, out-first, zero-alloc (`filter.js`).
|
|
375
|
+
|
|
376
|
+
**`oneEuro({ minCutoff, beta, dCutoff })`** is the [1€ filter](https://gery.casiez.net/1euro/) (Casiez et al., CHI'12): a first-order low-pass whose cutoff rises with signal speed — heavy smoothing at rest, low lag under motion. It returns a stateful carrying function, dispatched on its first argument:
|
|
377
|
+
|
|
378
|
+
```js
|
|
379
|
+
import { oneEuro } from '@nakednous/tree'
|
|
380
|
+
|
|
381
|
+
const f = oneEuro({ minCutoff: 1, beta: 0.5 }) // params live-mutable: f.minCutoff, f.beta
|
|
382
|
+
|
|
383
|
+
// scalar form — returns the filtered number
|
|
384
|
+
const y = f(rawScalar, dt)
|
|
385
|
+
|
|
386
|
+
// vec form — out-first, zero-alloc after warm-up
|
|
387
|
+
const out = [0, 0, 0]
|
|
388
|
+
f(out, rawVec3, dt)
|
|
389
|
+
|
|
390
|
+
f.reset() // drop state — next call re-seeds
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
It removes zero-mean **jitter**, not a DC **bias**: a low-pass passes a constant offset, so a resting bias survives it and still integrates to drift — pair it with a deadzone (the helm applies filter → deadzone in that order).
|
|
394
|
+
|
|
395
|
+
**`poseDelta(out, prev, cur, dt)`** differences two absolute poses into the `{ lin, ang }` rate a helm feeds on — the bridge from an absolute transport (a tracked hand, a marker, a played keyframe) to the rate stream.
|
|
396
|
+
|
|
397
|
+
```js
|
|
398
|
+
import { poseDelta } from '@nakednous/tree'
|
|
399
|
+
|
|
400
|
+
const rate = { lin: [0, 0, 0], ang: [0, 0, 0] }
|
|
401
|
+
poseDelta(rate, prevPose, curPose, dt) // prev / cur: { pos:[x,y,z], rot:[x,y,z,w] }
|
|
402
|
+
helm.feed(rate.lin, rate.ang)
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
The angular half carries a **double-cover guard**: a quaternion and its negation are the same orientation, so a source that returns a canonicalised quaternion makes the stored value jump hemispheres as the true orientation sweeps through `w = 0`. When `dot(prev, cur) < 0`, `poseDelta` flips `cur` into `prev`'s hemisphere before differencing, so the relative rotation always takes the short arc — without it the angular rate spikes toward `2π/dt` at every crossing.
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
362
409
|
### Coordinate-space mapping
|
|
363
410
|
|
|
364
411
|
`mapLocation` and `mapDirection` convert points and vectors between any pair of named spaces. All work is done in flat scalar arithmetic — no objects created per call.
|
|
@@ -434,6 +481,45 @@ Three-state result: `VISIBLE` (fully inside), `SEMIVISIBLE` (intersecting), `INV
|
|
|
434
481
|
|
|
435
482
|
---
|
|
436
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
|
+
|
|
437
523
|
### Manipulator constraints
|
|
438
524
|
|
|
439
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.
|
|
@@ -441,6 +527,7 @@ Three-state result: `VISIBLE` (fully inside), `SEMIVISIBLE` (intersecting), `INV
|
|
|
441
527
|
```js
|
|
442
528
|
import { createConstraint, SPHERE, PLANE, AXIS, DIAL, POINT, DIRECTION,
|
|
443
529
|
raySphere, rayPlane, rayClosestPointOnAxis,
|
|
530
|
+
rayHitSphere, rayHitCapsule, rayHitRing,
|
|
444
531
|
dirFromAzEl, azElFromDir } from '@nakednous/tree'
|
|
445
532
|
|
|
446
533
|
const c = createConstraint(SPHERE, { radius: 1 }) // or PLANE / AXIS / DIAL
|
|
@@ -451,7 +538,28 @@ c.value(out, DIRECTION) // write the reported value into out(3)
|
|
|
451
538
|
|
|
452
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.
|
|
453
540
|
|
|
454
|
-
**
|
|
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).
|
|
455
563
|
|
|
456
564
|
---
|
|
457
565
|
|
|
@@ -472,7 +580,7 @@ qToAxisAngle
|
|
|
472
580
|
|
|
473
581
|
**Mat4 arithmetic** (`query.js`):
|
|
474
582
|
```
|
|
475
|
-
mat4Mul mat4Invert
|
|
583
|
+
mat4Mul mat4Invert mat4MulPoint mat4MulDir
|
|
476
584
|
mat3NormalFromMat4 mat4Location mat3Direction
|
|
477
585
|
mat4PV mat4MV
|
|
478
586
|
```
|
|
@@ -482,8 +590,8 @@ mat4PV mat4MV
|
|
|
482
590
|
**Matrix construction from specs** (`form.js`):
|
|
483
591
|
```
|
|
484
592
|
mat4FromBasis — rigid frame from orthonormal basis + translation
|
|
485
|
-
mat4View — view matrix (world→eye) from lookat params
|
|
486
|
-
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)
|
|
487
595
|
mat4FromTRS — column-major mat4 from flat TRS scalars
|
|
488
596
|
mat4FromTranslation — translation-only mat4
|
|
489
597
|
mat4FromScale — scale-only mat4
|
|
@@ -510,6 +618,20 @@ projLeft projRight projTop projBottom
|
|
|
510
618
|
|
|
511
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.
|
|
512
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
|
+
|
|
513
635
|
---
|
|
514
636
|
|
|
515
637
|
### Constants
|
|
@@ -532,6 +654,9 @@ INVISIBLE, VISIBLE, SEMIVISIBLE
|
|
|
532
654
|
SPHERE, PLANE, AXIS, DIAL
|
|
533
655
|
POINT, DIRECTION
|
|
534
656
|
|
|
657
|
+
// Pointer-hit shapes
|
|
658
|
+
CIRCLE, SQUARE
|
|
659
|
+
|
|
535
660
|
// Basis vectors (frozen)
|
|
536
661
|
ORIGIN, i, j, k, _i, _j, _k
|
|
537
662
|
```
|
|
@@ -562,6 +687,19 @@ mapLocation(out, px, py, pz, WORLD, SCREEN,
|
|
|
562
687
|
|
|
563
688
|
---
|
|
564
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
|
+
|
|
565
703
|
## Relationship to `p5.tree`
|
|
566
704
|
|
|
567
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.
|