@unrulysystems/native-motion-core 0.1.0-alpha.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/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +48 -0
- package/dist/clock.cjs +71 -0
- package/dist/clock.d.cts +23 -0
- package/dist/clock.d.ts +23 -0
- package/dist/clock.js +66 -0
- package/dist/collect-reads.cjs +68 -0
- package/dist/collect-reads.d.cts +23 -0
- package/dist/collect-reads.d.ts +23 -0
- package/dist/collect-reads.js +62 -0
- package/dist/component/boundedArray.cjs +74 -0
- package/dist/component/boundedArray.d.cts +25 -0
- package/dist/component/boundedArray.d.ts +25 -0
- package/dist/component/boundedArray.js +68 -0
- package/dist/component/index.cjs +57 -0
- package/dist/component/index.d.cts +12 -0
- package/dist/component/index.d.ts +12 -0
- package/dist/component/index.js +22 -0
- package/dist/component/orchestration.cjs +129 -0
- package/dist/component/orchestration.d.cts +66 -0
- package/dist/component/orchestration.d.ts +66 -0
- package/dist/component/orchestration.js +124 -0
- package/dist/component/resolve.cjs +104 -0
- package/dist/component/resolve.d.cts +11 -0
- package/dist/component/resolve.d.ts +11 -0
- package/dist/component/resolve.js +97 -0
- package/dist/component/transition.cjs +352 -0
- package/dist/component/transition.d.cts +33 -0
- package/dist/component/transition.d.ts +33 -0
- package/dist/component/transition.js +339 -0
- package/dist/component/types.cjs +114 -0
- package/dist/component/types.d.cts +60 -0
- package/dist/component/types.d.ts +60 -0
- package/dist/component/types.js +111 -0
- package/dist/component/validate.cjs +1015 -0
- package/dist/component/validate.d.cts +37 -0
- package/dist/component/validate.d.ts +37 -0
- package/dist/component/validate.js +1002 -0
- package/dist/component/variants.cjs +333 -0
- package/dist/component/variants.d.cts +106 -0
- package/dist/component/variants.d.ts +106 -0
- package/dist/component/variants.js +321 -0
- package/dist/config/constants.cjs +41 -0
- package/dist/config/constants.d.cts +29 -0
- package/dist/config/constants.d.ts +29 -0
- package/dist/config/constants.js +38 -0
- package/dist/delay.cjs +72 -0
- package/dist/delay.d.cts +26 -0
- package/dist/delay.d.ts +26 -0
- package/dist/delay.js +70 -0
- package/dist/derived.cjs +143 -0
- package/dist/derived.d.cts +41 -0
- package/dist/derived.d.ts +41 -0
- package/dist/derived.js +139 -0
- package/dist/driver/index.cjs +22 -0
- package/dist/driver/index.d.cts +7 -0
- package/dist/driver/index.d.ts +7 -0
- package/dist/driver/index.js +14 -0
- package/dist/driver/keyframeTiming.cjs +130 -0
- package/dist/driver/keyframeTiming.d.cts +18 -0
- package/dist/driver/keyframeTiming.d.ts +18 -0
- package/dist/driver/keyframeTiming.js +124 -0
- package/dist/driver/keyframeTimingConfig.cjs +24 -0
- package/dist/driver/keyframeTimingConfig.d.cts +5 -0
- package/dist/driver/keyframeTimingConfig.d.ts +5 -0
- package/dist/driver/keyframeTimingConfig.js +22 -0
- package/dist/driver/prepare.cjs +459 -0
- package/dist/driver/prepare.d.cts +4 -0
- package/dist/driver/prepare.d.ts +4 -0
- package/dist/driver/prepare.js +454 -0
- package/dist/driver/reference.cjs +762 -0
- package/dist/driver/reference.d.cts +53 -0
- package/dist/driver/reference.d.ts +53 -0
- package/dist/driver/reference.js +757 -0
- package/dist/driver/step.cjs +55 -0
- package/dist/driver/step.d.cts +9 -0
- package/dist/driver/step.d.ts +9 -0
- package/dist/driver/step.js +52 -0
- package/dist/driver/tiers.cjs +40 -0
- package/dist/driver/tiers.d.cts +2 -0
- package/dist/driver/tiers.d.ts +2 -0
- package/dist/driver/tiers.js +37 -0
- package/dist/driver/types.cjs +9 -0
- package/dist/driver/types.d.cts +68 -0
- package/dist/driver/types.d.ts +68 -0
- package/dist/driver/types.js +8 -0
- package/dist/external-animation-ledger.cjs +1250 -0
- package/dist/external-animation-ledger.d.cts +416 -0
- package/dist/external-animation-ledger.d.ts +416 -0
- package/dist/external-animation-ledger.js +1243 -0
- package/dist/gesture/directionLock.cjs +25 -0
- package/dist/gesture/directionLock.d.cts +8 -0
- package/dist/gesture/directionLock.d.ts +8 -0
- package/dist/gesture/directionLock.js +21 -0
- package/dist/gesture/dragConfig.cjs +634 -0
- package/dist/gesture/dragConfig.d.cts +298 -0
- package/dist/gesture/dragConfig.d.ts +298 -0
- package/dist/gesture/dragConfig.js +624 -0
- package/dist/gesture/elastic.cjs +44 -0
- package/dist/gesture/elastic.d.cts +4 -0
- package/dist/gesture/elastic.d.ts +4 -0
- package/dist/gesture/elastic.js +39 -0
- package/dist/gesture/handoffSession.cjs +161 -0
- package/dist/gesture/handoffSession.d.cts +35 -0
- package/dist/gesture/handoffSession.d.ts +35 -0
- package/dist/gesture/handoffSession.js +158 -0
- package/dist/gesture/index.cjs +37 -0
- package/dist/gesture/index.d.cts +12 -0
- package/dist/gesture/index.d.ts +12 -0
- package/dist/gesture/index.js +11 -0
- package/dist/gesture/projection.cjs +95 -0
- package/dist/gesture/projection.d.cts +7 -0
- package/dist/gesture/projection.d.ts +7 -0
- package/dist/gesture/projection.js +87 -0
- package/dist/gesture/session.cjs +162 -0
- package/dist/gesture/session.d.cts +29 -0
- package/dist/gesture/session.d.ts +29 -0
- package/dist/gesture/session.js +158 -0
- package/dist/gesture/types.cjs +5 -0
- package/dist/gesture/types.d.cts +13 -0
- package/dist/gesture/types.d.ts +13 -0
- package/dist/gesture/types.js +4 -0
- package/dist/gesture/viewportConstraints.cjs +38 -0
- package/dist/gesture/viewportConstraints.d.cts +6 -0
- package/dist/gesture/viewportConstraints.d.ts +6 -0
- package/dist/gesture/viewportConstraints.js +34 -0
- package/dist/graph.cjs +226 -0
- package/dist/graph.d.cts +2 -0
- package/dist/graph.d.ts +2 -0
- package/dist/graph.js +223 -0
- package/dist/index.cjs +140 -0
- package/dist/index.d.cts +33 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +70 -0
- package/dist/inertia.cjs +214 -0
- package/dist/inertia.d.cts +53 -0
- package/dist/inertia.d.ts +53 -0
- package/dist/inertia.js +212 -0
- package/dist/instant.cjs +66 -0
- package/dist/instant.d.cts +16 -0
- package/dist/instant.d.ts +16 -0
- package/dist/instant.js +63 -0
- package/dist/internal-driver.cjs +81 -0
- package/dist/internal-driver.d.cts +19 -0
- package/dist/internal-driver.d.ts +19 -0
- package/dist/internal-driver.js +35 -0
- package/dist/keyframes.cjs +191 -0
- package/dist/keyframes.d.cts +13 -0
- package/dist/keyframes.d.ts +13 -0
- package/dist/keyframes.js +188 -0
- package/dist/layout/commitDetector.cjs +67 -0
- package/dist/layout/commitDetector.d.cts +21 -0
- package/dist/layout/commitDetector.d.ts +21 -0
- package/dist/layout/commitDetector.js +64 -0
- package/dist/layout/compose.cjs +67 -0
- package/dist/layout/compose.d.cts +30 -0
- package/dist/layout/compose.d.ts +30 -0
- package/dist/layout/compose.js +65 -0
- package/dist/layout/constants.cjs +13 -0
- package/dist/layout/constants.d.cts +6 -0
- package/dist/layout/constants.d.ts +6 -0
- package/dist/layout/constants.js +10 -0
- package/dist/layout/identity.cjs +700 -0
- package/dist/layout/identity.d.cts +67 -0
- package/dist/layout/identity.d.ts +67 -0
- package/dist/layout/identity.js +698 -0
- package/dist/layout/index.cjs +36 -0
- package/dist/layout/index.d.cts +18 -0
- package/dist/layout/index.d.ts +18 -0
- package/dist/layout/index.js +13 -0
- package/dist/layout/measure.cjs +81 -0
- package/dist/layout/measure.d.cts +43 -0
- package/dist/layout/measure.d.ts +43 -0
- package/dist/layout/measure.js +78 -0
- package/dist/layout/projection.cjs +99 -0
- package/dist/layout/projection.d.cts +23 -0
- package/dist/layout/projection.d.ts +23 -0
- package/dist/layout/projection.js +97 -0
- package/dist/layout/scroll.cjs +27 -0
- package/dist/layout/scroll.d.cts +13 -0
- package/dist/layout/scroll.d.ts +13 -0
- package/dist/layout/scroll.js +24 -0
- package/dist/layout/session.cjs +207 -0
- package/dist/layout/session.d.cts +73 -0
- package/dist/layout/session.d.ts +73 -0
- package/dist/layout/session.js +205 -0
- package/dist/layout/tree.cjs +826 -0
- package/dist/layout/tree.d.cts +70 -0
- package/dist/layout/tree.d.ts +70 -0
- package/dist/layout/tree.js +823 -0
- package/dist/layout/types.cjs +36 -0
- package/dist/layout/types.d.cts +31 -0
- package/dist/layout/types.d.ts +31 -0
- package/dist/layout/types.js +35 -0
- package/dist/motion-arc.cjs +184 -0
- package/dist/motion-arc.d.cts +78 -0
- package/dist/motion-arc.d.ts +78 -0
- package/dist/motion-arc.js +183 -0
- package/dist/motion-mix.cjs +205 -0
- package/dist/motion-mix.d.cts +3 -0
- package/dist/motion-mix.d.ts +3 -0
- package/dist/motion-mix.js +202 -0
- package/dist/motion-value-driver-port.cjs +636 -0
- package/dist/motion-value-driver-port.d.cts +406 -0
- package/dist/motion-value-driver-port.d.ts +406 -0
- package/dist/motion-value-driver-port.js +627 -0
- package/dist/motion-value.cjs +189 -0
- package/dist/motion-value.d.cts +51 -0
- package/dist/motion-value.d.ts +51 -0
- package/dist/motion-value.js +185 -0
- package/dist/presence/controller.cjs +657 -0
- package/dist/presence/controller.d.cts +6 -0
- package/dist/presence/controller.d.ts +6 -0
- package/dist/presence/controller.js +652 -0
- package/dist/presence/index.cjs +19 -0
- package/dist/presence/index.d.cts +4 -0
- package/dist/presence/index.d.ts +4 -0
- package/dist/presence/index.js +12 -0
- package/dist/presence/machine.cjs +50 -0
- package/dist/presence/machine.d.cts +10 -0
- package/dist/presence/machine.d.ts +10 -0
- package/dist/presence/machine.js +46 -0
- package/dist/presence/types.cjs +6 -0
- package/dist/presence/types.d.cts +32 -0
- package/dist/presence/types.d.ts +32 -0
- package/dist/presence/types.js +5 -0
- package/dist/repeat.cjs +311 -0
- package/dist/repeat.d.cts +174 -0
- package/dist/repeat.d.ts +174 -0
- package/dist/repeat.js +300 -0
- package/dist/spring.cjs +128 -0
- package/dist/spring.d.cts +18 -0
- package/dist/spring.d.ts +18 -0
- package/dist/spring.js +125 -0
- package/dist/subscriptions.cjs +74 -0
- package/dist/subscriptions.d.cts +19 -0
- package/dist/subscriptions.d.ts +19 -0
- package/dist/subscriptions.js +69 -0
- package/dist/subset/index.cjs +23 -0
- package/dist/subset/index.d.cts +6 -0
- package/dist/subset/index.d.ts +6 -0
- package/dist/subset/index.js +13 -0
- package/dist/subset/normalize.cjs +90 -0
- package/dist/subset/normalize.d.cts +10 -0
- package/dist/subset/normalize.d.ts +10 -0
- package/dist/subset/normalize.js +85 -0
- package/dist/subset/registry.cjs +259 -0
- package/dist/subset/registry.d.cts +25 -0
- package/dist/subset/registry.d.ts +25 -0
- package/dist/subset/registry.js +256 -0
- package/dist/subset/resolve.cjs +98 -0
- package/dist/subset/resolve.d.cts +24 -0
- package/dist/subset/resolve.d.ts +24 -0
- package/dist/subset/resolve.js +90 -0
- package/dist/timing.cjs +206 -0
- package/dist/timing.d.cts +17 -0
- package/dist/timing.d.ts +17 -0
- package/dist/timing.js +201 -0
- package/dist/transformTemplate.cjs +219 -0
- package/dist/transformTemplate.d.cts +29 -0
- package/dist/transformTemplate.d.ts +29 -0
- package/dist/transformTemplate.js +215 -0
- package/dist/transition.cjs +231 -0
- package/dist/transition.d.cts +89 -0
- package/dist/transition.d.ts +89 -0
- package/dist/transition.js +223 -0
- package/dist/types.cjs +4 -0
- package/dist/types.d.cts +175 -0
- package/dist/types.d.ts +175 -0
- package/dist/types.js +3 -0
- package/dist/value-types/color.cjs +220 -0
- package/dist/value-types/color.d.cts +19 -0
- package/dist/value-types/color.d.ts +19 -0
- package/dist/value-types/color.js +218 -0
- package/dist/value-types/complex.cjs +160 -0
- package/dist/value-types/complex.d.cts +19 -0
- package/dist/value-types/complex.d.ts +19 -0
- package/dist/value-types/complex.js +155 -0
- package/dist/value-types/constants.cjs +15 -0
- package/dist/value-types/constants.d.cts +6 -0
- package/dist/value-types/constants.d.ts +6 -0
- package/dist/value-types/constants.js +12 -0
- package/dist/value-types/discrete.cjs +62 -0
- package/dist/value-types/discrete.d.cts +4 -0
- package/dist/value-types/discrete.d.ts +4 -0
- package/dist/value-types/discrete.js +56 -0
- package/dist/value-types/index.cjs +59 -0
- package/dist/value-types/index.d.cts +14 -0
- package/dist/value-types/index.d.ts +14 -0
- package/dist/value-types/index.js +24 -0
- package/dist/value-types/measure-resolve.cjs +325 -0
- package/dist/value-types/measure-resolve.d.cts +91 -0
- package/dist/value-types/measure-resolve.d.ts +91 -0
- package/dist/value-types/measure-resolve.js +313 -0
- package/dist/value-types/mix.cjs +90 -0
- package/dist/value-types/mix.d.cts +27 -0
- package/dist/value-types/mix.d.ts +27 -0
- package/dist/value-types/mix.js +85 -0
- package/dist/value-types/named-colors.cjs +61 -0
- package/dist/value-types/named-colors.d.cts +2 -0
- package/dist/value-types/named-colors.d.ts +2 -0
- package/dist/value-types/named-colors.js +58 -0
- package/dist/value-types/numeric.cjs +86 -0
- package/dist/value-types/numeric.d.cts +16 -0
- package/dist/value-types/numeric.d.ts +16 -0
- package/dist/value-types/numeric.js +79 -0
- package/dist/worklet-layout/config/constants.js +39 -0
- package/dist/worklet-layout/layout/constants.js +11 -0
- package/dist/worklet-layout/layout/identity.js +699 -0
- package/dist/worklet-layout/layout/projection.js +98 -0
- package/dist/worklet-layout/layout/session.js +206 -0
- package/dist/worklet-layout/layout/tree.js +824 -0
- package/dist/worklet-layout/layout/types.js +36 -0
- package/dist/worklet-layout/spring.js +125 -0
- package/dist/worklet-layout/timing.js +201 -0
- package/dist/worklet-layout/transition.js +223 -0
- package/dist/worklet-layout.cjs +47 -0
- package/dist/worklet-layout.d.cts +15 -0
- package/dist/worklet-layout.d.ts +15 -0
- package/dist/worklet-layout.js +28 -0
- package/dist/wrap.cjs +7 -0
- package/dist/wrap.d.cts +1 -0
- package/dist/wrap.d.ts +1 -0
- package/dist/wrap.js +4 -0
- package/package.json +45 -0
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
// @frame-path — executes inside the UI-runtime frame step; allocation-gated (REQ-DRIVER-021).
|
|
2
|
+
// Layout identity registry (specs/L2-BUILD-PACKET.md) — the ratified shared layout-identity seam
|
|
3
|
+
// (BRIEF Decisions 2026-07-06). Pairs LAYOUT geometry with PRESENCE-owned lifetimes without owning
|
|
4
|
+
// either: per `layoutId` a HOLDER STACK in mount order plus ONE shared flight (the existing
|
|
5
|
+
// projection session — reused, not reimplemented), which every holder reads RELATIVE TO ITS OWN
|
|
6
|
+
// layout rect (both elements coincide visually mid-flight; no reparenting anywhere,
|
|
7
|
+
// REQ-LAYOUT-015). Host-agnostic and deterministic under the fake clock; within-screen only
|
|
8
|
+
// (REQ-LAYOUT-003).
|
|
9
|
+
//
|
|
10
|
+
// Modeling notes:
|
|
11
|
+
// - The flight is a per-id `createProjectionSession`; holders derive their transforms from the
|
|
12
|
+
// SHARED visual rect via `invert(visual, ownLayout)` — the lead's math and the trailing's math
|
|
13
|
+
// are the same function over different layout rects, so coincidence holds by construction.
|
|
14
|
+
// - The crossfade rides ONE scalar on the [0, 100] internal progress convention (REQ-SPRING-013,
|
|
15
|
+
// the same convention L1 colors use), driven through the same graph and spring config as the
|
|
16
|
+
// rect flight: lead opacity = clamp01(s / 100), the flight's departure partner = 1 − that.
|
|
17
|
+
// Opacity CLAMPS on read (spring overshoot past 100 must not produce out-of-range opacity).
|
|
18
|
+
// - Per-id settle derives from the flight channels + scalar against the layout epsilons —
|
|
19
|
+
// `graph.isSettled()` is GLOBAL (any other animation on the shared graph would leak into it),
|
|
20
|
+
// so the registry never uses it for per-id state.
|
|
21
|
+
import { resolveSpringGenerator } from "../transition.js";
|
|
22
|
+
import { timingGenerator } from "../timing.js";
|
|
23
|
+
import { EPSILON_POS, EPSILON_VEL } from "./constants.js";
|
|
24
|
+
import { createProjectionSession } from "./session.js";
|
|
25
|
+
// Frames of exclusive-remount grace after a measured sole leave. Covers deferred host onLayout
|
|
26
|
+
// attach after React unmount/mount; far longer delays must not invent flights from forgotten rects.
|
|
27
|
+
const EXCLUSIVE_HANDOFF_GRACE_FRAMES = 8;
|
|
28
|
+
// The crossfade scalar's internal progress span (REQ-SPRING-013 convention, shared with L1 colors).
|
|
29
|
+
const SCALAR_END = 100;
|
|
30
|
+
// One edge authors at most ONE flight mode — the host resolver is single-outcome (T22 L1);
|
|
31
|
+
// both present is an authoring contradiction the registry refuses loud (severity law: the
|
|
32
|
+
// host maps this to dev-throw / prod-report at its boundary).
|
|
33
|
+
function assertSingleFlightMode(options) {
|
|
34
|
+
if (options?.timing !== undefined && options?.spring !== undefined) {
|
|
35
|
+
throw new Error('layout identity: flight options carry BOTH spring and timing — one edge executes exactly one mode; author one.');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Spring-config identity for session reuse (beside timingEqual): null session-spring pairs
|
|
39
|
+
// with an undefined request (both mean the registry default); otherwise field-wise over the
|
|
40
|
+
// engine SpringConfig surface.
|
|
41
|
+
function springEqual(a, b) {
|
|
42
|
+
if (a === null || b === undefined)
|
|
43
|
+
return a === null && b === undefined;
|
|
44
|
+
return (a.stiffness === b.stiffness &&
|
|
45
|
+
a.damping === b.damping &&
|
|
46
|
+
a.mass === b.mass &&
|
|
47
|
+
a.duration === b.duration &&
|
|
48
|
+
a.bounce === b.bounce &&
|
|
49
|
+
a.visualDuration === b.visualDuration &&
|
|
50
|
+
a.velocity === b.velocity &&
|
|
51
|
+
a.restSpeed === b.restSpeed &&
|
|
52
|
+
a.restDelta === b.restDelta);
|
|
53
|
+
}
|
|
54
|
+
function timingEqual(a, b) {
|
|
55
|
+
if (b === undefined)
|
|
56
|
+
return a === null;
|
|
57
|
+
if (a === null)
|
|
58
|
+
return false;
|
|
59
|
+
if (a.duration !== b.duration)
|
|
60
|
+
return false;
|
|
61
|
+
const ae = a.ease;
|
|
62
|
+
const be = b.ease;
|
|
63
|
+
if (ae === be)
|
|
64
|
+
return true;
|
|
65
|
+
if (ae === undefined || be === undefined)
|
|
66
|
+
return ae === be;
|
|
67
|
+
if (typeof ae === 'string' || typeof be === 'string')
|
|
68
|
+
return ae === be;
|
|
69
|
+
if (!Array.isArray(ae) || !Array.isArray(be) || ae.length !== be.length)
|
|
70
|
+
return false;
|
|
71
|
+
for (let i = 0; i < ae.length; i++) {
|
|
72
|
+
if (ae[i] !== be[i])
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
function requireFiniteRect(id, operation, rect) {
|
|
78
|
+
if (!Number.isFinite(rect.x) ||
|
|
79
|
+
!Number.isFinite(rect.y) ||
|
|
80
|
+
!Number.isFinite(rect.width) ||
|
|
81
|
+
!Number.isFinite(rect.height) ||
|
|
82
|
+
rect.width <= 0 ||
|
|
83
|
+
rect.height <= 0) {
|
|
84
|
+
throw new Error(`layout identity '${id}': ${operation} received a malformed rect ` +
|
|
85
|
+
`(${JSON.stringify(rect)}) — non-finite fields never fabricate a flight (packet §semantics 6).`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function requireRegistryRect(id, operation, rect) {
|
|
89
|
+
requireFiniteRect(id, operation, rect);
|
|
90
|
+
return rect;
|
|
91
|
+
}
|
|
92
|
+
function clamp01(v) {
|
|
93
|
+
return v < 0 ? 0 : v > 1 ? 1 : v;
|
|
94
|
+
}
|
|
95
|
+
// alloc-ok: construction — one registry per driver; the per-frame surface is readInto/holds/
|
|
96
|
+
// isSettled/releasable + the sessions' in-place reads, which allocate nothing.
|
|
97
|
+
export function createLayoutIdentityRegistry(options) {
|
|
98
|
+
'worklet';
|
|
99
|
+
if (options.graph === undefined) {
|
|
100
|
+
throw new Error('createLayoutIdentityRegistry: a MotionGraph is required.');
|
|
101
|
+
}
|
|
102
|
+
const { graph } = options;
|
|
103
|
+
// Handle provenance + resolution (round-8 major 6, round-9 major 7): the private mint map.
|
|
104
|
+
// One WeakMap.get per readInto does BOTH the membership check (by the handle object's
|
|
105
|
+
// reference identity — clones/forgeries/foreign handles miss) and the internals lookup (the
|
|
106
|
+
// data lives here, not on the handle, so mutating a minted handle redirects nothing). Weakly
|
|
107
|
+
// held — a departed holder's handle is GC-able the moment the engine drops it.
|
|
108
|
+
const handleData = new WeakMap();
|
|
109
|
+
// One spring drives the flight AND the crossfade scalar (packet §Design); the default matches
|
|
110
|
+
// the session's own LAYOUT_PROJECTION_SPRING.
|
|
111
|
+
const flightSpring = options.spring ?? { stiffness: 550, damping: 45 };
|
|
112
|
+
const entries = new Map();
|
|
113
|
+
// One-shot exclusive-remount handoffs: measured sole leave → next enter within frame grace is
|
|
114
|
+
// lead-from-snapshot. Parallel id list avoids Map iterators on the frame-path tick.
|
|
115
|
+
const exclusiveHandoffs = new Map();
|
|
116
|
+
const exclusiveHandoffFrames = new Map();
|
|
117
|
+
const exclusiveHandoffIds = [];
|
|
118
|
+
function seedExclusiveHandoff(id, rect) {
|
|
119
|
+
if (!exclusiveHandoffs.has(id))
|
|
120
|
+
exclusiveHandoffIds.push(id);
|
|
121
|
+
exclusiveHandoffs.set(id, rect);
|
|
122
|
+
exclusiveHandoffFrames.set(id, EXCLUSIVE_HANDOFF_GRACE_FRAMES);
|
|
123
|
+
}
|
|
124
|
+
function clearExclusiveHandoff(id) {
|
|
125
|
+
if (!exclusiveHandoffs.has(id))
|
|
126
|
+
return;
|
|
127
|
+
exclusiveHandoffs.delete(id);
|
|
128
|
+
exclusiveHandoffFrames.delete(id);
|
|
129
|
+
for (let i = 0; i < exclusiveHandoffIds.length; i++) {
|
|
130
|
+
if (exclusiveHandoffIds[i] === id) {
|
|
131
|
+
exclusiveHandoffIds[i] = exclusiveHandoffIds[exclusiveHandoffIds.length - 1];
|
|
132
|
+
exclusiveHandoffIds.length -= 1;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function entryOf(id, operation) {
|
|
138
|
+
const entry = entries.get(id);
|
|
139
|
+
if (entry === undefined) {
|
|
140
|
+
throw new Error(`layout identity '${id}': no holders are registered (${operation}).`);
|
|
141
|
+
}
|
|
142
|
+
return entry;
|
|
143
|
+
}
|
|
144
|
+
function holderIn(list, token) {
|
|
145
|
+
// Indexed lookup: this runs per painted holder per frame through readInto/releasable —
|
|
146
|
+
// Array.find allocates its callback closure per call (REQ-DRIVER-015, review round 6).
|
|
147
|
+
for (let i = 0; i < list.length; i++) {
|
|
148
|
+
const h = list[i];
|
|
149
|
+
if (h.token === token)
|
|
150
|
+
return h;
|
|
151
|
+
}
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
function requireHolder(entry, id, token, operation) {
|
|
155
|
+
const holder = holderIn(entry.holders, token) ?? holderIn(entry.exiting, token);
|
|
156
|
+
if (holder === undefined) {
|
|
157
|
+
throw new Error(`layout identity '${id}': '${String(token)}' does not hold this id (${operation}).`);
|
|
158
|
+
}
|
|
159
|
+
return holder;
|
|
160
|
+
}
|
|
161
|
+
function lead(entry) {
|
|
162
|
+
return entry.holders[entry.holders.length - 1];
|
|
163
|
+
}
|
|
164
|
+
// Ensure a projection session matches the requested flight mode (spring vs duration tween)
|
|
165
|
+
// AND config (per-flight spring vs registry default). Mode or config changes dispose and
|
|
166
|
+
// rebuild — never retarget across modes or spring configs (T22 L2).
|
|
167
|
+
// alloc-ok: lifecycle-edge — pairing only, never per frame.
|
|
168
|
+
function ensureSession(entry, timing, spring) {
|
|
169
|
+
if (entry.session !== null &&
|
|
170
|
+
timingEqual(entry.sessionTiming, timing) &&
|
|
171
|
+
springEqual(entry.sessionSpring, spring)) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
entry.session?.dispose();
|
|
175
|
+
if (timing !== undefined) {
|
|
176
|
+
entry.session = createProjectionSession({ graph, timing });
|
|
177
|
+
entry.sessionTiming = timing;
|
|
178
|
+
entry.sessionSpring = null;
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
entry.session = createProjectionSession({ graph, spring: spring ?? flightSpring });
|
|
182
|
+
entry.sessionTiming = null;
|
|
183
|
+
entry.sessionSpring = spring ?? null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function animateScalarToEnd(entry, timing, spring) {
|
|
187
|
+
if (entry.scalar === null)
|
|
188
|
+
return;
|
|
189
|
+
if (timing !== undefined) {
|
|
190
|
+
graph.animate(entry.scalar, timingGenerator(SCALAR_END, timing));
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
graph.animate(entry.scalar, resolveSpringGenerator(SCALAR_END, spring ?? flightSpring));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// Restart the crossfade scalar at 0 (a fresh fade for a fresh pairing; the READ mapping —
|
|
197
|
+
// who is lead, who fades — carries visual continuity across re-pairings). It ANIMATES only
|
|
198
|
+
// when the flight it rides actually exists: a pairing made before the lead measured holds at
|
|
199
|
+
// 0 (lead invisible, predecessor opaque) until the first commit begins the real flight.
|
|
200
|
+
// `instant` jumps straight to SCALAR_END (pin type:false — no multi-frame crossfade).
|
|
201
|
+
function restartScalar(entry, animate, instant = false, timing, spring) {
|
|
202
|
+
if (entry.scalar === null)
|
|
203
|
+
entry.scalar = graph.motionValue(0);
|
|
204
|
+
else
|
|
205
|
+
entry.scalar.jump(0);
|
|
206
|
+
if (instant) {
|
|
207
|
+
entry.scalar.jump(SCALAR_END);
|
|
208
|
+
entry.scalarPending = false;
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (animate) {
|
|
212
|
+
animateScalarToEnd(entry, timing, spring);
|
|
213
|
+
entry.scalarPending = false;
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
entry.scalarPending = true;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// Instant settle of rect + crossfade (REQ-API-035 layout lane under MotionConfig policy).
|
|
220
|
+
// Jump visual to target, scalar to end, release every exiting rider — no multi-frame flight.
|
|
221
|
+
// alloc-ok: lifecycle-edge — enter/commitRect/leave only; never per frame.
|
|
222
|
+
function settleInstant(entry, target, fadingFrom) {
|
|
223
|
+
// Retain the live spring config (a spring-mode session needs no rebuild just to settle);
|
|
224
|
+
// a timing session still collapses to the default spring, as before.
|
|
225
|
+
ensureSession(entry, undefined, entry.sessionSpring ?? undefined);
|
|
226
|
+
entry.session.settleAt(target);
|
|
227
|
+
entry.target = target;
|
|
228
|
+
entry.pendingBefore = null;
|
|
229
|
+
entry.fadingFrom = fadingFrom;
|
|
230
|
+
restartScalar(entry, false, true);
|
|
231
|
+
// Crossfade already complete: every exiting rider is releasable (no flight left to await).
|
|
232
|
+
// Indexed loop: for..of iterator allocation is banned on frame-path modules (REQ-DRIVER-021).
|
|
233
|
+
for (let i = 0; i < entry.exiting.length; i++) {
|
|
234
|
+
entry.exiting[i].released = true;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// Begin (or re-pair) the shared flight from `before` toward the lead's rect. Uses retarget when
|
|
238
|
+
// a measured flight is live (C0/C1 via the session's own semantics); begin otherwise.
|
|
239
|
+
// alloc-ok: lifecycle-edge — pairing/retarget resolution at enter/leave edges, never per frame.
|
|
240
|
+
function fly(entry, id, before, fadingFrom, options) {
|
|
241
|
+
const instant = options?.instant === true;
|
|
242
|
+
// Read-current tri-state on the enter/leave edges (review r2 major 1 — the commitRect
|
|
243
|
+
// 7htzcf law verbatim): options===undefined keeps the live mode (internal paths and
|
|
244
|
+
// callers that do not restate policy); an options object without timing/spring clears to
|
|
245
|
+
// the registry default; a config sets.
|
|
246
|
+
const timing = options === undefined ? (entry.sessionTiming ?? undefined) : options.timing;
|
|
247
|
+
const spring = options === undefined ? (entry.sessionSpring ?? undefined) : options.spring;
|
|
248
|
+
const target = lead(entry)?.rect ?? null;
|
|
249
|
+
if (instant && target !== null) {
|
|
250
|
+
settleInstant(entry, target, fadingFrom);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (entry.session !== null &&
|
|
254
|
+
timingEqual(entry.sessionTiming, timing) &&
|
|
255
|
+
springEqual(entry.sessionSpring, spring) &&
|
|
256
|
+
entry.session.state() === 'projecting' &&
|
|
257
|
+
entry.session.measured() &&
|
|
258
|
+
target !== null) {
|
|
259
|
+
entry.session.retarget(target);
|
|
260
|
+
}
|
|
261
|
+
else if (before !== null) {
|
|
262
|
+
ensureSession(entry, timing, spring);
|
|
263
|
+
entry.session.begin(id, before, target);
|
|
264
|
+
if (target === null)
|
|
265
|
+
entry.pendingBefore = before;
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
// No visible departure exists anywhere in the stack (nothing ever measured): the flight
|
|
269
|
+
// is impossible. Degrade to a plain crossfade at natural spots and never park a
|
|
270
|
+
// fail-closed session — an unmeasurable wait would hold the graph's settle ledger open
|
|
271
|
+
// (review cycle 4 major 9).
|
|
272
|
+
entry.session?.dispose();
|
|
273
|
+
entry.session = null;
|
|
274
|
+
entry.sessionTiming = null;
|
|
275
|
+
entry.sessionSpring = null;
|
|
276
|
+
entry.pendingBefore = null;
|
|
277
|
+
}
|
|
278
|
+
// A new pairing supersedes every exiting fade it does not itself carry: a superseded
|
|
279
|
+
// exiting holder paints 0 from now on and is owed nothing — release it rather than strand
|
|
280
|
+
// it behind the new flight's settle (review cycle 7 major 12). The incoming fadingFrom may
|
|
281
|
+
// itself be an exiting holder (the leave path) — that one still rides.
|
|
282
|
+
for (const ex of entry.exiting) {
|
|
283
|
+
if (ex.token !== fadingFrom)
|
|
284
|
+
ex.released = true;
|
|
285
|
+
}
|
|
286
|
+
entry.target = target;
|
|
287
|
+
entry.fadingFrom = fadingFrom;
|
|
288
|
+
restartScalar(entry, target !== null, false, timing, spring);
|
|
289
|
+
}
|
|
290
|
+
function flightSettled(entry) {
|
|
291
|
+
// The rect leg: no session means no rect flight is pending (never flown, a reverted
|
|
292
|
+
// pairing, or a degraded plain fade) — settled rect-wise, but NEVER settled overall while
|
|
293
|
+
// the crossfade below is still live (review cycle 5 major 10: a degraded fade is active
|
|
294
|
+
// graph work; per-id settle must not lie and read() must render the fade, not snap it).
|
|
295
|
+
// Allocation-free (REQ-DRIVER-015): the session compares its live channels in place —
|
|
296
|
+
// this runs per active id per frame, so visualRect()/velocity() record-building is banned.
|
|
297
|
+
if (entry.session !== null) {
|
|
298
|
+
if (entry.target === null)
|
|
299
|
+
return false; // unmeasured target: fail-closed, never settled
|
|
300
|
+
if (!entry.session.settledAgainst(entry.target, EPSILON_POS, EPSILON_VEL))
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
const s = entry.scalar;
|
|
304
|
+
if (s === null)
|
|
305
|
+
return true;
|
|
306
|
+
return Math.abs(s.get() - SCALAR_END) < EPSILON_POS && Math.abs(s.getVelocity()) < EPSILON_VEL;
|
|
307
|
+
}
|
|
308
|
+
// The registry's one per-frame scratch rect: readHolderInto runs per painted holder per frame,
|
|
309
|
+
// so the visual rect is written in place here instead of allocated per read (REQ-DRIVER-015).
|
|
310
|
+
const scratchVisual = { x: 0, y: 0, width: 0, height: 0 };
|
|
311
|
+
// Single source of truth for read() and readInto(): a RESOLVED entry+holder in, ONE settle
|
|
312
|
+
// evaluation (read+zLifted as separate calls evaluated flightSettled twice), written field-wise
|
|
313
|
+
// into the caller-owned record. read() resolves by token (js-thread); readInto() dereferences
|
|
314
|
+
// its enter()-minted handle O(1) (the per-frame path, round-7 major 4).
|
|
315
|
+
function writeHolderView(entry, h, out) {
|
|
316
|
+
const holder = h.token;
|
|
317
|
+
const settled = flightSettled(entry);
|
|
318
|
+
const isLead = lead(entry)?.token === holder;
|
|
319
|
+
// Opacity: the lead fades in with the scalar; the flight's departure partner fades out;
|
|
320
|
+
// everyone else (older stack members, exiting holders no longer paired) sits at 0. Exact
|
|
321
|
+
// endpoints at settle — no epsilon residue on what the substrate paints. With no scalar
|
|
322
|
+
// (no pairing ever, or a reverted one) the identity sits with the LEAD alone — two
|
|
323
|
+
// holders both painting opaque is never a valid state (review cycle 6 major 11).
|
|
324
|
+
if (entry.scalar === null)
|
|
325
|
+
out.opacity = isLead ? 1 : 0;
|
|
326
|
+
else if (isLead)
|
|
327
|
+
out.opacity = settled ? 1 : clamp01(entry.scalar.get() / SCALAR_END);
|
|
328
|
+
else if (entry.fadingFrom === holder) {
|
|
329
|
+
out.opacity = settled ? 0 : 1 - clamp01(entry.scalar.get() / SCALAR_END);
|
|
330
|
+
}
|
|
331
|
+
else
|
|
332
|
+
out.opacity = 0;
|
|
333
|
+
// Transform: the shared visual rect relative to this holder's OWN layout rect. Identity
|
|
334
|
+
// while no flight exists, the holder is unmeasured, or the flight is unmeasured
|
|
335
|
+
// (fail-closed — never a fabricated delta, REQ-LAYOUT-011). The invert is written field-wise
|
|
336
|
+
// (projection.invert allocates its Transform — banned per frame); the divisions are safe by
|
|
337
|
+
// construction: every stored holder rect passed requireRegistryRect's width/height > 0 gate.
|
|
338
|
+
if (entry.session === null ||
|
|
339
|
+
!entry.session.writeVisualInto(scratchVisual) ||
|
|
340
|
+
h.rect === null) {
|
|
341
|
+
out.translateX = 0;
|
|
342
|
+
out.translateY = 0;
|
|
343
|
+
out.scaleX = 1;
|
|
344
|
+
out.scaleY = 1;
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
out.translateX = scratchVisual.x - h.rect.x;
|
|
348
|
+
out.translateY = scratchVisual.y - h.rect.y;
|
|
349
|
+
out.scaleX = scratchVisual.width / h.rect.width;
|
|
350
|
+
out.scaleY = scratchVisual.height / h.rect.height;
|
|
351
|
+
}
|
|
352
|
+
// zLifted()'s exact definition, folded in so the hot path is one call, not three.
|
|
353
|
+
out.zLifted = isLead && !settled;
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
// alloc-ok: lifecycle-edge — holder mount (React commit); builds the entry/holder records once.
|
|
357
|
+
enter(id, holder, rect, options) {
|
|
358
|
+
assertSingleFlightMode(options);
|
|
359
|
+
const registryRect = rect === null ? null : requireRegistryRect(id, 'enter', rect);
|
|
360
|
+
let entry = entries.get(id);
|
|
361
|
+
if (entry === undefined) {
|
|
362
|
+
entry = {
|
|
363
|
+
holders: [],
|
|
364
|
+
exiting: [],
|
|
365
|
+
session: null,
|
|
366
|
+
sessionTiming: null,
|
|
367
|
+
sessionSpring: null,
|
|
368
|
+
target: null,
|
|
369
|
+
pendingBefore: null,
|
|
370
|
+
fadingFrom: null,
|
|
371
|
+
scalar: null,
|
|
372
|
+
scalarPending: false,
|
|
373
|
+
};
|
|
374
|
+
entries.set(id, entry);
|
|
375
|
+
}
|
|
376
|
+
if (holderIn(entry.holders, holder) !== undefined ||
|
|
377
|
+
holderIn(entry.exiting, holder) !== undefined) {
|
|
378
|
+
throw new Error(`layout identity '${id}': token '${String(holder)}' already holds this id (enter).`);
|
|
379
|
+
}
|
|
380
|
+
const predecessor = lead(entry);
|
|
381
|
+
const h = { token: holder, rect: registryRect };
|
|
382
|
+
entry.holders.push(h);
|
|
383
|
+
// Frozen field-less token: nothing to mutate, nothing to leak (round-9 major 7).
|
|
384
|
+
const handle = Object.freeze({
|
|
385
|
+
__identityHolderHandle: true,
|
|
386
|
+
});
|
|
387
|
+
handleData.set(handle, { id, entry, holder: h });
|
|
388
|
+
if (predecessor === undefined) {
|
|
389
|
+
// Exclusive remount handoff: measured sole leave left a one-shot departure rect.
|
|
390
|
+
const handoff = exclusiveHandoffs.get(id);
|
|
391
|
+
clearExclusiveHandoff(id);
|
|
392
|
+
if (handoff !== undefined && handoff.width > 0 && handoff.height > 0) {
|
|
393
|
+
fly(entry, id, handoff, null, options);
|
|
394
|
+
return { role: 'lead', handle };
|
|
395
|
+
}
|
|
396
|
+
return { role: 'sole', handle };
|
|
397
|
+
}
|
|
398
|
+
// Live concurrent predecessor wins over any stale exclusive handoff.
|
|
399
|
+
clearExclusiveHandoff(id);
|
|
400
|
+
// The flight departs from — and the crossfade fades out — the last VISIBLE identity, not
|
|
401
|
+
// blindly the immediate predecessor: an unmeasured pending lead was never painted and can
|
|
402
|
+
// be neither the departure rect nor the fade-out partner (review cycle 4 major 9). A live
|
|
403
|
+
// flight's visual rect wins (C0 on rapid re-pairing); otherwise the nearest MEASURED
|
|
404
|
+
// holder down the stack; an all-unmeasured stack degrades to a plain crossfade in fly().
|
|
405
|
+
const liveVisual = entry.session?.visualRect() ?? null;
|
|
406
|
+
const nearestMeasured = [...entry.holders]
|
|
407
|
+
.reverse()
|
|
408
|
+
.find((c) => c.token !== holder && c.rect !== null);
|
|
409
|
+
const before = liveVisual ?? nearestMeasured?.rect ?? null;
|
|
410
|
+
const fadingFrom = predecessor.rect !== null
|
|
411
|
+
? predecessor.token
|
|
412
|
+
: (entry.fadingFrom ?? nearestMeasured?.token ?? predecessor.token);
|
|
413
|
+
fly(entry, id, before, fadingFrom, options);
|
|
414
|
+
return { role: 'lead', handle };
|
|
415
|
+
},
|
|
416
|
+
commitRect(id, holder, rect, options) {
|
|
417
|
+
assertSingleFlightMode(options);
|
|
418
|
+
const instant = options?.instant === true;
|
|
419
|
+
const registryRect = requireRegistryRect(id, 'commitRect', rect);
|
|
420
|
+
const entry = entryOf(id, 'commitRect');
|
|
421
|
+
const h = requireHolder(entry, id, holder, 'commitRect');
|
|
422
|
+
h.rect = registryRect;
|
|
423
|
+
const currentLead = lead(entry);
|
|
424
|
+
if (currentLead?.token !== holder)
|
|
425
|
+
return;
|
|
426
|
+
// A LEAD commit (re)targets the shared flight — host layout stays authoritative
|
|
427
|
+
// (REQ-LAYOUT-002). A pairing made before the lead measured departs from the recorded
|
|
428
|
+
// predecessor rect now that a target exists. A degraded pairing (no session — nothing in
|
|
429
|
+
// the stack ever measured) skips the flight but still starts its deferred crossfade below.
|
|
430
|
+
if (instant) {
|
|
431
|
+
settleInstant(entry, registryRect, entry.fadingFrom);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
// Read-current flight mode (7htzcf): when this edge carries options, timing/spring
|
|
435
|
+
// absence means the default spring (clear prior mode/config). options===undefined keeps
|
|
436
|
+
// the live mode (internal paths that do not restate policy).
|
|
437
|
+
const priorTiming = entry.sessionTiming;
|
|
438
|
+
const priorSpring = entry.sessionSpring;
|
|
439
|
+
const flightTiming = options === undefined ? (entry.sessionTiming ?? undefined) : options.timing;
|
|
440
|
+
const flightSpringConfig = options === undefined ? (entry.sessionSpring ?? undefined) : options.spring;
|
|
441
|
+
if (entry.session === null) {
|
|
442
|
+
// fall through to the deferred-crossfade start
|
|
443
|
+
}
|
|
444
|
+
else if (entry.pendingBefore !== null) {
|
|
445
|
+
const before = entry.pendingBefore;
|
|
446
|
+
entry.pendingBefore = null;
|
|
447
|
+
ensureSession(entry, flightTiming, flightSpringConfig);
|
|
448
|
+
entry.session.begin(id, before, registryRect);
|
|
449
|
+
entry.target = registryRect;
|
|
450
|
+
}
|
|
451
|
+
else if (timingEqual(entry.sessionTiming, flightTiming) &&
|
|
452
|
+
springEqual(entry.sessionSpring, flightSpringConfig) &&
|
|
453
|
+
entry.session.state() === 'projecting' &&
|
|
454
|
+
entry.session.measured()) {
|
|
455
|
+
entry.session.retarget(registryRect);
|
|
456
|
+
entry.target = registryRect;
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
const before = entry.session.visualRect() ?? entry.target;
|
|
460
|
+
if (before === null) {
|
|
461
|
+
// No departure rect can ever exist (the predecessor never measured): the flight is
|
|
462
|
+
// impossible. CLOSE the rect leg instead of parking the session in a wait that can
|
|
463
|
+
// never resolve (review cycle 3 major 6) — the deferred crossfade below still hands
|
|
464
|
+
// the identity over as a plain fade; geometry stays fail-closed at identity.
|
|
465
|
+
entry.session.dispose();
|
|
466
|
+
entry.session = null;
|
|
467
|
+
entry.sessionTiming = null;
|
|
468
|
+
entry.sessionSpring = null;
|
|
469
|
+
entry.target = null;
|
|
470
|
+
entry.pendingBefore = null;
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
ensureSession(entry, flightTiming, flightSpringConfig);
|
|
474
|
+
entry.session.begin(id, before, registryRect);
|
|
475
|
+
entry.target = registryRect;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (entry.scalarPending && entry.scalar !== null) {
|
|
479
|
+
// The deferred crossfade starts with the REAL flight — or degrades to a plain fade when
|
|
480
|
+
// the predecessor never measured (geometry stays fail-closed, but a lead stranded
|
|
481
|
+
// invisible would be a silent severity-law violation).
|
|
482
|
+
entry.scalar.jump(0);
|
|
483
|
+
animateScalarToEnd(entry, flightTiming, flightSpringConfig);
|
|
484
|
+
entry.scalarPending = false;
|
|
485
|
+
}
|
|
486
|
+
else if (
|
|
487
|
+
// Live scalar must share read-current config with geometry (2vqwp8 5d2c7f9a8e41):
|
|
488
|
+
// session rebuild alone left a prior duration tween finishing for hundreds of frames.
|
|
489
|
+
entry.scalar !== null &&
|
|
490
|
+
options !== undefined &&
|
|
491
|
+
(!timingEqual(priorTiming, flightTiming) || !springEqual(priorSpring, flightSpringConfig))) {
|
|
492
|
+
animateScalarToEnd(entry, flightTiming, flightSpringConfig);
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
// alloc-ok: lifecycle-edge — holder unmount (React commit); exiting-fade bookkeeping.
|
|
496
|
+
leave(id, holder, options) {
|
|
497
|
+
assertSingleFlightMode(options);
|
|
498
|
+
const entry = entryOf(id, 'leave');
|
|
499
|
+
const exitingHolder = holderIn(entry.exiting, holder);
|
|
500
|
+
if (exitingHolder !== undefined) {
|
|
501
|
+
// A former lead being released (post-crossfade removal path).
|
|
502
|
+
entry.exiting.splice(entry.exiting.indexOf(exitingHolder), 1);
|
|
503
|
+
exitingHolder.departed = true; // handle validity ends with registry membership
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
const h = requireHolder(entry, id, holder, 'leave');
|
|
507
|
+
const wasLead = lead(entry)?.token === holder;
|
|
508
|
+
entry.holders.splice(entry.holders.indexOf(h), 1);
|
|
509
|
+
if (wasLead && entry.holders.length > 0) {
|
|
510
|
+
const before = entry.session?.visualRect() ?? h.rect;
|
|
511
|
+
if (before === null) {
|
|
512
|
+
h.departed = true; // dropped outright — no exit ride, handle validity ends here
|
|
513
|
+
// The leaver never became visible (unmeasured, no live flight): it drops outright —
|
|
514
|
+
// immediately releasable, no exit ride, no crossfade owed (review cycle 2 major 4).
|
|
515
|
+
// What remains is decided by what sits beneath it (review cycle 6 major 11):
|
|
516
|
+
const resumed = lead(entry);
|
|
517
|
+
const resumedSource = resumed === undefined
|
|
518
|
+
? undefined
|
|
519
|
+
: [...entry.holders]
|
|
520
|
+
.reverse()
|
|
521
|
+
.find((c) => c.token !== resumed.token && c.rect !== null);
|
|
522
|
+
if (resumed !== undefined &&
|
|
523
|
+
resumed.rect === null &&
|
|
524
|
+
(resumedSource !== undefined || entry.fadingFrom !== null)) {
|
|
525
|
+
// An older PENDING pairing survives beneath the leaver (an unmeasured resumed
|
|
526
|
+
// lead over a visible identity): RE-ESTABLISH it — the resumed lead's first
|
|
527
|
+
// commit must still run the original shared flight + crossfade, never snap in.
|
|
528
|
+
fly(entry, id, resumedSource?.rect ?? null, entry.fadingFrom ?? resumedSource?.token ?? null, options);
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
// Nothing pending underneath (measured resumed lead, or nothing visible
|
|
532
|
+
// anywhere): the entry returns to its at-rest state. dispose() releases the
|
|
533
|
+
// session's waiting-for-measurement activity — an abandoned session must not
|
|
534
|
+
// hold the graph's settle ledger open (review cycle 3 major 6).
|
|
535
|
+
entry.session?.dispose();
|
|
536
|
+
entry.session = null;
|
|
537
|
+
entry.sessionTiming = null;
|
|
538
|
+
entry.sessionSpring = null;
|
|
539
|
+
entry.target = null;
|
|
540
|
+
entry.pendingBefore = null;
|
|
541
|
+
entry.fadingFrom = null;
|
|
542
|
+
entry.scalar = null;
|
|
543
|
+
entry.scalarPending = false;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
// The lead leaving hands lead back and REVERSES the flight; the leaver rides as
|
|
548
|
+
// exiting-trailing (fade-out) until settle, then `releasable` (packet §Design) —
|
|
549
|
+
// its handle stays valid for the ride.
|
|
550
|
+
entry.exiting.push(h);
|
|
551
|
+
fly(entry, id, before, holder, options);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
// A trailing/non-lead holder (or the last holder) leaving gets no exit ride: its
|
|
556
|
+
// handle's validity ends with its registry membership. Presence-retained followers
|
|
557
|
+
// must not take this leave while the successor's forward flight is live
|
|
558
|
+
// (REQ-PRESENCE-024): beginExit skip-detaches; unmount still leaves.
|
|
559
|
+
h.departed = true;
|
|
560
|
+
// Exclusive remount: last measured holder seeds a short-lived handoff for the successor
|
|
561
|
+
// that React mounts in the same commit (catalog Shared-layout-reparenting).
|
|
562
|
+
if (entry.holders.length === 0 && entry.exiting.length === 0 && h.rect !== null) {
|
|
563
|
+
seedExclusiveHandoff(id, h.rect);
|
|
564
|
+
}
|
|
565
|
+
if (entry.fadingFrom === holder) {
|
|
566
|
+
// The fade-out partner left mid-crossfade: nothing remains to fade against — the lead
|
|
567
|
+
// paints fully opaque immediately (a half-faded lead with no visible counterpart is a
|
|
568
|
+
// continuity dip, review cycle 2 minor 5). jump() itself ends the live scalar spring
|
|
569
|
+
// (the pinned notify-then-stop contract, r17) — no pre-stop, which would reverse the
|
|
570
|
+
// cancellation/change order. The rect flight continues unaffected.
|
|
571
|
+
entry.fadingFrom = null;
|
|
572
|
+
if (entry.scalar !== null) {
|
|
573
|
+
entry.scalar.jump(SCALAR_END);
|
|
574
|
+
entry.scalarPending = false;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (entry.holders.length === 0 && entry.exiting.length === 0) {
|
|
580
|
+
// GC (packet §Design): the last holder leaving drops the id. A measured sole leave may
|
|
581
|
+
// have seeded exclusiveHandoffs above — that snapshot is independent of the entry GC.
|
|
582
|
+
// dispose() closes out the session with the entry: a fail-closed wait releases its
|
|
583
|
+
// activity, a live flight's springs stop (nothing reads them anymore) — either way the
|
|
584
|
+
// graph's settle ledger is left clean (review cycle 3 major 6).
|
|
585
|
+
entry.session?.dispose();
|
|
586
|
+
entries.delete(id);
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
// alloc-ok: js-thread — record-building read for JS-side callers; the UI frame path uses
|
|
590
|
+
// readInto's caller-owned record.
|
|
591
|
+
read(id, holder) {
|
|
592
|
+
// Delegates to the same single-source computation the hot path uses; this record-building
|
|
593
|
+
// shape stays for JS-side callers (checks, e2e) — the per-frame path is readInto().
|
|
594
|
+
const out = {
|
|
595
|
+
translateX: 0,
|
|
596
|
+
translateY: 0,
|
|
597
|
+
scaleX: 1,
|
|
598
|
+
scaleY: 1,
|
|
599
|
+
opacity: 0,
|
|
600
|
+
zLifted: false,
|
|
601
|
+
};
|
|
602
|
+
const entry = entryOf(id, 'read');
|
|
603
|
+
const h = requireHolder(entry, id, holder, 'read');
|
|
604
|
+
writeHolderView(entry, h, out);
|
|
605
|
+
return {
|
|
606
|
+
transform: {
|
|
607
|
+
translateX: out.translateX,
|
|
608
|
+
translateY: out.translateY,
|
|
609
|
+
scaleX: out.scaleX,
|
|
610
|
+
scaleY: out.scaleY,
|
|
611
|
+
},
|
|
612
|
+
opacity: out.opacity,
|
|
613
|
+
};
|
|
614
|
+
},
|
|
615
|
+
readInto(handle, out) {
|
|
616
|
+
// O(1) handle resolution (round-7 major 4) with the by-token discipline's fail-loud
|
|
617
|
+
// posture intact: ONE WeakMap.get is both the provenance check and the lookup (rounds
|
|
618
|
+
// 8–9) — forged objects, spread-clones, foreign-registry handles, and mutated real
|
|
619
|
+
// handles all miss THIS registry's private map or resolve to the untouched internals;
|
|
620
|
+
// only then is liveness checked. WeakMap.get on a primitive returns undefined, so the
|
|
621
|
+
// single miss branch covers malformed values too.
|
|
622
|
+
const hi = handleData.get(handle);
|
|
623
|
+
if (hi === undefined) {
|
|
624
|
+
throw new Error('layout identity: readInto received a value that is not an enter()-minted ' +
|
|
625
|
+
'IdentityHolderHandle of this registry (forged and cross-registry handles never dereference).');
|
|
626
|
+
}
|
|
627
|
+
if (hi.holder.departed === true) {
|
|
628
|
+
throw new Error(`layout identity '${hi.id}': this handle's holder ('${String(hi.holder.token)}') no longer holds the id (readInto).`);
|
|
629
|
+
}
|
|
630
|
+
writeHolderView(hi.entry, hi.holder, out);
|
|
631
|
+
},
|
|
632
|
+
releasable(id, holder) {
|
|
633
|
+
const entry = entries.get(id);
|
|
634
|
+
if (entry === undefined)
|
|
635
|
+
return true; // the id was dropped — nothing holds the element
|
|
636
|
+
const exitingHolder = holderIn(entry.exiting, holder);
|
|
637
|
+
if (exitingHolder !== undefined) {
|
|
638
|
+
// Sticky latch (review cycle 7 major 12): once the fade this holder rides has settled
|
|
639
|
+
// (or a new pairing superseded it in fly()), the release is permanent — a later
|
|
640
|
+
// unrelated flight must never flip it back.
|
|
641
|
+
if (exitingHolder.released === true)
|
|
642
|
+
return true;
|
|
643
|
+
if (flightSettled(entry)) {
|
|
644
|
+
exitingHolder.released = true;
|
|
645
|
+
return true;
|
|
646
|
+
}
|
|
647
|
+
return false;
|
|
648
|
+
}
|
|
649
|
+
// Still holding → not releasable; not tracked at all → the registry is done with it
|
|
650
|
+
// (e.g. a reverted pending pairing dropped the leaver without an exit ride — review
|
|
651
|
+
// cycle 2 major 4: a coupled safeToRemove must still fire for it).
|
|
652
|
+
return holderIn(entry.holders, holder) === undefined;
|
|
653
|
+
},
|
|
654
|
+
zLifted(id, holder) {
|
|
655
|
+
const entry = entryOf(id, 'zLifted');
|
|
656
|
+
requireHolder(entry, id, holder, 'zLifted');
|
|
657
|
+
return lead(entry)?.token === holder && !flightSettled(entry);
|
|
658
|
+
},
|
|
659
|
+
isSettled(id) {
|
|
660
|
+
const entry = entries.get(id);
|
|
661
|
+
if (entry === undefined)
|
|
662
|
+
return true;
|
|
663
|
+
return flightSettled(entry);
|
|
664
|
+
},
|
|
665
|
+
holds(id, holder) {
|
|
666
|
+
const entry = entries.get(id);
|
|
667
|
+
if (entry === undefined)
|
|
668
|
+
return false; // the id was dropped — nothing is tracked
|
|
669
|
+
return (holderIn(entry.holders, holder) !== undefined ||
|
|
670
|
+
holderIn(entry.exiting, holder) !== undefined);
|
|
671
|
+
},
|
|
672
|
+
isLead(id, holder) {
|
|
673
|
+
const entry = entries.get(id);
|
|
674
|
+
if (entry === undefined)
|
|
675
|
+
return false;
|
|
676
|
+
return lead(entry)?.token === holder;
|
|
677
|
+
},
|
|
678
|
+
// Once per UI frame from the layout engine step (REQ-DRIVER-015: indexed, no Map iterators).
|
|
679
|
+
tickExclusiveHandoffs() {
|
|
680
|
+
for (let i = 0; i < exclusiveHandoffIds.length;) {
|
|
681
|
+
const id = exclusiveHandoffIds[i];
|
|
682
|
+
const remaining = exclusiveHandoffFrames.get(id);
|
|
683
|
+
if (remaining === undefined || remaining <= 1) {
|
|
684
|
+
exclusiveHandoffs.delete(id);
|
|
685
|
+
exclusiveHandoffFrames.delete(id);
|
|
686
|
+
exclusiveHandoffIds[i] = exclusiveHandoffIds[exclusiveHandoffIds.length - 1];
|
|
687
|
+
exclusiveHandoffIds.length -= 1;
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
exclusiveHandoffFrames.set(id, remaining - 1);
|
|
691
|
+
i += 1;
|
|
692
|
+
}
|
|
693
|
+
},
|
|
694
|
+
hasPendingExclusiveHandoffs() {
|
|
695
|
+
return exclusiveHandoffIds.length > 0;
|
|
696
|
+
},
|
|
697
|
+
};
|
|
698
|
+
}
|