@vibecook/ice 0.1.0 → 0.3.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 (119) hide show
  1. package/README.md +37 -2
  2. package/dist/chunk-2GUYATUM.js +643 -0
  3. package/dist/chunk-2GUYATUM.js.map +1 -0
  4. package/dist/{chunk-5S7GSXJG.js → chunk-5QY7HK6G.js} +3145 -1306
  5. package/dist/chunk-5QY7HK6G.js.map +1 -0
  6. package/dist/{chunk-BCJ47SX7.js → chunk-OR5FNVUE.js} +460 -1
  7. package/dist/chunk-OR5FNVUE.js.map +1 -0
  8. package/dist/{chunk-FIAXPNNY.js → chunk-PKK6GLSA.js} +143 -551
  9. package/dist/chunk-PKK6GLSA.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 +80 -2
  16. package/dist/kernel.js +31 -1
  17. package/dist/r3f.js +605 -62
  18. package/dist/r3f.js.map +1 -1
  19. package/dist/react.js +36 -418
  20. package/dist/react.js.map +1 -1
  21. package/dist/types/packages/core/src/catalog/camera-derived.d.ts +65 -3
  22. package/dist/types/packages/core/src/catalog/gesture.d.ts +40 -4
  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 +49 -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/engine.d.ts +11 -0
  41. package/dist/types/packages/core/src/engine/frame-control.d.ts +71 -0
  42. package/dist/types/packages/core/src/engine/frame-info.d.ts +16 -1
  43. package/dist/types/packages/core/src/facade/create-canvas-engine.d.ts +79 -6
  44. package/dist/types/packages/core/src/guards/guarded-tx.d.ts +4 -2
  45. package/dist/types/packages/core/src/helpers/churn-guard.d.ts +40 -0
  46. package/dist/types/packages/core/src/index.d.ts +15 -7
  47. package/dist/types/packages/core/src/input/queue.d.ts +11 -0
  48. package/dist/types/packages/core/src/interaction/gesture-status.d.ts +13 -0
  49. package/dist/types/packages/core/src/nav/nested-canvas.d.ts +47 -11
  50. package/dist/types/packages/core/src/ops/arrange.d.ts +15 -0
  51. package/dist/types/packages/core/src/ops/insert.d.ts +62 -0
  52. package/dist/types/packages/core/src/ops/point-pick.d.ts +11 -7
  53. package/dist/types/packages/core/src/ops/sibling-order.d.ts +70 -0
  54. package/dist/types/packages/core/src/schema/meta.d.ts +1 -0
  55. package/dist/types/packages/core/src/settings/defaults.d.ts +45 -0
  56. package/dist/types/packages/core/src/settings/ground-config.d.ts +55 -0
  57. package/dist/types/packages/core/src/systems/chrome.d.ts +6 -2
  58. package/dist/types/packages/core/src/systems/insert-ghost.d.ts +25 -0
  59. package/dist/types/packages/core/src/systems/l1-pick.d.ts +25 -11
  60. package/dist/types/packages/core/src/systems/l2-recognize.d.ts +10 -4
  61. package/dist/types/packages/core/src/systems/l3-behave.d.ts +6 -4
  62. package/dist/types/packages/core/src/systems/l3-claim.d.ts +7 -3
  63. package/dist/types/packages/core/src/systems/l3-drop.d.ts +3 -1
  64. package/dist/types/packages/core/src/systems/l3-resize.d.ts +3 -1
  65. package/dist/types/packages/core/src/systems/l3-snap.d.ts +25 -7
  66. package/dist/types/packages/core/src/systems/nav-flight.d.ts +95 -0
  67. package/dist/types/packages/core/src/widget/define-widget.d.ts +39 -0
  68. package/dist/types/packages/core/src/widget/mount-store.d.ts +11 -3
  69. package/dist/types/packages/core/src/widget/set-props.d.ts +16 -0
  70. package/dist/types/packages/core/src/widget/spawn.d.ts +21 -1
  71. package/dist/types/packages/devtools/src/attach.d.ts +77 -0
  72. package/dist/types/packages/devtools/src/dock.d.ts +38 -0
  73. package/dist/types/packages/devtools/src/gl-panel.d.ts +69 -0
  74. package/dist/types/packages/devtools/src/index.d.ts +8 -6
  75. package/dist/types/packages/dom/src/index.d.ts +1 -3
  76. package/dist/types/packages/dom/src/loop.d.ts +11 -0
  77. package/dist/types/packages/dom/src/measure-wiring.d.ts +2 -2
  78. package/dist/types/packages/dom/src/pointer-adapter.d.ts +21 -1
  79. package/dist/types/packages/dom/src/reflectors/dom-widgets.d.ts +5 -0
  80. package/dist/types/packages/ground/src/index.d.ts +52 -0
  81. package/dist/types/packages/ground/src/layer.d.ts +40 -0
  82. package/dist/types/packages/ground/src/pass.d.ts +41 -0
  83. package/dist/types/packages/ground/src/passes/grid.d.ts +21 -0
  84. package/dist/types/packages/ground/src/passes/guides-collect.d.ts +14 -0
  85. package/dist/types/packages/ground/src/passes/guides.d.ts +3 -0
  86. package/dist/types/packages/ground/src/passes/soup-collect.d.ts +35 -0
  87. package/dist/types/packages/ground/src/passes/soup-mesh.d.ts +8 -0
  88. package/dist/types/packages/ground/src/passes/wires-collect.d.ts +31 -0
  89. package/dist/types/packages/ground/src/passes/wires.d.ts +10 -0
  90. package/dist/types/packages/ground/src/renderer.d.ts +15 -0
  91. package/dist/types/packages/ice/src/ground.d.ts +1 -0
  92. package/dist/types/packages/kernel/src/bezier.d.ts +15 -0
  93. package/dist/types/packages/kernel/src/coords.d.ts +15 -0
  94. package/dist/types/packages/kernel/src/easing.d.ts +11 -0
  95. package/dist/types/packages/kernel/src/index.d.ts +3 -0
  96. package/dist/types/packages/kernel/src/layout.d.ts +118 -0
  97. package/dist/types/packages/kernel/src/nav-flight.d.ts +93 -0
  98. package/dist/types/packages/r3f/src/bridge.d.ts +73 -12
  99. package/dist/types/packages/r3f/src/composite-material.d.ts +9 -0
  100. package/dist/types/packages/r3f/src/compositor-pass.d.ts +22 -0
  101. package/dist/types/packages/r3f/src/gl-root.d.ts +76 -1
  102. package/dist/types/packages/r3f/src/gl-router.d.ts +10 -0
  103. package/dist/types/packages/r3f/src/index.d.ts +3 -2
  104. package/dist/types/packages/r3f/src/island-state.d.ts +44 -0
  105. package/dist/types/packages/r3f/src/preview-capture.d.ts +19 -0
  106. package/dist/types/packages/r3f/src/self-sustain.d.ts +39 -0
  107. package/dist/types/packages/r3f/src/use-island-frame.d.ts +16 -1
  108. package/dist/types/packages/react/src/engine-context.d.ts +21 -0
  109. package/dist/types/packages/react/src/index.d.ts +4 -2
  110. package/dist/types/packages/react/src/infinite-canvas.d.ts +40 -6
  111. package/dist/types/packages/react/src/preview-snapshots.d.ts +17 -0
  112. package/dist/types/packages/react/src/widget-preview.d.ts +14 -0
  113. package/package.json +32 -61
  114. package/dist/chunk-5S7GSXJG.js.map +0 -1
  115. package/dist/chunk-BCJ47SX7.js.map +0 -1
  116. package/dist/chunk-FIAXPNNY.js.map +0 -1
  117. package/dist/types/packages/devtools/src/panel.d.ts +0 -48
  118. package/dist/types/packages/dom/src/reflectors/grid.d.ts +0 -63
  119. package/dist/types/packages/dom/src/reflectors/wires.d.ts +0 -88
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Wires collector — the pure ECS→triangles half of the wires pass. Port of
3
+ * the 2D-canvas wires reflector's drawing, kernel-tessellated:
4
+ *
5
+ * - Wire geometry NEVER reads a port ENTITY (design-001 §5.3): endpoints
6
+ * resolve from the wire's `WireFrom`/`WireTo` widgets + `WirePorts` ids
7
+ * against the type registry's static port schema (core `portSlots` — the
8
+ * old local resolver's TODO, retired), so a CULLED endpoint still draws.
9
+ * - Cubics build in WORLD space then map control points through the kernel
10
+ * `worldToScreen` seam — bezier curves are affine-invariant, so the mapped
11
+ * cubic IS the screen curve — and `tessellateCubic` (0.25 px tolerance)
12
+ * flattens with density that tracks zoom for free.
13
+ * - Connect preview: solid when snapped-compatible, dashed [6,4] otherwise
14
+ * (dash placement from the tessellation's arc-length prefix).
15
+ * - Materialized port dots: triangle-fan discs, active color while any
16
+ * connect drag is routed.
17
+ */
18
+ import { type WirePreviewBuffer, type WiresConfig, type World } from "@ice/core";
19
+ import type { GroundFrame } from "../pass";
20
+ import { type TriSoup } from "./soup-collect";
21
+ export declare const wireQ: import("@vibecook/strata-ecs").Query;
22
+ export declare const geometryQ: import("@vibecook/strata-ecs").Query;
23
+ export declare const measuredQ: import("@vibecook/strata-ecs").Query;
24
+ export declare const selectedQ: import("@vibecook/strata-ecs").Query;
25
+ export declare const portQ: import("@vibecook/strata-ecs").Query;
26
+ export declare const routedConnectQ: import("@vibecook/strata-ecs").Query;
27
+ /** A live connect drag moves the preview every frame — Drag value dirt covers it. */
28
+ export declare const connectDragQ: import("@vibecook/strata-ecs").Query;
29
+ /** Nav churn signal: Active membership flips re-collect (scope filter below). */
30
+ export declare const activeQ: import("@vibecook/strata-ecs").Query;
31
+ export declare function collectWires(world: World, frame: GroundFrame, config?: WiresConfig, preview?: WirePreviewBuffer): TriSoup;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Wires pass — the three-side wrapper over the pure `collectWires` (renders
3
+ * above the grid, below the guides, matching the old P0 canvas order).
4
+ * Dirt mirrors the old 2D reflector's observers; the connect preview rides
5
+ * `[RoutedConnect, Drag]` value dirt (the buffer updates exactly when the
6
+ * connect recognizer's Drag advances) instead of a per-frame value diff.
7
+ */
8
+ import { type WirePreviewBuffer, type WiresConfig } from "@ice/core";
9
+ import type { GroundPass } from "../pass";
10
+ export declare function createWiresPass(initial?: Partial<WiresConfig>, readPreview?: () => WirePreviewBuffer): GroundPass;
@@ -0,0 +1,15 @@
1
+ import type { Camera, Scene } from "three/webgpu";
2
+ /** The layer's renderer seam — tests inject a fake (compositor `GlLike` precedent). */
3
+ export interface GroundRendererLike {
4
+ readonly canvas: HTMLCanvasElement;
5
+ ready(): boolean;
6
+ failed(): boolean;
7
+ /** Register a one-shot "became ready" callback (fires immediately if already ready). */
8
+ onReady(cb: () => void): void;
9
+ setSize(cssW: number, cssH: number, dpr: number): void;
10
+ render(scene: Scene, camera: Camera): void;
11
+ dispose(): void;
12
+ }
13
+ export declare function createGroundRenderer(doc: Document, opts?: {
14
+ forceWebGL?: boolean;
15
+ }): GroundRendererLike;
@@ -0,0 +1 @@
1
+ export * from "../../ground/src/index";
@@ -29,6 +29,21 @@ export declare function cubicPoint(c: Cubic, t: number): Vec2;
29
29
  * culling/picking prefilters.
30
30
  */
31
31
  export declare function cubicAABB(c: Cubic): AABB;
32
+ export interface Tessellation {
33
+ /** Flattened polyline, interleaved [x0,y0, x1,y1, …] in the cubic's own space. */
34
+ points: Float32Array;
35
+ /** Cumulative arc length at each polyline point (same unit; [0] = 0). */
36
+ arcLength: Float32Array;
37
+ }
38
+ /**
39
+ * Adaptive cubic flattening + prefix arc length (2026-07-16, the @ice/ground
40
+ * wires pass: stroke tessellation and dash placement both ride this). Emit
41
+ * density follows curvature — flat spans emit one segment regardless of
42
+ * length; tolerance is in the cubic's OWN space (the ground renderer passes
43
+ * screen-px cubics with ~0.25 px tolerance, so density tracks zoom for free).
44
+ * `maxDepth` bounds pathological inputs (2^depth segments worst case).
45
+ */
46
+ export declare function tessellateCubic(c: Cubic, tolerance?: number, maxDepth?: number): Tessellation;
32
47
  /** Distance from a point to a line segment. */
33
48
  export declare function distanceToSegment(px: number, py: number, ax: number, ay: number, bx: number, by: number): number;
34
49
  /**
@@ -26,6 +26,21 @@ export declare function worldToScreen(worldX: number, worldY: number, camera: Ca
26
26
  * Returns the new camera (does not mutate). Clamping is the caller's policy.
27
27
  */
28
28
  export declare function zoomAtPoint(camera: CameraState, screenX: number, screenY: number, newZoom: number): CameraState;
29
+ /**
30
+ * The DEFAULT framing camera (2026-07-18, James: "do zoom to fit, but with a
31
+ * upper and bottom cap … should feel natural"): zoom-to-fit `content` (a
32
+ * world-space bbox) with `pad` px of breathing room on every side, zoom
33
+ * clamped into [minZoom, maxZoom] — the natural band: never so far in that
34
+ * one small card fills the screen, never so far out that the board turns
35
+ * into an ant farm. The content CENTER stays centered whether or not a cap
36
+ * engages. Callers own the band policy (engine default: FIT_DEFAULTS ∩ the
37
+ * session's hard CameraLimits) and must pass a real viewport.
38
+ */
39
+ export declare function fitCamera(content: Rect, vpW: number, vpH: number, opts: {
40
+ pad: number;
41
+ minZoom: number;
42
+ maxZoom: number;
43
+ }): CameraState;
29
44
  /**
30
45
  * Camera → the ONE per-plane CSS transform (design-002 §5 `planeTransform`).
31
46
  * Children are laid out in WORLD units inside the plane (left/top = world x/y),
@@ -0,0 +1,11 @@
1
+ /**
2
+ * CSS-style cubic-bezier timing functions (pure math — kernel).
3
+ *
4
+ * `cubicBezierEase(x1, y1, x2, y2)` builds f(t): the exact curve CSS
5
+ * `transition-timing-function: cubic-bezier(…)` runs, so GL surfaces can
6
+ * match DOM transitions beat-for-beat (the composite-quad lift mirrors the
7
+ * DOM card lift's spring, cubic-bezier(0.2, 0.9, 0.3, 1.2)). y outside [0,1]
8
+ * (overshoot springs) is legal and expected; x1/x2 are clamped to [0,1] per
9
+ * spec so x(u) stays monotone and invertible.
10
+ */
11
+ export declare function cubicBezierEase(x1: number, y1: number, x2: number, y2: number): (t: number) => number;
@@ -11,3 +11,6 @@ export * from "./zoom-bands";
11
11
  export * from "./eviction";
12
12
  export * from "./anchors";
13
13
  export * from "./bezier";
14
+ export * from "./easing";
15
+ export * from "./nav-flight";
16
+ export * from "./layout";
@@ -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";