@vibecook/ice 0.1.0 → 0.2.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.
Files changed (115) hide show
  1. package/README.md +37 -2
  2. package/dist/chunk-5SJYHQI4.js +636 -0
  3. package/dist/chunk-5SJYHQI4.js.map +1 -0
  4. package/dist/{chunk-5S7GSXJG.js → chunk-DXU3RZZS.js} +2913 -1180
  5. package/dist/chunk-DXU3RZZS.js.map +1 -0
  6. package/dist/{chunk-FIAXPNNY.js → chunk-IASTQMU5.js} +130 -551
  7. package/dist/chunk-IASTQMU5.js.map +1 -0
  8. package/dist/{chunk-BCJ47SX7.js → chunk-OR5FNVUE.js} +460 -1
  9. package/dist/chunk-OR5FNVUE.js.map +1 -0
  10. package/dist/devtools.js +508 -372
  11. package/dist/devtools.js.map +1 -1
  12. package/dist/dom.js +3 -11
  13. package/dist/ground.js +715 -0
  14. package/dist/ground.js.map +1 -0
  15. package/dist/index.js +70 -2
  16. package/dist/kernel.js +31 -1
  17. package/dist/r3f.js +599 -62
  18. package/dist/r3f.js.map +1 -1
  19. package/dist/react.js +34 -418
  20. package/dist/react.js.map +1 -1
  21. package/dist/types/packages/core/src/catalog/camera-derived.d.ts +41 -3
  22. package/dist/types/packages/core/src/catalog/gesture.d.ts +39 -3
  23. package/dist/types/packages/core/src/catalog/graph.d.ts +7 -6
  24. package/dist/types/packages/core/src/catalog/index.d.ts +1 -0
  25. package/dist/types/packages/core/src/catalog/insert.d.ts +31 -0
  26. package/dist/types/packages/core/src/catalog/pointer.d.ts +11 -0
  27. package/dist/types/packages/core/src/catalog/scene.d.ts +37 -4
  28. package/dist/types/packages/core/src/catalog/selection-presence.d.ts +7 -0
  29. package/dist/types/packages/core/src/catalog/settings-resources.d.ts +21 -0
  30. package/dist/types/packages/core/src/doc/autosave.d.ts +15 -2
  31. package/dist/types/packages/core/src/doc/bootstrap.d.ts +29 -0
  32. package/dist/types/packages/core/src/doc/channels.d.ts +9 -0
  33. package/dist/types/packages/core/src/doc/doc-commit-sink.d.ts +2 -1
  34. package/dist/types/packages/core/src/doc/doc-kit.d.ts +16 -1
  35. package/dist/types/packages/core/src/doc/envelope.d.ts +2 -1
  36. package/dist/types/packages/core/src/doc/migrate.d.ts +8 -6
  37. package/dist/types/packages/core/src/doc/schema-migrate.d.ts +50 -0
  38. package/dist/types/packages/core/src/doc/version-gate.d.ts +3 -0
  39. package/dist/types/packages/core/src/engine/commit-sink.d.ts +24 -2
  40. package/dist/types/packages/core/src/engine/frame-info.d.ts +16 -1
  41. package/dist/types/packages/core/src/facade/create-canvas-engine.d.ts +70 -6
  42. package/dist/types/packages/core/src/guards/guarded-tx.d.ts +4 -2
  43. package/dist/types/packages/core/src/helpers/churn-guard.d.ts +40 -0
  44. package/dist/types/packages/core/src/index.d.ts +13 -7
  45. package/dist/types/packages/core/src/input/queue.d.ts +11 -0
  46. package/dist/types/packages/core/src/nav/nested-canvas.d.ts +47 -11
  47. package/dist/types/packages/core/src/ops/arrange.d.ts +15 -0
  48. package/dist/types/packages/core/src/ops/insert.d.ts +62 -0
  49. package/dist/types/packages/core/src/ops/point-pick.d.ts +11 -7
  50. package/dist/types/packages/core/src/ops/sibling-order.d.ts +70 -0
  51. package/dist/types/packages/core/src/schema/meta.d.ts +1 -0
  52. package/dist/types/packages/core/src/settings/defaults.d.ts +45 -0
  53. package/dist/types/packages/core/src/settings/ground-config.d.ts +55 -0
  54. package/dist/types/packages/core/src/systems/chrome.d.ts +6 -2
  55. package/dist/types/packages/core/src/systems/insert-ghost.d.ts +25 -0
  56. package/dist/types/packages/core/src/systems/l1-pick.d.ts +25 -11
  57. package/dist/types/packages/core/src/systems/l2-recognize.d.ts +10 -4
  58. package/dist/types/packages/core/src/systems/l3-behave.d.ts +6 -4
  59. package/dist/types/packages/core/src/systems/l3-claim.d.ts +7 -3
  60. package/dist/types/packages/core/src/systems/l3-drop.d.ts +3 -1
  61. package/dist/types/packages/core/src/systems/l3-resize.d.ts +3 -1
  62. package/dist/types/packages/core/src/systems/l3-snap.d.ts +25 -7
  63. package/dist/types/packages/core/src/systems/nav-flight.d.ts +95 -0
  64. package/dist/types/packages/core/src/widget/define-widget.d.ts +39 -0
  65. package/dist/types/packages/core/src/widget/mount-store.d.ts +11 -3
  66. package/dist/types/packages/core/src/widget/set-props.d.ts +16 -0
  67. package/dist/types/packages/core/src/widget/spawn.d.ts +21 -1
  68. package/dist/types/packages/devtools/src/attach.d.ts +77 -0
  69. package/dist/types/packages/devtools/src/dock.d.ts +38 -0
  70. package/dist/types/packages/devtools/src/gl-panel.d.ts +69 -0
  71. package/dist/types/packages/devtools/src/index.d.ts +8 -6
  72. package/dist/types/packages/dom/src/index.d.ts +1 -3
  73. package/dist/types/packages/dom/src/measure-wiring.d.ts +2 -2
  74. package/dist/types/packages/dom/src/pointer-adapter.d.ts +21 -1
  75. package/dist/types/packages/dom/src/reflectors/dom-widgets.d.ts +5 -0
  76. package/dist/types/packages/ground/src/index.d.ts +52 -0
  77. package/dist/types/packages/ground/src/layer.d.ts +40 -0
  78. package/dist/types/packages/ground/src/pass.d.ts +41 -0
  79. package/dist/types/packages/ground/src/passes/grid.d.ts +21 -0
  80. package/dist/types/packages/ground/src/passes/guides-collect.d.ts +14 -0
  81. package/dist/types/packages/ground/src/passes/guides.d.ts +3 -0
  82. package/dist/types/packages/ground/src/passes/soup-collect.d.ts +35 -0
  83. package/dist/types/packages/ground/src/passes/soup-mesh.d.ts +8 -0
  84. package/dist/types/packages/ground/src/passes/wires-collect.d.ts +31 -0
  85. package/dist/types/packages/ground/src/passes/wires.d.ts +10 -0
  86. package/dist/types/packages/ground/src/renderer.d.ts +15 -0
  87. package/dist/types/packages/ice/src/ground.d.ts +1 -0
  88. package/dist/types/packages/kernel/src/bezier.d.ts +15 -0
  89. package/dist/types/packages/kernel/src/coords.d.ts +15 -0
  90. package/dist/types/packages/kernel/src/easing.d.ts +11 -0
  91. package/dist/types/packages/kernel/src/index.d.ts +3 -0
  92. package/dist/types/packages/kernel/src/layout.d.ts +118 -0
  93. package/dist/types/packages/kernel/src/nav-flight.d.ts +93 -0
  94. package/dist/types/packages/r3f/src/bridge.d.ts +73 -12
  95. package/dist/types/packages/r3f/src/composite-material.d.ts +9 -0
  96. package/dist/types/packages/r3f/src/compositor-pass.d.ts +22 -0
  97. package/dist/types/packages/r3f/src/gl-root.d.ts +76 -1
  98. package/dist/types/packages/r3f/src/gl-router.d.ts +10 -0
  99. package/dist/types/packages/r3f/src/index.d.ts +3 -2
  100. package/dist/types/packages/r3f/src/island-state.d.ts +44 -0
  101. package/dist/types/packages/r3f/src/preview-capture.d.ts +19 -0
  102. package/dist/types/packages/r3f/src/self-sustain.d.ts +39 -0
  103. package/dist/types/packages/r3f/src/use-island-frame.d.ts +16 -1
  104. package/dist/types/packages/react/src/engine-context.d.ts +8 -0
  105. package/dist/types/packages/react/src/index.d.ts +4 -2
  106. package/dist/types/packages/react/src/infinite-canvas.d.ts +40 -6
  107. package/dist/types/packages/react/src/preview-snapshots.d.ts +17 -0
  108. package/dist/types/packages/react/src/widget-preview.d.ts +14 -0
  109. package/package.json +13 -8
  110. package/dist/chunk-5S7GSXJG.js.map +0 -1
  111. package/dist/chunk-BCJ47SX7.js.map +0 -1
  112. package/dist/chunk-FIAXPNNY.js.map +0 -1
  113. package/dist/types/packages/devtools/src/panel.d.ts +0 -48
  114. package/dist/types/packages/dom/src/reflectors/grid.d.ts +0 -63
  115. package/dist/types/packages/dom/src/reflectors/wires.d.ts +0 -88
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Pure auto-layout math (2026-07-17, James: cards dropped into a folder "just
3
+ * pile up on each other" — and, later, an explicit desktop-style Clean Up).
4
+ *
5
+ * Two primitives, two products, one law each:
6
+ * - `insertSlot` — place ONE newcomer near a drop hint without moving
7
+ * anything: newcomers get a free slot, incumbents never move. Consumed by
8
+ * the drop-consume path (l3-behave). Candidates are edge-aligned to the
9
+ * incumbents (flush right/below/left/above with a gutter), so successive
10
+ * drops grow aligned rows and columns instead of a global imposed grid.
11
+ * - `packLayout` — bottom-left candidate packing for the explicit
12
+ * "Clean Up" command (v2 2026-07-17, James: shelves were "not really
13
+ * efficient" and selection packs "overlay on top of unselected widgets"):
14
+ * items flow in reading order (current y, then x, then index) and each
15
+ * takes the lowest-then-leftmost free spot, so small cards fill the air
16
+ * beside tall ones instead of opening a new shelf. `obstacles` rects are
17
+ * honored as immovable — a scoped pack flows AROUND bystanders. Still
18
+ * "my mess, straightened": deterministic, anchored at the cluster's own
19
+ * top-left, idempotent on packed input (pinned by test).
20
+ * - `layerGraph` — the Unreal-Blueprint-style arrange for WIRED widgets:
21
+ * Sugiyama-lite layered layout (longest-path ranks along edge direction,
22
+ * barycenter ordering, columns left→right, vertically centered). Callers
23
+ * lay out each connected component with this and pack the resulting
24
+ * blocks with `packLayout`.
25
+ *
26
+ * Plain structs in, plain structs out (Law 13): no ECS, no DOM, no camera.
27
+ * All coordinates live in ONE frame — callers convert (container-local for
28
+ * folder drops, world for canvas clean-up) before and after.
29
+ */
30
+ export interface LayoutRect {
31
+ readonly x: number;
32
+ readonly y: number;
33
+ readonly w: number;
34
+ readonly h: number;
35
+ }
36
+ export interface LayoutSize {
37
+ readonly w: number;
38
+ readonly h: number;
39
+ }
40
+ export interface LayoutPoint {
41
+ readonly x: number;
42
+ readonly y: number;
43
+ }
44
+ /**
45
+ * Nearest free slot to `hint` for a `size` newcomer among `incumbents`.
46
+ *
47
+ * The hint wins verbatim when it is already free (a deliberate drop into
48
+ * empty space is user intent — don't second-guess it). Otherwise candidates
49
+ * flush against each incumbent's edges (top- and far-aligned variants) are
50
+ * ranked by squared distance to the hint; ties resolve to the first-seen
51
+ * candidate (stable in incumbent order — deterministic across runs). The
52
+ * fallback — a spot gutter-right of EVERYTHING — cannot crowd anyone, so a
53
+ * slot always exists.
54
+ */
55
+ export declare function insertSlot(incumbents: readonly LayoutRect[], size: LayoutSize, hint: LayoutPoint, gutter: number): {
56
+ x: number;
57
+ y: number;
58
+ };
59
+ export interface PackOpts {
60
+ readonly gutter: number;
61
+ /** Pack origin; defaults to the items' current bbox top-left (the cluster stays put). */
62
+ readonly origin?: LayoutPoint;
63
+ /**
64
+ * Band wrap width; defaults to a near-square derivation (√(total area × 1.6),
65
+ * never narrower than the widest item). Soft: an item that fits nowhere
66
+ * inside the band still places (lowest spot, band ignored) rather than
67
+ * failing.
68
+ */
69
+ readonly maxWidth?: number;
70
+ /**
71
+ * Immovable rects the pack must flow AROUND (e.g. unselected widgets when
72
+ * arranging a selection). Never returned, never moved, never crowded.
73
+ */
74
+ readonly obstacles?: readonly LayoutRect[];
75
+ }
76
+ /**
77
+ * Bottom-left candidate packing: items in reading order of their CURRENT
78
+ * positions (strict y, then x, then input index) each take the
79
+ * lowest-then-leftmost free spot from the candidate set (the origin, plus
80
+ * gutter-flush right-of and below-of every placed/obstacle rect, plus the
81
+ * band-left row start under each). Dense with mixed sizes — small cards fill
82
+ * the air beside tall ones — and obstacle-aware. Returns placements parallel
83
+ * to the input array.
84
+ */
85
+ export declare function packLayout(items: readonly LayoutRect[], opts: PackOpts): Array<{
86
+ x: number;
87
+ y: number;
88
+ }>;
89
+ export interface LayerGraphOpts {
90
+ /** Horizontal gap between columns (leave room for wires to breathe). */
91
+ readonly gapX: number;
92
+ /** Vertical gap between nodes inside a column. */
93
+ readonly gapY: number;
94
+ /**
95
+ * Wrap the column run into stacked BANDS when the unwrapped width would
96
+ * exceed this (carriage-return style: each band flows left→right, bands
97
+ * stack top→down with a 2×gapY separator). Omit for a single band — a
98
+ * graph is inherently wide and only wraps when the caller says the canvas
99
+ * band demands it.
100
+ */
101
+ readonly maxWidth?: number;
102
+ }
103
+ /**
104
+ * Layered graph layout (Sugiyama-lite) — the Unreal-Blueprint-style arrange
105
+ * for wired widgets. Rank = longest path from the sources along edge
106
+ * direction (a DFS drops back edges, so cycles are safe); columns flow
107
+ * left→right; nodes stack top→down within a column. Ordering = alternating
108
+ * barycenter sweeps with the BEST ordering kept (counted wire crossings),
109
+ * then the classic transpose refinement (adjacent swaps that strictly
110
+ * reduce crossings) — not provably minimal, but the standard practical
111
+ * pair. Columns center vertically on their band's tallest. Placements are
112
+ * relative to (0,0) — callers anchor the block. Deterministic: all
113
+ * orderings tie-break on node index.
114
+ */
115
+ export declare function layerGraph(nodes: readonly LayoutSize[], edges: readonly (readonly [number, number])[], opts: LayerGraphOpts): Array<{
116
+ x: number;
117
+ y: number;
118
+ }>;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Nav-flight math (design-006 §2/§4 — the portal zoom, T1 camera-flight half).
3
+ *
4
+ * Pure math, no ECS/DOM. Conventions are coords.ts's (Law 13): camera {x,y} =
5
+ * world point at the viewport top-left, zoom = screen px per world unit,
6
+ * `screen = (world − cam)·zoom`.
7
+ *
8
+ * The portal embedding: a container's content coordinates are FRAME-LOCAL
9
+ * (design-001 §5.1) — the transition DEFINES the embedding by mapping the
10
+ * arrival camera's visible rect onto the container's body rect (`portalAffine`,
11
+ * aspect-fit, centers aligned). With that affine fixed, both transition layers
12
+ * ride ONE camera: the destination frame renders under `c(t)` directly; the
13
+ * departed frame renders under `c(t) ∘ A` where A maps departed-frame coords
14
+ * into destination-frame coords. Continuity at the cut is exact by
15
+ * construction (`solveFlightStart`), and no drift is possible mid-flight —
16
+ * there is no second clock.
17
+ *
18
+ * Numerics locked by the mock (2026-07-15, three rounds of field testing):
19
+ * - the progress spring uses the CLOSED-FORM critically-damped solution —
20
+ * semi-implicit Euler diverges once ω·dt nears 1 (a fast exit response on
21
+ * one dropped frame sent the mock's camera to zoom 1501);
22
+ * - the path interpolates zoom in LOG space (constant perceived zoom
23
+ * velocity — van Wijk & Nuij) with the view center linear in 1/zoom, which
24
+ * is exactly an anchored zoom whenever a fixed screen point exists;
25
+ * - flights beyond `maxOctaves` must be depth-capped (`capFlightStart`):
26
+ * sweeping a DOM plane's raster scale ~30× in one flight outruns
27
+ * Chromium's async tile re-raster (stale/missing tiles, persisting at
28
+ * rest) — the capped flight is presented as a crossfade-through-zoom.
29
+ */
30
+ import type { CameraState } from "./coords";
31
+ import type { Rect } from "./shapes";
32
+ /** Uniform scale + translate: `A(q) = o + q·s`. */
33
+ export interface PortalAffine {
34
+ readonly s: number;
35
+ readonly ox: number;
36
+ readonly oy: number;
37
+ }
38
+ /** The world rect a camera shows through a viewport. */
39
+ export declare function visibleRect(cam: CameraState, vpW: number, vpH: number): Rect;
40
+ /**
41
+ * The portal embedding: map `arrival` (the world rect the arrival camera will
42
+ * show, in the CHILD frame's coords) onto `portal` (the container's body rect,
43
+ * in the PARENT frame's coords). Aspect-FIT with centers aligned (design-006
44
+ * §8.3 — the card chrome absorbs the letterbox).
45
+ */
46
+ export declare function portalAffine(arrival: Rect, portal: Rect): PortalAffine;
47
+ export declare function invertAffine(a: PortalAffine): PortalAffine;
48
+ /** `(outer ∘ inner)(q) = outer(inner(q))` — for multi-level exit portals. */
49
+ export declare function composeAffine(outer: PortalAffine, inner: PortalAffine): PortalAffine;
50
+ /**
51
+ * Continuity solve: the camera `c0` (in the DESTINATION frame) under which
52
+ * departed-frame content mapped through `A` (departed → destination coords)
53
+ * renders EXACTLY as it did under `camPre` (in the departed frame) the instant
54
+ * before the cut. Derivation: a departed point q renders at
55
+ * `(A(q) − c0)·z0 = q·(A.s·z0) + (A.o − c0)·z0`; matching `(q − camPre)·zPre`
56
+ * coefficient-wise gives z0 and the origin below. Zoom limits deliberately do
57
+ * NOT apply here — flight paths transit far outside the user-gesture band
58
+ * (design-006 §3.1); only the ARRIVAL camera is clamped by the caller.
59
+ */
60
+ export declare function solveFlightStart(A: PortalAffine, camPre: CameraState): CameraState;
61
+ /** Zoom distance in octaves — the flight-length measure (design-006 §4/§5). */
62
+ export declare function flightOctaves(from: CameraState, to: CameraState): number;
63
+ /**
64
+ * Depth-cap a flight start (design-006 §5): beyond `maxOctaves`, clamp the
65
+ * start zoom to `capFactor ×` the arrival zoom (dive direction preserved),
66
+ * keeping the exact start's view CENTER — the portal anchor — so the motion
67
+ * still comes from/goes to the right place. `capped: true` means the caller
68
+ * must present the flight as a crossfade (frozen departing cover + incoming
69
+ * fade-in): geometric continuity is unpresentable at that raster-scale swing.
70
+ */
71
+ export declare function capFlightStart(exact: CameraState, arrival: CameraState, vpW: number, vpH: number, maxOctaves: number, capFactor: number): {
72
+ c0: CameraState;
73
+ capped: boolean;
74
+ };
75
+ /**
76
+ * The flight path `c(p)`, p ∈ [0,1]: zoom log-lerped (constant perceived zoom
77
+ * velocity); view CENTER linear in 1/zoom — exactly an anchored zoom whenever
78
+ * a fixed screen point exists between the endpoints, a graceful blend
79
+ * otherwise. Endpoints are exact: c(0) = c0, c(1) = c1.
80
+ */
81
+ export declare function flightCamera(c0: CameraState, c1: CameraState, p: number, vpW: number, vpH: number): CameraState;
82
+ /**
83
+ * One step of the critically-damped progress spring toward 1, in CLOSED FORM:
84
+ * with u = 1 − p, the exact solution is `u(t) = (u₀ + Bt)·e^(−ωt)`,
85
+ * `B = −v₀ + ω·u₀`. Unconditionally stable at ANY dt — the discretized form
86
+ * (semi-implicit Euler) diverges once ω·dt approaches 1, which one dropped
87
+ * frame at a fast response reaches easily (mock-measured, 2026-07-15).
88
+ * From rest (v=0) the approach is monotonic: p never overshoots 1.
89
+ */
90
+ export declare function springStep(p: number, v: number, omega: number, dtSec: number): {
91
+ p: number;
92
+ v: number;
93
+ };
@@ -3,15 +3,16 @@
3
3
  *
4
4
  * Owns everything ENGINE-facing: the module-side island state, the island
5
5
  * registry (scene+camera handles the frame pass iterates), per-island dirt
6
- * observers, the `r3fInvalidator` reflector, `useIslandFrame` callbacks, the
7
- * render write trap, and entity-death cleanup. Everything GL-facing (FBO
8
- * pool, quads, the actual passes) lives in `GLViews`, whose lifetime is the
9
- * Canvas/context's — the bridge outlives Canvas remounts (HMR) and carries
10
- * the invalidation wiring across them.
6
+ * observers, the `r3fInvalidator`/`r3fAdvance` reflectors, `useIslandFrame`
7
+ * callbacks, the render write trap, and entity-death cleanup. Everything
8
+ * GL-facing (FBO pool, quads, the actual passes) lives in `GLViews`, whose
9
+ * lifetime is the Canvas/context's — the bridge outlives Canvas remounts
10
+ * (HMR) and carries the frame wiring across them.
11
11
  *
12
12
  * Two-level invalidation, mechanized:
13
13
  * - COMPOSITE dirt (cheap — re-run the frame pass, sample cached textures):
14
- * camera/viewport via the reflector; per-island Position/StackZ Tier-3;
14
+ * camera/viewport via the reflector; per-island Position Tier-3; the frame
15
+ * ChildOf reorder wake (petition 8 — quad renderOrder is sibling order);
15
16
  * island enter/exit via register/unregister. All call `requestFrame()`.
16
17
  * - ISLAND dirt (expensive — repaint the FBO): per-island props-group and
17
18
  * Size Tier-3 → `bumpPaint()` + `requestFrame()`; the animation signal
@@ -20,11 +21,28 @@
20
21
  * `Camera.gesturing`; the gesture-end camera write re-fires the reflector,
21
22
  * so the once-at-gesture-end repaint needs no extra machinery).
22
23
  *
24
+ * REFLECT-PHASE ADVANCE (design-004 §3 amendment, 2026-07-20 — the pan-lag
25
+ * fix): the composite render is driven FROM the engine's reflect phase, not
26
+ * from R3F's demand loop. `requestFrame()` only latches `wantsFrame`; the
27
+ * `r3fAdvance` reflector (`always: true` — registered right after the
28
+ * observed invalidator so same-flush dirt is consumed) calls the wired
29
+ * `renderNow` (R3F `advance`) synchronously. DOM plane transforms and the GL
30
+ * composite therefore commit in the SAME task and present in the same frame.
31
+ * The prior wiring (`invalidate()` → R3F's own rAF) could only arm R3F for
32
+ * the NEXT frame, and rAF registration order between the two loops is sticky
33
+ * luck — field-measured on 2026-07-20: R3F rendering the PREVIOUS frame's
34
+ * camera while the DOM plane showed the current one, a permanent one-frame
35
+ * smear under fast pans. Ambient animation rides the same seam: the pass
36
+ * reports its self-sustain plan via `schedulePass`, and the due-time check
37
+ * replaces the old wake-up timer (the engine loop runs every frame anyway).
38
+ * The Canvas must mount `frameloop="never"` — a self-scheduling loop would
39
+ * re-render out of phase again.
40
+ *
23
41
  * Runtime-three-free (type-only imports): unit tests drive the bridge with a
24
42
  * real world and fake scene/camera handles, no GL context.
25
43
  */
26
44
  import type { OrthographicCamera, Scene } from "three";
27
- import { type Engine, type Entity } from "@ice/core";
45
+ import { type Engine, type Entity, type SiblingOrderIndex } from "@ice/core";
28
46
  import { type IslandStateStore } from "./island-state";
29
47
  import { type RenderWriteTrap } from "./dev-write-trap";
30
48
  export interface IslandHandle {
@@ -36,11 +54,22 @@ export type IslandFrameCallback = (dtMs: number) => void;
36
54
  export interface GLBridgeOpts {
37
55
  /** Arm the zero-render→ECS-writes trap (DEV builds / traces). Default false. */
38
56
  readonly devAssertRenderWrites?: boolean;
57
+ /**
58
+ * Injected monotonic clock for the ambient-animation due-time (tests pass a
59
+ * stub — the pool's injected-`now` precedent). Default `performance.now`.
60
+ */
61
+ readonly now?: () => number;
39
62
  }
40
63
  export interface GLBridge {
41
64
  readonly engine: Engine;
42
65
  readonly state: IslandStateStore;
43
66
  readonly renderAssert: RenderWriteTrap;
67
+ /**
68
+ * The frame ordinal cache (petition 8): the compositor pass reads quad
69
+ * renderOrder from it, and its `ordinals()` read resets the staleness the
70
+ * bridge's reorder wake filters on — bridge and pass MUST share it.
71
+ */
72
+ readonly order: SiblingOrderIndex;
44
73
  /**
45
74
  * Island mount (the `<Island>` component): registers the scene/camera pair,
46
75
  * stamps `animatedDecl` from the widget type, and arms the per-island dirt
@@ -54,14 +83,46 @@ export interface GLBridge {
54
83
  /** Subscribe a paint-time callback; holds an animation ref while live. */
55
84
  addFrameCallback(entity: Entity, cb: IslandFrameCallback): () => void;
56
85
  frameCallbacksFor(entity: Entity): readonly IslandFrameCallback[];
57
- /** Composite invalidation — schedule a frame pass (coalesced). */
86
+ /**
87
+ * Composite invalidation — latch a frame pass (coalesced). Consumed by the
88
+ * `r3fAdvance` reflector on the NEXT engine flush: dirt raised during a
89
+ * step (observers, the invalidator reflector) renders the SAME frame; dirt
90
+ * raised outside a step (React effects) renders on the next tick.
91
+ */
58
92
  requestFrame(): void;
59
93
  /** Island invalidation — this island must repaint (props/size/manual dirt). */
60
94
  bumpPaint(entity: Entity): void;
61
- /** Composite-quad lift scale (1 = rest). Scales the CARD on the canvas, corners intact. */
62
- setCompositeScale(entity: Entity, scale: number): void;
63
- /** GLViews wires R3F's demand-loop `invalidate` here (null on unmount). */
64
- setInvalidate(fn: (() => void) | null): void;
95
+ /**
96
+ * Composite-quad lift scale (1 = rest). Scales the CARD on the canvas,
97
+ * corners intact. Sets the TARGET: the compositor pass eases the drawn
98
+ * value there over `durationMs` (default 180 — the DOM card-lift spring;
99
+ * 0 snaps). Mid-flight retargets restart from the current drawn value.
100
+ */
101
+ setCompositeScale(entity: Entity, scale: number, durationMs?: number): void;
102
+ /**
103
+ * Composite-quad opacity (1 = rest) — the lift's fade twin. MULTIPLIES the
104
+ * widget's durable `Opacity` cell in the pass (presentation-transient over
105
+ * the doc fact). Sets the TARGET: the pass eases the drawn value there over
106
+ * `durationMs` (default 180; 0 snaps); mid-flight retargets restart from the
107
+ * current drawn value.
108
+ */
109
+ setCompositeOpacity(entity: Entity, opacity: number, durationMs?: number): void;
110
+ /**
111
+ * GLViews wires R3F's `advance` here (null on unmount). Called ONLY by the
112
+ * `r3fAdvance` reflector, synchronously inside the engine's reflect phase —
113
+ * the same task the DOM reflectors write in, so both surfaces present the
114
+ * same camera. While unwired, latched dirt survives and renders on the
115
+ * first flush after (re)mount.
116
+ */
117
+ setRenderNow(fn: ((nowMs: number) => void) | null): void;
118
+ /**
119
+ * The pass reports how the frame loop continues (`selfSustainPlan`):
120
+ * `"now"` latches an immediate next frame (lift eases, stagger backlogs —
121
+ * native refresh); a number arms the ambient-animation due-time (the
122
+ * `maxAnimationFps` cap — replaces the old wake-up timer); `"none"` parks.
123
+ * Each pass re-reports, so the due-time is always the LAST pass's plan.
124
+ */
125
+ schedulePass(plan: "none" | "now" | number): void;
65
126
  /** Tear down reflector + observers + state (doc close / app teardown). */
66
127
  uninstall(): void;
67
128
  }
@@ -9,9 +9,18 @@ import type { Texture } from "three";
9
9
  export declare class CompositeMaterial extends ShaderMaterial {
10
10
  private readonly mapUniform;
11
11
  private readonly opacityUniform;
12
+ private readonly draggedRectUniform;
13
+ private readonly isDraggedUniform;
12
14
  constructor();
13
15
  /** Bind the island's FBO texture (null clears). */
14
16
  setMap(map: Texture | null): void;
15
17
  /** Composite opacity. Pass-through — no clamping; callers pass valid 0..1. */
16
18
  setOpacity(opacity: number): void;
19
+ /**
20
+ * Drag clip, composite-space min/max (v1's uDraggedRect re-cut from screen
21
+ * space). (0,0,0,0) = inactive. Mutates the vec4 in place — no allocation.
22
+ */
23
+ setDraggedRect(minX: number, minY: number, maxX: number, maxY: number): void;
24
+ /** True on the dragged card's own quad — exempt from the clip. */
25
+ setIsDragged(isDragged: boolean): void;
17
26
  }
@@ -21,6 +21,17 @@ export interface QuadLike {
21
21
  setTexture(t: unknown): void;
22
22
  setVisible(v: boolean): void;
23
23
  setRenderOrder(n: number): void;
24
+ /**
25
+ * Composite opacity, 0..1 (design-004 §3: the neutral composite's whole
26
+ * per-widget fact) — the pass passes `Opacity` cell × eased fade channel.
27
+ */
28
+ setOpacity(o: number): void;
29
+ /**
30
+ * Drag clip (v1 RFC-003, composite-space): non-exempt quads discard
31
+ * fragments inside the rect so a dragged card's P1 DOM chrome shows
32
+ * through. (0,0,0,0, false) = inactive.
33
+ */
34
+ setDragClip(minX: number, minY: number, maxX: number, maxY: number, exempt: boolean): void;
24
35
  }
25
36
  export interface QuadsLike {
26
37
  ensure(key: number): QuadLike;
@@ -70,6 +81,15 @@ export interface PassContext {
70
81
  readonly compositeScene: object;
71
82
  readonly maxFboBytes: number;
72
83
  readonly maxRepaintsPerFrame: number;
84
+ /**
85
+ * Idle ceiling on island paint resolution (design-004 §3 amendment,
86
+ * 2026-07-14): new paints allocate at `min(pixelRatio, maxPaintDpr) × band`.
87
+ * On a dpr-2 display the composite's bilinear upscale from a 1.5× texture is
88
+ * visually indistinguishable (A/B knot crops) while Hot repaint cost drops
89
+ * ~22 %. Composes with the gesture drop (pixelRatio is already 1 while
90
+ * gesturing) and with zoom bands. `Infinity` = uncapped.
91
+ */
92
+ readonly maxPaintDpr: number;
73
93
  readonly dtMs: number;
74
94
  }
75
95
  export interface PassStats {
@@ -79,5 +99,7 @@ export interface PassStats {
79
99
  evicted: number;
80
100
  fboBytes: number;
81
101
  anyHot: boolean;
102
+ /** A composite ease (lift scale or fade) is mid-flight — caller re-invalidates (composite-only frames). */
103
+ liftAnimating: boolean;
82
104
  }
83
105
  export declare function runCompositorPass(ctx: PassContext): PassStats;
@@ -10,6 +10,22 @@ export interface GLViewsProps {
10
10
  readonly maxFboBytes?: number;
11
11
  /** Repaint stagger per composited frame (v1 default 4). */
12
12
  readonly maxRepaintsPerFrame?: number;
13
+ /**
14
+ * Ambient-animation rate cap (default 60; `Infinity` = uncapped). Applies
15
+ * ONLY to self-sustained frames — Hot islands and stagger backlogs park the
16
+ * loop between paints instead of spinning at native refresh (5 Hot islands
17
+ * at 120 Hz dpr-2 measured 96 % GPU duty at idle, 2026-07-14). Lift eases
18
+ * and externally invalidated frames (camera, drags, props dirt) still run
19
+ * at display rate. Approximate: frames land on the vsync grid.
20
+ */
21
+ readonly maxAnimationFps?: number;
22
+ /**
23
+ * Idle paint-DPR ceiling (default 1.5; `Infinity` = uncapped): island FBOs
24
+ * allocate at `min(pixelRatio, maxPaintDpr) × band`. On dpr-2 displays the
25
+ * composite's bilinear upscale from 1.5× is visually indistinguishable
26
+ * (2026-07-14 A/B) and cuts Hot repaint cost ~22 %.
27
+ */
28
+ readonly maxPaintDpr?: number;
13
29
  /**
14
30
  * Shared IBL: stamped as `scene.environment` on EVERY island's private
15
31
  * scene (arrival/change repaints all islands). Load it app-side (e.g.
@@ -17,5 +33,64 @@ export interface GLViewsProps {
17
33
  * fine; islands paint unlit-by-IBL and repaint when it lands.
18
34
  */
19
35
  readonly environment?: Texture | null;
36
+ /**
37
+ * GL frame profiling (2026-07-13): fires once per composited frame with the
38
+ * pass's real costs. CPU/counts/pass-shape are engine-measured every
39
+ * profiled frame; GPU ms rides stats-gl's headless `StatsProfiler`
40
+ * (EXT_disjoint_timer_query / WebGPU timestamps — 0 where unsupported, e.g.
41
+ * Safari without its flag), DYNAMICALLY imported on first use so the
42
+ * dependency costs nothing when profiling is off. Feed the numbers to the
43
+ * devtools profiler HUD as lanes (`devtools.lane("gpu", s.gpuMs)`).
44
+ */
45
+ readonly onFrameStats?: (stats: GlFrameStats) => void;
46
+ }
47
+ /**
48
+ * One profiled composite frame — see {@link GLViewsProps.onFrameStats}.
49
+ * MIRRORED structurally as `GlPanelStats` in @ice/devtools (which cannot
50
+ * import r3f under the walls) — keep the shapes aligned.
51
+ */
52
+ export interface GlFrameStats {
53
+ /** Whole-pass main-thread ms (reconcile + island paints + composite). */
54
+ readonly cpuMs: number;
55
+ /** GPU ms summed across the pass's render calls (0 until queries resolve / unsupported). */
56
+ readonly gpuMs: number;
57
+ /** stats-gl smoothed fps (0 until the profiler is live). */
58
+ readonly fps: number;
59
+ /** `renderer.info` aggregated across ALL of this pass's render calls. */
60
+ readonly drawCalls: number;
61
+ readonly triangles: number;
62
+ readonly points: number;
63
+ readonly lines: number;
64
+ /** Live compiled shader programs (renderer-lifetime, not per-frame). */
65
+ readonly programs: number;
66
+ readonly geometries: number;
67
+ readonly textures: number;
68
+ /** Virtual texturing: live island render targets + their summed resolution. */
69
+ readonly renderTargets: number;
70
+ readonly renderMegaPixels: number;
71
+ readonly fboBytes: number;
72
+ readonly fboBudgetBytes: number;
73
+ /** Island phase census (the demand/retention state machine, design-004 §3/§7). */
74
+ readonly islands: {
75
+ readonly total: number;
76
+ readonly hot: number;
77
+ readonly warm: number;
78
+ readonly waking: number;
79
+ readonly cold: number;
80
+ readonly dormant: number;
81
+ };
82
+ /** Live targets per painted zoom band ("×1", "×0.5", …) — the LOD downgrade census. */
83
+ readonly bandHistogram: Readonly<Record<string, number>>;
84
+ /** This pass's shape (PassStats). */
85
+ readonly repainted: number;
86
+ readonly pendingPaints: number;
87
+ readonly evicted: number;
88
+ /** LOD context: camera zoom → hysteresis band → the DPR new paints get. */
89
+ readonly zoom: number;
90
+ readonly band: number;
91
+ readonly effectiveDpr: number;
92
+ /** Scene population: Visible vs Culled widgets (all surfaces). */
93
+ readonly visibleWidgets: number;
94
+ readonly culledWidgets: number;
20
95
  }
21
- export declare function GLViews({ engine, bridge, store, maxFboBytes, maxRepaintsPerFrame, environment, }: GLViewsProps): ReactElement;
96
+ export declare function GLViews({ engine, bridge, store, maxFboBytes, maxRepaintsPerFrame, maxAnimationFps, maxPaintDpr, environment, onFrameStats, }: GLViewsProps): ReactElement;
@@ -56,5 +56,15 @@ export interface GLPointerRouterDeps {
56
56
  export interface GLPointerRouter {
57
57
  /** The adapter's `glRoute` seam. True = island content claimed the event. */
58
58
  route(kind: "down" | "move" | "up" | "cancel", screenX: number, screenY: number, native: PointerEvent): boolean;
59
+ /**
60
+ * Hover-time verdict as of the LAST routed `move` (design-002 §8 amendment,
61
+ * 2026-07-18): the pick chain under the pointer holds claim-capable island
62
+ * content — a mesh whose handlers include `onPointerDown`/`onClick` (the
63
+ * claim/click paths; hover-only handlers like the shapes swarm's move reader
64
+ * do NOT count — they never opt a down out). True for the whole life of an
65
+ * island capture. Feed it into the adapter's `GLRouteVerdict.overInteractive`
66
+ * on moves; presentation truth only, never a routing input.
67
+ */
68
+ overInteractive(): boolean;
59
69
  }
60
70
  export declare function createGLPointerRouter({ world, bridge, index }: GLPointerRouterDeps): GLPointerRouter;
@@ -6,11 +6,12 @@
6
6
  export { createGLBridge, type GLBridge, type GLBridgeOpts, type IslandHandle, type IslandFrameCallback } from "./bridge";
7
7
  export { createIslandStateStore, type IslandRenderState, type IslandStateStore, type PaintedAt } from "./island-state";
8
8
  export { runCompositorPass, type CompCameraLike, type GlLike, type IslandCameraLike, type PassContext, type PassStats, type PoolLike, type QuadLike, type QuadsLike, type TargetLike, } from "./compositor-pass";
9
- export { GLViews, type GLViewsProps } from "./gl-root";
9
+ export { GLViews, type GLViewsProps, type GlFrameStats } from "./gl-root";
10
10
  export { Island, type IslandProps } from "./island";
11
- export { IslandContext, useIslandContext, useIslandFrame, useIslandInvalidate, useIslandLift, type IslandContextValue, } from "./use-island-frame";
11
+ export { IslandContext, useIslandContext, useIslandFrame, useIslandInvalidate, useIslandLift, useIslandOpacity, type IslandContextValue, } from "./use-island-frame";
12
12
  export { RenderTargetPool, type PoolEntryInfo } from "./pool";
13
13
  export { ResourceRegistry } from "./resource-registry";
14
14
  export { CompositeMaterial } from "./composite-material";
15
15
  export { createRenderWriteTrap, type RenderWriteTrap } from "./dev-write-trap";
16
16
  export { createGLPointerRouter, type GLPointerRouter, type GLPointerRouterDeps, type IslandPointerEvent, } from "./gl-router";
17
+ export { captureWidgetPreviews, type CapturePreviewOpts } from "./preview-capture";
@@ -39,14 +39,58 @@ export interface IslandRenderState {
39
39
  * every pass (5 Hot vs cap 4 froze the shapes card, 2026-07-12).
40
40
  */
41
41
  lastPaintSeq: number;
42
+ /**
43
+ * Animation time OWED to this island's `useIslandFrame` callbacks: every
44
+ * pass a Hot island is paint-eligible adds the pass dtMs; the paint that
45
+ * finally runs delivers the whole balance and zeroes it. Without this, a
46
+ * stagger-deferred island simply LOST the deferred passes' time — 5 Hot vs
47
+ * cap 4 ran every animation at 4/5 speed (2026-07-20 rate probe), violating
48
+ * design-004's "the cap changes cadence, never speed". Freeze semantics are
49
+ * preserved: frozen islands are not paint-eligible, so a stage hold still
50
+ * PAUSES animation rather than banking it.
51
+ */
52
+ pendingDtMs: number;
42
53
  /**
43
54
  * Composite-quad scale (lift-on-hold): scales the CARD on the canvas —
44
55
  * texture, rounded alpha corners and all — instead of the scene content
45
56
  * (which the card-sized frustum would crop into hard corners, 2026-07-12
46
57
  * field report). ≠1 also pops render order over neighbors.
58
+ *
59
+ * This is the DRAWN value: the compositor pass eases it toward
60
+ * `liftTarget` (dtMs-driven spring). GL widgets have no CSS transitions, so
61
+ * the DOM-lift parity (2026-07-13 field report: "GL lift snaps") lives in
62
+ * the pass, not in userland.
47
63
  */
48
64
  compositeScale: number;
65
+ /** Where the lift is headed; `setCompositeScale` retargets from the drawn value. */
66
+ liftTarget: number;
67
+ /** Drawn value at (re)target time — the ease runs liftFrom → liftTarget. */
68
+ liftFrom: number;
69
+ /** Accumulated pass dtMs since the last retarget; ≥ liftMs ⇒ settled. */
70
+ liftElapsedMs: number;
71
+ /** Transition duration in ms; 0 = snap (the pre-ease behavior). */
72
+ liftMs: number;
73
+ /**
74
+ * Composite-quad opacity channel (the lift's fade twin): the DRAWN value the
75
+ * pass eases toward `opacityTarget` and MULTIPLIES with the widget's durable
76
+ * `Opacity` cell — presentation-transient (chrome-grade, e.g. the drag-lift
77
+ * fade) stacked over the doc-level fact, never written to ECS.
78
+ */
79
+ compositeOpacity: number;
80
+ /** Where the fade is headed; `setCompositeOpacity` retargets from the drawn value. */
81
+ opacityTarget: number;
82
+ /** Drawn value at (re)target time — the ease runs opacityFrom → opacityTarget. */
83
+ opacityFrom: number;
84
+ /** Accumulated pass dtMs since the last retarget; ≥ opacityMs ⇒ settled. */
85
+ opacityElapsedMs: number;
86
+ /** Transition duration in ms; 0 = snap. */
87
+ opacityMs: number;
49
88
  }
89
+ /**
90
+ * Default lift transition — the GL twin of the DOM card-lift idiom (180ms
91
+ * springy pop; the curve itself lives in the compositor pass).
92
+ */
93
+ export declare const LIFT_DEFAULT_MS = 180;
50
94
  export interface IslandStateStore {
51
95
  /** Get-or-create (Cold, unpainted). */
52
96
  ensure(key: number): IslandRenderState;
@@ -0,0 +1,19 @@
1
+ import type { Texture, WebGLRenderer } from "three";
2
+ export interface CapturePreviewOpts {
3
+ /** Types to capture (default: every registered gl-surface widget). */
4
+ readonly types?: readonly string[];
5
+ /**
6
+ * Shared IBL. Prefer the FACTORY form: PMREM textures live on the GPU of
7
+ * the renderer that generated them (no CPU image), so the app's main-canvas
8
+ * env is EMPTY on the capture renderer (field bug 2026-07-19 — black
9
+ * metals). The factory receives the capture renderer and builds the env
10
+ * there (e.g. `(gl) => new PMREMGenerator(gl).fromScene(new RoomEnvironment(), 0.04).texture`).
11
+ * A plain Texture is accepted for same-renderer/CPU-backed cases.
12
+ */
13
+ readonly environment?: Texture | ((gl: WebGLRenderer) => Texture | null) | null;
14
+ /** Pixel density of the capture (default: fit 512px on the long side, ≤2). */
15
+ readonly scale?: number;
16
+ /** Frames advanced before the capture (animations reach a live pose). */
17
+ readonly settleFrames?: number;
18
+ }
19
+ export declare function captureWidgetPreviews(opts?: CapturePreviewOpts): Promise<string[]>;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Self-sustain scheduling for the demand loop (design-004 §3 animation-rate
3
+ * amendment, 2026-07-14).
4
+ *
5
+ * When the ONLY reason to run another frame is ambient animation (Hot islands
6
+ * or a stagger backlog), the loop no longer re-invalidates at native refresh:
7
+ * on a 120 Hz panel that was 5 always-Hot islands × 120 paints/sec ≈ 96 % GPU
8
+ * duty at idle — the fan. Instead the next pass is scheduled `1000/maxFps` ms
9
+ * after the last pass STARTED, so ambient animation runs at ~`maxFps` and the
10
+ * GPU idles between frames.
11
+ *
12
+ * What stays at native refresh:
13
+ * - lift eases (`liftAnimating`) — 180 ms of interaction feedback;
14
+ * - every EXTERNALLY invalidated frame (camera, drag, props dirt): the
15
+ * bridge's `requestFrame` path is untouched, so interaction still runs at
16
+ * display rate and Hot islands ride along uncapped while it lasts.
17
+ *
18
+ * The cap is approximate: the wake-up is a timeout and the actual frame lands
19
+ * on the next vsync after it. {@link RATE_CAP_SLACK_MS} pulls the timeout half
20
+ * a 120 Hz vsync early so the following vsync is the intended one, not one
21
+ * past it (naive `interval` scheduling on a 120 Hz grid yields 40 fps, not 60).
22
+ */
23
+ /** Timeout lead so the post-timeout vsync lands on the target boundary. */
24
+ export declare const RATE_CAP_SLACK_MS = 4;
25
+ export interface SelfSustainStats {
26
+ readonly anyHot: boolean;
27
+ readonly pendingPaints: number;
28
+ readonly liftAnimating: boolean;
29
+ }
30
+ /**
31
+ * Decide how the demand loop continues after a pass.
32
+ *
33
+ * @param stats The pass's self-sustain signals.
34
+ * @param elapsedMs ms since the pass started (schedule anchor).
35
+ * @param maxFps Animation rate cap; `Infinity` restores uncapped.
36
+ * @returns `"none"` (park), `"now"` (invalidate immediately), or the timeout
37
+ * delay in ms before invalidating.
38
+ */
39
+ export declare function selfSustainPlan(stats: SelfSustainStats, elapsedMs: number, maxFps: number): "none" | "now" | number;