@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.
- package/README.md +37 -2
- package/dist/chunk-5SJYHQI4.js +636 -0
- package/dist/chunk-5SJYHQI4.js.map +1 -0
- package/dist/{chunk-5S7GSXJG.js → chunk-DXU3RZZS.js} +2913 -1180
- package/dist/chunk-DXU3RZZS.js.map +1 -0
- package/dist/{chunk-FIAXPNNY.js → chunk-IASTQMU5.js} +130 -551
- package/dist/chunk-IASTQMU5.js.map +1 -0
- package/dist/{chunk-BCJ47SX7.js → chunk-OR5FNVUE.js} +460 -1
- package/dist/chunk-OR5FNVUE.js.map +1 -0
- package/dist/devtools.js +508 -372
- package/dist/devtools.js.map +1 -1
- package/dist/dom.js +3 -11
- package/dist/ground.js +715 -0
- package/dist/ground.js.map +1 -0
- package/dist/index.js +70 -2
- package/dist/kernel.js +31 -1
- package/dist/r3f.js +599 -62
- package/dist/r3f.js.map +1 -1
- package/dist/react.js +34 -418
- package/dist/react.js.map +1 -1
- package/dist/types/packages/core/src/catalog/camera-derived.d.ts +41 -3
- package/dist/types/packages/core/src/catalog/gesture.d.ts +39 -3
- package/dist/types/packages/core/src/catalog/graph.d.ts +7 -6
- package/dist/types/packages/core/src/catalog/index.d.ts +1 -0
- package/dist/types/packages/core/src/catalog/insert.d.ts +31 -0
- package/dist/types/packages/core/src/catalog/pointer.d.ts +11 -0
- package/dist/types/packages/core/src/catalog/scene.d.ts +37 -4
- package/dist/types/packages/core/src/catalog/selection-presence.d.ts +7 -0
- package/dist/types/packages/core/src/catalog/settings-resources.d.ts +21 -0
- package/dist/types/packages/core/src/doc/autosave.d.ts +15 -2
- package/dist/types/packages/core/src/doc/bootstrap.d.ts +29 -0
- package/dist/types/packages/core/src/doc/channels.d.ts +9 -0
- package/dist/types/packages/core/src/doc/doc-commit-sink.d.ts +2 -1
- package/dist/types/packages/core/src/doc/doc-kit.d.ts +16 -1
- package/dist/types/packages/core/src/doc/envelope.d.ts +2 -1
- package/dist/types/packages/core/src/doc/migrate.d.ts +8 -6
- package/dist/types/packages/core/src/doc/schema-migrate.d.ts +50 -0
- package/dist/types/packages/core/src/doc/version-gate.d.ts +3 -0
- package/dist/types/packages/core/src/engine/commit-sink.d.ts +24 -2
- package/dist/types/packages/core/src/engine/frame-info.d.ts +16 -1
- package/dist/types/packages/core/src/facade/create-canvas-engine.d.ts +70 -6
- package/dist/types/packages/core/src/guards/guarded-tx.d.ts +4 -2
- package/dist/types/packages/core/src/helpers/churn-guard.d.ts +40 -0
- package/dist/types/packages/core/src/index.d.ts +13 -7
- package/dist/types/packages/core/src/input/queue.d.ts +11 -0
- package/dist/types/packages/core/src/nav/nested-canvas.d.ts +47 -11
- package/dist/types/packages/core/src/ops/arrange.d.ts +15 -0
- package/dist/types/packages/core/src/ops/insert.d.ts +62 -0
- package/dist/types/packages/core/src/ops/point-pick.d.ts +11 -7
- package/dist/types/packages/core/src/ops/sibling-order.d.ts +70 -0
- package/dist/types/packages/core/src/schema/meta.d.ts +1 -0
- package/dist/types/packages/core/src/settings/defaults.d.ts +45 -0
- package/dist/types/packages/core/src/settings/ground-config.d.ts +55 -0
- package/dist/types/packages/core/src/systems/chrome.d.ts +6 -2
- package/dist/types/packages/core/src/systems/insert-ghost.d.ts +25 -0
- package/dist/types/packages/core/src/systems/l1-pick.d.ts +25 -11
- package/dist/types/packages/core/src/systems/l2-recognize.d.ts +10 -4
- package/dist/types/packages/core/src/systems/l3-behave.d.ts +6 -4
- package/dist/types/packages/core/src/systems/l3-claim.d.ts +7 -3
- package/dist/types/packages/core/src/systems/l3-drop.d.ts +3 -1
- package/dist/types/packages/core/src/systems/l3-resize.d.ts +3 -1
- package/dist/types/packages/core/src/systems/l3-snap.d.ts +25 -7
- package/dist/types/packages/core/src/systems/nav-flight.d.ts +95 -0
- package/dist/types/packages/core/src/widget/define-widget.d.ts +39 -0
- package/dist/types/packages/core/src/widget/mount-store.d.ts +11 -3
- package/dist/types/packages/core/src/widget/set-props.d.ts +16 -0
- package/dist/types/packages/core/src/widget/spawn.d.ts +21 -1
- package/dist/types/packages/devtools/src/attach.d.ts +77 -0
- package/dist/types/packages/devtools/src/dock.d.ts +38 -0
- package/dist/types/packages/devtools/src/gl-panel.d.ts +69 -0
- package/dist/types/packages/devtools/src/index.d.ts +8 -6
- package/dist/types/packages/dom/src/index.d.ts +1 -3
- package/dist/types/packages/dom/src/measure-wiring.d.ts +2 -2
- package/dist/types/packages/dom/src/pointer-adapter.d.ts +21 -1
- package/dist/types/packages/dom/src/reflectors/dom-widgets.d.ts +5 -0
- package/dist/types/packages/ground/src/index.d.ts +52 -0
- package/dist/types/packages/ground/src/layer.d.ts +40 -0
- package/dist/types/packages/ground/src/pass.d.ts +41 -0
- package/dist/types/packages/ground/src/passes/grid.d.ts +21 -0
- package/dist/types/packages/ground/src/passes/guides-collect.d.ts +14 -0
- package/dist/types/packages/ground/src/passes/guides.d.ts +3 -0
- package/dist/types/packages/ground/src/passes/soup-collect.d.ts +35 -0
- package/dist/types/packages/ground/src/passes/soup-mesh.d.ts +8 -0
- package/dist/types/packages/ground/src/passes/wires-collect.d.ts +31 -0
- package/dist/types/packages/ground/src/passes/wires.d.ts +10 -0
- package/dist/types/packages/ground/src/renderer.d.ts +15 -0
- package/dist/types/packages/ice/src/ground.d.ts +1 -0
- package/dist/types/packages/kernel/src/bezier.d.ts +15 -0
- package/dist/types/packages/kernel/src/coords.d.ts +15 -0
- package/dist/types/packages/kernel/src/easing.d.ts +11 -0
- package/dist/types/packages/kernel/src/index.d.ts +3 -0
- package/dist/types/packages/kernel/src/layout.d.ts +118 -0
- package/dist/types/packages/kernel/src/nav-flight.d.ts +93 -0
- package/dist/types/packages/r3f/src/bridge.d.ts +73 -12
- package/dist/types/packages/r3f/src/composite-material.d.ts +9 -0
- package/dist/types/packages/r3f/src/compositor-pass.d.ts +22 -0
- package/dist/types/packages/r3f/src/gl-root.d.ts +76 -1
- package/dist/types/packages/r3f/src/gl-router.d.ts +10 -0
- package/dist/types/packages/r3f/src/index.d.ts +3 -2
- package/dist/types/packages/r3f/src/island-state.d.ts +44 -0
- package/dist/types/packages/r3f/src/preview-capture.d.ts +19 -0
- package/dist/types/packages/r3f/src/self-sustain.d.ts +39 -0
- package/dist/types/packages/r3f/src/use-island-frame.d.ts +16 -1
- package/dist/types/packages/react/src/engine-context.d.ts +8 -0
- package/dist/types/packages/react/src/index.d.ts +4 -2
- package/dist/types/packages/react/src/infinite-canvas.d.ts +40 -6
- package/dist/types/packages/react/src/preview-snapshots.d.ts +17 -0
- package/dist/types/packages/react/src/widget-preview.d.ts +14 -0
- package/package.json +13 -8
- package/dist/chunk-5S7GSXJG.js.map +0 -1
- package/dist/chunk-BCJ47SX7.js.map +0 -1
- package/dist/chunk-FIAXPNNY.js.map +0 -1
- package/dist/types/packages/devtools/src/panel.d.ts +0 -48
- package/dist/types/packages/dom/src/reflectors/grid.d.ts +0 -63
- 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`
|
|
7
|
-
* render write trap, and entity-death cleanup. Everything
|
|
8
|
-
* pool, quads, the actual passes) lives in `GLViews`, whose
|
|
9
|
-
* Canvas/context's — the bridge outlives Canvas remounts
|
|
10
|
-
* the
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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;
|