@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,824 @@
|
|
|
1
|
+
'worklet';
|
|
2
|
+
// @frame-path — executes inside the UI-runtime frame step when hosted by native; allocation-gated there.
|
|
3
|
+
// SPEC-LAYOUT §4a — a host-agnostic projection-node tree. The layoutId registry deliberately owns
|
|
4
|
+
// shared-holder roles; this tree owns component ancestry and accumulated geometry only. Every Motion
|
|
5
|
+
// component can register, but only a node with a target or an explicit projection contributes output.
|
|
6
|
+
import { IDENTITY_TRANSFORM, projectAtProgress } from "./projection.js";
|
|
7
|
+
// The stale-report ring's CONTAINED bound: the dedupe set already collapses per-id spam, so the
|
|
8
|
+
// ring only ever holds distinct-episode reports — a contained flood past the cap is itself the
|
|
9
|
+
// signal (formalized as the delivered projection-report-overflow record). Lifecycle-edge only;
|
|
10
|
+
// the per-frame fast path never touches it.
|
|
11
|
+
const MAX_STALE_REPORTS = 32;
|
|
12
|
+
// The ring's ESCALATED bound (seal r10, F1): escalations are PRESERVED, never evicted — a
|
|
13
|
+
// wrong-forest publish, malformed live input, undefined scale geometry, or the overflow record
|
|
14
|
+
// itself is a script-bug/fail-loud signal that must reach the severity channel. 256 matches the
|
|
15
|
+
// escalated dedupe window: exceeding it needs 256 DISTINCT undrained escalations in one
|
|
16
|
+
// inter-drain window (the driver drains every frame — a catastrophic storm), and past it the
|
|
17
|
+
// overflow record aggregates every dropped id with its count rather than losing one silently.
|
|
18
|
+
const MAX_STALE_ESCALATED_REPORTS = 256;
|
|
19
|
+
// The overflow record's named-id bound (seal r12, F4): the first distinct dropped ids per class
|
|
20
|
+
// are named with their counts in the record's detail; everything past this cap aggregates as an
|
|
21
|
+
// honest "+N more" count. Without it, a callback-only session (no drain) grew the loss maps
|
|
22
|
+
// with every distinct dropped id and rebuilt a string over ALL of them per drop — the
|
|
23
|
+
// reviewer's 1,200-id probe produced a 12,631-character detail. Truth stays exact: the total
|
|
24
|
+
// count is always precise; only the NAMING is capped (the detail's length stays bounded for
|
|
25
|
+
// realistic id lengths).
|
|
26
|
+
const MAX_OVERFLOW_NAMED_IDS = 32;
|
|
27
|
+
// The dedupe sets' own bound (iOS AUTO SIGABRT F2): an unbounded set grows with every distinct
|
|
28
|
+
// stale id a session ever sees AND pins each id's suppression forever — a long session both leaks
|
|
29
|
+
// and loses the signal of a genuinely new episode on an ancient id. Insertion-ordered eviction
|
|
30
|
+
// (a Set iterates in insertion order): past the cap the oldest entry drops, so an evicted id
|
|
31
|
+
// reports again while an in-window id stays deduped. No refresh-on-hit — a hot stale id must not
|
|
32
|
+
// pin itself inside the window forever. The cap applies to EACH of the two class-partitioned
|
|
33
|
+
// sets (seal r9, F2), so the total stays bounded at 2× this value.
|
|
34
|
+
const MAX_STALE_IDS = 256;
|
|
35
|
+
// The known-id ledger's bound (seal r7, F4 — see the ledger comment inside createProjectionTree).
|
|
36
|
+
// Eviction misclassifies an ancient KNOWN id toward the severe 'projection-unregistered-node'
|
|
37
|
+
// code; that direction is deliberate — a publish still landing for a component registered more
|
|
38
|
+
// than this many mounts ago means a lane is holding a long-dead id, itself the script-bug class
|
|
39
|
+
// the severe code exists to surface.
|
|
40
|
+
const MAX_KNOWN_IDS = 4096;
|
|
41
|
+
// The shared empty-drain constant: a quiet frame (the overwhelming common case) allocates nothing.
|
|
42
|
+
const EMPTY_STALE_REPORTS = Object.freeze([]);
|
|
43
|
+
// The identity output for a stale read: shared and frozen, never a per-call allocation.
|
|
44
|
+
const STALE_IDENTITY_OUTPUT = Object.freeze({
|
|
45
|
+
...IDENTITY_TRANSFORM,
|
|
46
|
+
borderRadius: null,
|
|
47
|
+
targetDeltaScaleX: 1,
|
|
48
|
+
targetDeltaScaleY: 1,
|
|
49
|
+
treeScaleX: 1,
|
|
50
|
+
treeScaleY: 1,
|
|
51
|
+
});
|
|
52
|
+
// The per-frame path writes into the node's persistent cell; allocating a fresh radius pair per
|
|
53
|
+
// frame is exactly what REQ-DRIVER-021 forbids.
|
|
54
|
+
// Pure correction math (REQ-LAYOUT-013: scale × corrected == radius). Callers gate scale
|
|
55
|
+
// validity at the boundary — project()'s report-and-skip above — so this module never throws on
|
|
56
|
+
// a UI-executed path: an operating error is reported at the call boundary (seal r2, F3).
|
|
57
|
+
function writeCorrectedRadius(cell, radius, scaleX, scaleY) {
|
|
58
|
+
cell.x = radius / scaleX;
|
|
59
|
+
cell.y = radius / scaleY;
|
|
60
|
+
}
|
|
61
|
+
// Finite-fields probe for setProjection's pre-mutation validation (seal r11, F2a): position may
|
|
62
|
+
// be any finite number; the dimensions carry their own sign rules at the call site (target must
|
|
63
|
+
// be positive — the divisor inside invert; source may be zero — a collapsed host).
|
|
64
|
+
function isFiniteRectGeometry(rect) {
|
|
65
|
+
return (Number.isFinite(rect.x) &&
|
|
66
|
+
Number.isFinite(rect.y) &&
|
|
67
|
+
Number.isFinite(rect.width) &&
|
|
68
|
+
Number.isFinite(rect.height));
|
|
69
|
+
}
|
|
70
|
+
// The internal-invariant refusals that STAY loud (registration-time, never a UI frame path):
|
|
71
|
+
// a parent-change after registration and a path cycle (refreshPathsAndStableSortAtRegistration).
|
|
72
|
+
// Every frame/update-path refusal converted to report+skip in Phase 1.5 (iOS AUTO SIGABRT campaign).
|
|
73
|
+
// The severity-class partition behind the dedupe law (seal r9, F2) and the ring's delivery
|
|
74
|
+
// reserve (seal r9, F3): escalated codes name script bugs on live nodes — a wrong-forest or
|
|
75
|
+
// mistyped publish, malformed live input, undefined scale geometry, and the ring-overflow
|
|
76
|
+
// record itself; contained codes name routine teardown races and operating conditions.
|
|
77
|
+
function isEscalatedProjectionStaleCode(code) {
|
|
78
|
+
return (code === 'projection-unregistered-node' ||
|
|
79
|
+
code === 'projection-invalid-frame-input' ||
|
|
80
|
+
code === 'projection-non-finite-output' ||
|
|
81
|
+
code === 'projection-report-overflow');
|
|
82
|
+
}
|
|
83
|
+
function resetOutput(node) {
|
|
84
|
+
node.output.translateX = 0;
|
|
85
|
+
node.output.translateY = 0;
|
|
86
|
+
node.output.scaleX = 1;
|
|
87
|
+
node.output.scaleY = 1;
|
|
88
|
+
node.output.borderRadius = null;
|
|
89
|
+
node.output.targetDeltaScaleX = 1;
|
|
90
|
+
node.output.targetDeltaScaleY = 1;
|
|
91
|
+
node.output.treeScaleX = 1;
|
|
92
|
+
node.output.treeScaleY = 1;
|
|
93
|
+
}
|
|
94
|
+
// alloc-ok: lifecycle-edge — runs only inside register(); the path array is built once per node.
|
|
95
|
+
function updateDepth(node, parent) {
|
|
96
|
+
node.depth = parent === undefined ? 0 : parent.depth + 1;
|
|
97
|
+
node.path = parent === undefined ? [node.componentId] : [...parent.path, node.componentId];
|
|
98
|
+
}
|
|
99
|
+
// Hoisted module-level comparator: the registration-edge stable sort allocates no closure.
|
|
100
|
+
function compareByDepth(left, right) {
|
|
101
|
+
return left.depth - right.depth;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Creates a stable, depth-sorted component tree. `project()` deliberately walks parents before
|
|
105
|
+
* children: a non-projecting intermediate has identity output but preserves its ancestor's scale
|
|
106
|
+
* in `treeScale`, which is precisely the muggle-through capability required by REQ-LAYOUT-012.
|
|
107
|
+
*
|
|
108
|
+
* iOS AUTO SIGABRT campaign: every frame/update path is NON-FATAL on a stale id, an orphaned
|
|
109
|
+
* path, or torn-down frame input — expected teardown races report once per id per episode into a
|
|
110
|
+
* bounded ring (drained by the host's severity channel); they never throw on the UI runtime.
|
|
111
|
+
*/
|
|
112
|
+
// alloc-ok: construction — one tree per driver; the per-frame surface is project(), which
|
|
113
|
+
// mutates persistent node cells only.
|
|
114
|
+
export function createProjectionTree(options) {
|
|
115
|
+
// Config-time invariant, loud JS-side like the register/sort-at-registration asserts below
|
|
116
|
+
// (seal r8, F2): the delivery surface is MANDATORY — a bare tree's escalated reports would
|
|
117
|
+
// accumulate silently. Untyped JS callers get the same law at runtime.
|
|
118
|
+
const opts = options;
|
|
119
|
+
const onStaleProjectionNode = opts !== undefined && 'onStaleProjectionNode' in opts ? opts.onStaleProjectionNode : undefined;
|
|
120
|
+
const drainDeclared = opts !== undefined && 'delivery' in opts && opts.delivery === 'drain';
|
|
121
|
+
if (onStaleProjectionNode === undefined && !drainDeclared) {
|
|
122
|
+
throw new Error('projection tree: createProjectionTree requires a stale-report delivery surface — install ' +
|
|
123
|
+
"onStaleProjectionNode (JS hosts) or pass { delivery: 'drain' } (the UI driver's " +
|
|
124
|
+
'frame-edge drain); a bare tree silently accumulates reports in an undrained ring ' +
|
|
125
|
+
'(seal r8, F2).');
|
|
126
|
+
}
|
|
127
|
+
const nodes = new Map();
|
|
128
|
+
// Depth-sorted once per register() by refreshPathsAndStableSortAtRegistration (packet §6
|
|
129
|
+
// projection repair): project() reads this list with NO orderDirty/reorder/sort edge in-frame.
|
|
130
|
+
// unregister() removes by index (order-preserving), so the invariant holds across teardown too.
|
|
131
|
+
const ordered = [];
|
|
132
|
+
// The bounded stale-report ring and its per-episode dedupe sets — partitioned by escalation
|
|
133
|
+
// class (seal r9, F2): a CONTAINED report must never suppress a later ESCALATED report for
|
|
134
|
+
// the same id (the severity channel would lose the deferred dev throw / production error),
|
|
135
|
+
// while same-class repeats still dedupe once per episode. Re-registration clears the id from
|
|
136
|
+
// BOTH sets, so a genuinely new teardown of the same id reports again.
|
|
137
|
+
const staleReports = [];
|
|
138
|
+
// Ring accounting (seal r10, F1): contained and escalated entries carry SEPARATE bounds — an
|
|
139
|
+
// escalation never competes with contained reports for ring space. A dropped report (past its
|
|
140
|
+
// class cap) is aggregated per id into the overflow record's detail, which names every dropped
|
|
141
|
+
// id and count; the whole window's accounting resets on drain.
|
|
142
|
+
let containedInRing = 0;
|
|
143
|
+
let escalatedInRing = 0;
|
|
144
|
+
let overflowPending = false;
|
|
145
|
+
let overflowRecordIndex = -1;
|
|
146
|
+
const droppedContainedCounts = new Map();
|
|
147
|
+
const droppedEscalatedCounts = new Map();
|
|
148
|
+
// The counts-only remainder past MAX_OVERFLOW_NAMED_IDS (seal r12, F4): drops of ids beyond
|
|
149
|
+
// the named set aggregate here, so the loss accounting AND the rebuilt detail stay bounded in
|
|
150
|
+
// a drain-free session.
|
|
151
|
+
let extraContainedDrops = 0;
|
|
152
|
+
let extraEscalatedDrops = 0;
|
|
153
|
+
const reportedContainedIds = new Set();
|
|
154
|
+
const reportedEscalatedIds = new Set();
|
|
155
|
+
// The known-id ledger behind the stale-vs-never-registered distinction (seal r7, F4): it only
|
|
156
|
+
// GROWS here at register() — an id that registered and later tore down stays known, so its
|
|
157
|
+
// teardown race keeps the contained 'projection-stale-node' code, while a never-registered id
|
|
158
|
+
// escalates to 'projection-unregistered-node'. Lifecycle-edge only; the frame path reads it.
|
|
159
|
+
const knownIds = new Set();
|
|
160
|
+
// alloc-ok: lifecycle-edge — the first stale hit per (class, id) per episode allocates the
|
|
161
|
+
// report record, the detail string (built ONLY here, after the dedupe gate), and the dedupe
|
|
162
|
+
// entry; the deduped fast path (every later frame) allocates nothing.
|
|
163
|
+
function reportStaleNode(code, componentId, subject, reason) {
|
|
164
|
+
const dedupeSet = isEscalatedProjectionStaleCode(code)
|
|
165
|
+
? reportedEscalatedIds
|
|
166
|
+
: reportedContainedIds;
|
|
167
|
+
if (dedupeSet.has(componentId))
|
|
168
|
+
return;
|
|
169
|
+
dedupeSet.add(componentId);
|
|
170
|
+
if (dedupeSet.size > MAX_STALE_IDS) {
|
|
171
|
+
// The size check makes the first value blatantly present; iteration is insertion-ordered,
|
|
172
|
+
// so this evicts the oldest episode (see MAX_STALE_IDS above).
|
|
173
|
+
dedupeSet.delete(dedupeSet.values().next().value);
|
|
174
|
+
}
|
|
175
|
+
const detail = code === 'projection-orphaned-path'
|
|
176
|
+
? `'${componentId}' orphaned: parent '${subject}' ${reason}.`
|
|
177
|
+
: code === 'projection-invalid-radius-correction'
|
|
178
|
+
? `borderRadius correction skipped on '${componentId}' (${reason}; the last valid radius is preserved).`
|
|
179
|
+
: code === 'projection-invalid-frame-input'
|
|
180
|
+
? `${subject} ${reason} on '${componentId}' (malformed input on a live node; update refused).`
|
|
181
|
+
: code === 'projection-unregistered-node'
|
|
182
|
+
? `${subject} refused on '${componentId}': the id was never registered in this projection tree (a wrong-forest or mistyped publish — a script bug, not a teardown race).`
|
|
183
|
+
: code === 'projection-non-finite-output'
|
|
184
|
+
? `${subject} ${reason} on '${componentId}' (undefined geometry on a live node; output frozen at the last valid finite state).`
|
|
185
|
+
: `${subject} ${reason} (teardown race — the host detached before this update).`;
|
|
186
|
+
const report = { source: 'projection-tree', code, detail };
|
|
187
|
+
pushStaleReport(report, isEscalatedProjectionStaleCode(code), componentId);
|
|
188
|
+
onStaleProjectionNode?.(report);
|
|
189
|
+
}
|
|
190
|
+
// The ring's fail-loud law (seal r9, F3; seal r10, F1): each class keeps its own bound — an
|
|
191
|
+
// ESCALATED report never competes with contained entries and is PRESERVED up to
|
|
192
|
+
// MAX_STALE_ESCALATED_REPORTS (nothing evicts an escalation to make room). A report dropped
|
|
193
|
+
// past its class cap is never silent: exactly one projection-report-overflow record per
|
|
194
|
+
// inter-drain window marks the loss, and its detail aggregates EVERY dropped id with its
|
|
195
|
+
// count, per class — the text always matches what was actually lost.
|
|
196
|
+
// alloc-ok: lifecycle-edge — drops happen only past a class cap (a teardown storm); the
|
|
197
|
+
// deduped fast path (a quiet frame) allocates nothing.
|
|
198
|
+
function pushStaleReport(report, escalated, componentId) {
|
|
199
|
+
if (escalated) {
|
|
200
|
+
if (escalatedInRing < MAX_STALE_ESCALATED_REPORTS) {
|
|
201
|
+
escalatedInRing += 1;
|
|
202
|
+
staleReports.push(report);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
recordDroppedReport(componentId, true);
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (containedInRing < MAX_STALE_REPORTS) {
|
|
210
|
+
containedInRing += 1;
|
|
211
|
+
staleReports.push(report);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
recordDroppedReport(componentId, false);
|
|
215
|
+
}
|
|
216
|
+
// alloc-ok: lifecycle-edge — one bounded map write per dropped report (already past a class
|
|
217
|
+
// cap); past the named-id bound a counter increments instead (seal r12, F4).
|
|
218
|
+
function recordDroppedReport(componentId, escalated) {
|
|
219
|
+
const counts = escalated ? droppedEscalatedCounts : droppedContainedCounts;
|
|
220
|
+
const existing = counts.get(componentId);
|
|
221
|
+
if (existing !== undefined) {
|
|
222
|
+
counts.set(componentId, existing + 1);
|
|
223
|
+
}
|
|
224
|
+
else if (counts.size < MAX_OVERFLOW_NAMED_IDS) {
|
|
225
|
+
counts.set(componentId, 1);
|
|
226
|
+
}
|
|
227
|
+
else if (escalated) {
|
|
228
|
+
extraEscalatedDrops += 1;
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
extraContainedDrops += 1;
|
|
232
|
+
}
|
|
233
|
+
markReportOverflow();
|
|
234
|
+
}
|
|
235
|
+
// alloc-ok: lifecycle-edge — detail text built only on an overflow (re)mark, over at most
|
|
236
|
+
// MAX_OVERFLOW_NAMED_IDS named entries plus the counts-only remainder (bounded, seal r12, F4).
|
|
237
|
+
function formatDroppedCounts(counts, extra) {
|
|
238
|
+
let total = extra;
|
|
239
|
+
let list = '';
|
|
240
|
+
for (const [id, count] of counts) {
|
|
241
|
+
total += count;
|
|
242
|
+
list += `${list === '' ? '' : ', '}${id}×${count}`;
|
|
243
|
+
}
|
|
244
|
+
if (extra > 0)
|
|
245
|
+
list += `${list === '' ? '' : ', '}+${extra} more`;
|
|
246
|
+
return `${total} [${list}]`;
|
|
247
|
+
}
|
|
248
|
+
// alloc-ok: lifecycle-edge — one record per inter-drain window (overflowPending dedupes);
|
|
249
|
+
// later drops in the same window REPLACE the in-ring record's detail so the drained record
|
|
250
|
+
// names every dropped id and count.
|
|
251
|
+
function markReportOverflow() {
|
|
252
|
+
const lossParts = [];
|
|
253
|
+
if (droppedContainedCounts.size > 0 || extraContainedDrops > 0)
|
|
254
|
+
lossParts.push(`dropped ${formatDroppedCounts(droppedContainedCounts, extraContainedDrops)} contained report(s)`);
|
|
255
|
+
if (droppedEscalatedCounts.size > 0 || extraEscalatedDrops > 0)
|
|
256
|
+
lossParts.push(`dropped ${formatDroppedCounts(droppedEscalatedCounts, extraEscalatedDrops)} escalated report(s)`);
|
|
257
|
+
if (droppedContainedCounts.size === 0 && extraContainedDrops === 0)
|
|
258
|
+
lossParts.push('no contained reports were dropped');
|
|
259
|
+
if (droppedEscalatedCounts.size === 0 && extraEscalatedDrops === 0)
|
|
260
|
+
lossParts.push('every escalated report was delivered');
|
|
261
|
+
const detail = `projection stale-report ring overflowed: ${lossParts.join('; ')} ` +
|
|
262
|
+
`(caps: ${MAX_STALE_REPORTS} contained, ${MAX_STALE_ESCALATED_REPORTS} escalated; seal r10, F1).`;
|
|
263
|
+
if (overflowPending) {
|
|
264
|
+
staleReports[overflowRecordIndex] = {
|
|
265
|
+
source: 'projection-tree',
|
|
266
|
+
code: 'projection-report-overflow',
|
|
267
|
+
detail,
|
|
268
|
+
};
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
overflowPending = true;
|
|
272
|
+
overflowRecordIndex = staleReports.length;
|
|
273
|
+
const report = {
|
|
274
|
+
source: 'projection-tree',
|
|
275
|
+
code: 'projection-report-overflow',
|
|
276
|
+
detail,
|
|
277
|
+
};
|
|
278
|
+
staleReports.push(report);
|
|
279
|
+
onStaleProjectionNode?.(report);
|
|
280
|
+
}
|
|
281
|
+
// The idempotent no-op path of every stale mutator/reader below: static subject/reason parts
|
|
282
|
+
// only — nothing is built at the call site (REQ-DRIVER-021). Seal r7, F4: the code
|
|
283
|
+
// branches on the known-id ledger — a never-registered id is a wrong-forest/mistyped publish
|
|
284
|
+
// (escalated), a known-then-torn-down id is the contained Phase-1.5 teardown race.
|
|
285
|
+
function staleNode(componentId, operation) {
|
|
286
|
+
if (knownIds.has(componentId)) {
|
|
287
|
+
reportStaleNode('projection-stale-node', componentId, operation, 'on an unregistered id');
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
reportStaleNode('projection-unregistered-node', componentId, operation, '');
|
|
291
|
+
}
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
294
|
+
// alloc-ok: lifecycle-edge — React mounts passive effects child-first. Resolve the complete
|
|
295
|
+
// component forest after each attach so a descendant may enter before its parent without
|
|
296
|
+
// weakening the frame-path's parent-before-child invariant, then perform the stable depth sort
|
|
297
|
+
// SYNCHRONOUSLY here (packet §6 registration-edge repair): project() never sorts in-frame.
|
|
298
|
+
// A stable sort retains sibling mount order, which keeps the traversal deterministic without
|
|
299
|
+
// encoding host tags.
|
|
300
|
+
function refreshPathsAndStableSortAtRegistration() {
|
|
301
|
+
const resolving = new Set();
|
|
302
|
+
const resolved = new Set();
|
|
303
|
+
function resolve(node) {
|
|
304
|
+
if (resolved.has(node.componentId))
|
|
305
|
+
return;
|
|
306
|
+
if (resolving.has(node.componentId)) {
|
|
307
|
+
throw new Error(`projection tree: parent cycle reaches '${node.componentId}'; a Motion component cannot be its own projection ancestor.`);
|
|
308
|
+
}
|
|
309
|
+
resolving.add(node.componentId);
|
|
310
|
+
const parent = node.parentId === null ? undefined : nodes.get(node.parentId);
|
|
311
|
+
if (parent !== undefined)
|
|
312
|
+
resolve(parent);
|
|
313
|
+
updateDepth(node, parent);
|
|
314
|
+
resolving.delete(node.componentId);
|
|
315
|
+
resolved.add(node.componentId);
|
|
316
|
+
}
|
|
317
|
+
for (const node of nodes.values())
|
|
318
|
+
resolve(node);
|
|
319
|
+
ordered.sort(compareByDepth);
|
|
320
|
+
}
|
|
321
|
+
// alloc-ok: lifecycle-edge — runs only inside unregister()/prune teardown turns.
|
|
322
|
+
function hasRegisteredDescendant(componentId) {
|
|
323
|
+
for (const candidate of nodes.values()) {
|
|
324
|
+
if (candidate.parentId === componentId)
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
// alloc-ok: lifecycle-edge — runs only inside unregister()'s teardown turn.
|
|
330
|
+
function removeNode(node) {
|
|
331
|
+
nodes.delete(node.componentId);
|
|
332
|
+
const index = ordered.indexOf(node);
|
|
333
|
+
if (index >= 0)
|
|
334
|
+
ordered.splice(index, 1);
|
|
335
|
+
}
|
|
336
|
+
// Passive-effect cleanup is parent-first. A parent is therefore retained as an inert ancestry
|
|
337
|
+
// record until its descendants have detached, then compacted in the same lifecycle turn.
|
|
338
|
+
// alloc-ok: lifecycle-edge — runs only inside unregister()'s teardown turn.
|
|
339
|
+
function prunePendingUnregisters() {
|
|
340
|
+
let pruned = true;
|
|
341
|
+
while (pruned) {
|
|
342
|
+
pruned = false;
|
|
343
|
+
for (const node of nodes.values()) {
|
|
344
|
+
if (node.pendingUnregister && !hasRegisteredDescendant(node.componentId)) {
|
|
345
|
+
removeNode(node);
|
|
346
|
+
pruned = true;
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
// alloc-ok: lifecycle-edge — host mount; builds the node record once, never per frame.
|
|
354
|
+
register(componentId, parentId, projectionParticipant = false) {
|
|
355
|
+
// A re-registered id opens a NEW episode in BOTH dedupe classes: its next teardown race
|
|
356
|
+
// (contained) and its next script-bug report (escalated) both fire again (Phase 1.5; r9 F2).
|
|
357
|
+
reportedContainedIds.delete(componentId);
|
|
358
|
+
reportedEscalatedIds.delete(componentId);
|
|
359
|
+
// The known-id ledger (seal r7, F4) only grows: once registered, an id's later teardown
|
|
360
|
+
// race stays the contained 'projection-stale-node' class forever (bounded above).
|
|
361
|
+
if (!knownIds.has(componentId)) {
|
|
362
|
+
knownIds.add(componentId);
|
|
363
|
+
if (knownIds.size > MAX_KNOWN_IDS)
|
|
364
|
+
knownIds.delete(knownIds.values().next().value);
|
|
365
|
+
}
|
|
366
|
+
const existing = nodes.get(componentId);
|
|
367
|
+
if (existing !== undefined) {
|
|
368
|
+
if (existing.parentId !== parentId) {
|
|
369
|
+
throw new Error(`projection tree: '${componentId}' changed parent from '${existing.parentId}' to '${parentId}' after registration; remount is required to preserve a coherent projection path.`);
|
|
370
|
+
}
|
|
371
|
+
// A re-registered node is ALIVE (seal r14, F1): the pending teardown belonged to its
|
|
372
|
+
// previous life — an unregister retained it as inert ancestry only because a descendant
|
|
373
|
+
// still lived, and this re-registration is the host returning. Leaving the flag set
|
|
374
|
+
// would let the next prune (when the last descendant later detaches) remove the LIVE
|
|
375
|
+
// node: its next update would then misreport a stale teardown and freeze at identity.
|
|
376
|
+
// Its later teardown is evaluated fresh by the next unregister.
|
|
377
|
+
existing.pendingUnregister = false;
|
|
378
|
+
if (existing.projectionParticipant !== projectionParticipant) {
|
|
379
|
+
existing.projectionParticipant = projectionParticipant;
|
|
380
|
+
// `layout`/active `layoutId` can be removed on a later render. Retaining either leg
|
|
381
|
+
// would keep a stale transform painted after the host has returned to ordinary layout.
|
|
382
|
+
if (!projectionParticipant) {
|
|
383
|
+
existing.target = null;
|
|
384
|
+
existing.delta = null;
|
|
385
|
+
existing.hasHostFrame = false;
|
|
386
|
+
existing.projectionPaintedByHost = false;
|
|
387
|
+
existing.treeScaleX = 1;
|
|
388
|
+
existing.treeScaleY = 1;
|
|
389
|
+
existing.treeTranslateX = 0;
|
|
390
|
+
existing.treeTranslateY = 0;
|
|
391
|
+
existing.frameTreeCenterOffsetX = 0;
|
|
392
|
+
existing.frameTreeCenterOffsetY = 0;
|
|
393
|
+
existing.committedInHostFrame = false;
|
|
394
|
+
resetOutput(existing);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return; // Same-state React rerender: never reset a live projection.
|
|
398
|
+
}
|
|
399
|
+
const node = {
|
|
400
|
+
componentId,
|
|
401
|
+
parentId,
|
|
402
|
+
depth: 0,
|
|
403
|
+
path: [],
|
|
404
|
+
target: null,
|
|
405
|
+
targetCell: { x: 0, y: 0, width: 0, height: 0 },
|
|
406
|
+
layoutCell: { x: 0, y: 0, width: 0, height: 0 },
|
|
407
|
+
hasHostFrame: false,
|
|
408
|
+
projectionParticipant,
|
|
409
|
+
projectionPaintedByHost: false,
|
|
410
|
+
pendingUnregister: false,
|
|
411
|
+
delta: null,
|
|
412
|
+
deltaCell: { ...IDENTITY_TRANSFORM },
|
|
413
|
+
borderRadius: null,
|
|
414
|
+
// alloc-ok: lifecycle-edge — one persistent radius cell per registered node, mutated per frame.
|
|
415
|
+
radiusCell: { x: 0, y: 0 },
|
|
416
|
+
radiusCorrected: false,
|
|
417
|
+
treeScaleX: 1,
|
|
418
|
+
treeScaleY: 1,
|
|
419
|
+
treeTranslateX: 0,
|
|
420
|
+
treeTranslateY: 0,
|
|
421
|
+
frameTreeCenterOffsetX: 0,
|
|
422
|
+
frameTreeCenterOffsetY: 0,
|
|
423
|
+
committedInHostFrame: false,
|
|
424
|
+
output: {
|
|
425
|
+
...IDENTITY_TRANSFORM,
|
|
426
|
+
borderRadius: null,
|
|
427
|
+
targetDeltaScaleX: 1,
|
|
428
|
+
targetDeltaScaleY: 1,
|
|
429
|
+
treeScaleX: 1,
|
|
430
|
+
treeScaleY: 1,
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
nodes.set(componentId, node);
|
|
434
|
+
ordered.push(node);
|
|
435
|
+
refreshPathsAndStableSortAtRegistration();
|
|
436
|
+
},
|
|
437
|
+
// alloc-ok: lifecycle-edge — host unmount (React passive cleanup is parent-first).
|
|
438
|
+
unregister(componentId) {
|
|
439
|
+
const node = nodes.get(componentId);
|
|
440
|
+
// Double-teardown is a routine race (parent-first cleanup can detach a node twice across
|
|
441
|
+
// overlapping lanes): an unknown id is an idempotent no-op, never a throw (Phase 1.5).
|
|
442
|
+
if (node === undefined)
|
|
443
|
+
return;
|
|
444
|
+
if (hasRegisteredDescendant(componentId)) {
|
|
445
|
+
node.pendingUnregister = true;
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
removeNode(node);
|
|
449
|
+
prunePendingUnregisters();
|
|
450
|
+
},
|
|
451
|
+
setTarget(componentId, target) {
|
|
452
|
+
const node = nodes.get(componentId) ?? staleNode(componentId, 'setTarget');
|
|
453
|
+
if (node === undefined)
|
|
454
|
+
return;
|
|
455
|
+
node.target = target;
|
|
456
|
+
if (target !== null)
|
|
457
|
+
node.projectionParticipant = true;
|
|
458
|
+
},
|
|
459
|
+
setProjection(componentId, source, target, progress) {
|
|
460
|
+
const node = nodes.get(componentId) ?? staleNode(componentId, 'setProjection');
|
|
461
|
+
if (node === undefined)
|
|
462
|
+
return;
|
|
463
|
+
if (!Number.isFinite(progress) || progress < 0 || progress > 1) {
|
|
464
|
+
reportStaleNode('projection-invalid-frame-input', componentId, 'setProjection', 'progress outside [0, 1]');
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
// Seal r11, F2a: validate BOTH rects BEFORE any mutation. A zero/negative/non-finite
|
|
468
|
+
// TARGET dimension divides by zero inside projectAtProgress's invert (its assertMeasurable
|
|
469
|
+
// throws — a UI throw through an imported callee the tree scan could not see, the review's
|
|
470
|
+
// probe), and non-finite or negative SOURCE fields poison the delta. Refuse through the
|
|
471
|
+
// escalated malformed-live-input channel: no mutation, no throw, the node keeps its last
|
|
472
|
+
// valid projection state. A zero SOURCE dimension stays legal (a collapsed host is a
|
|
473
|
+
// legal measurement — the finite-zero delta publishes, seal r9, F1).
|
|
474
|
+
if (!isFiniteRectGeometry(source) ||
|
|
475
|
+
!isFiniteRectGeometry(target) ||
|
|
476
|
+
source.width < 0 ||
|
|
477
|
+
source.height < 0 ||
|
|
478
|
+
target.width <= 0 ||
|
|
479
|
+
target.height <= 0) {
|
|
480
|
+
reportStaleNode('projection-invalid-frame-input', componentId, 'setProjection', 'non-finite rect fields or non-positive target dimensions');
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
node.target = target;
|
|
484
|
+
node.projectionParticipant = true;
|
|
485
|
+
node.hasHostFrame = false;
|
|
486
|
+
node.projectionPaintedByHost = false;
|
|
487
|
+
node.delta = projectAtProgress(source, target, progress);
|
|
488
|
+
},
|
|
489
|
+
setDirectProjection(componentId, translateX, translateY, scaleX, scaleY) {
|
|
490
|
+
const node = nodes.get(componentId) ?? staleNode(componentId, 'setDirectProjection');
|
|
491
|
+
if (node === undefined)
|
|
492
|
+
return;
|
|
493
|
+
if (!Number.isFinite(translateX) ||
|
|
494
|
+
!Number.isFinite(translateY) ||
|
|
495
|
+
!Number.isFinite(scaleX) ||
|
|
496
|
+
!Number.isFinite(scaleY) ||
|
|
497
|
+
scaleX <= 0 ||
|
|
498
|
+
scaleY <= 0) {
|
|
499
|
+
reportStaleNode('projection-invalid-frame-input', componentId, 'setDirectProjection', 'non-finite/non-positive translation or scale');
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
node.projectionParticipant = true;
|
|
503
|
+
node.hasHostFrame = false;
|
|
504
|
+
node.projectionPaintedByHost = true;
|
|
505
|
+
node.deltaCell.translateX = translateX;
|
|
506
|
+
node.deltaCell.translateY = translateY;
|
|
507
|
+
node.deltaCell.scaleX = scaleX;
|
|
508
|
+
node.deltaCell.scaleY = scaleY;
|
|
509
|
+
node.delta = node.deltaCell;
|
|
510
|
+
},
|
|
511
|
+
// @frame-path — host layout animations report their current rect on every UI frame. Mutate
|
|
512
|
+
// persistent cells rather than constructing Rect/Transform records: the worklet frame path
|
|
513
|
+
// is allocation-gated, while targetless descendants consume this exact delta in the same step.
|
|
514
|
+
setHostFrame(componentId, currentX, currentY, currentWidth, currentHeight, targetX, targetY, targetWidth, targetHeight) {
|
|
515
|
+
const node = nodes.get(componentId) ?? staleNode(componentId, 'setHostFrame');
|
|
516
|
+
if (node === undefined)
|
|
517
|
+
return;
|
|
518
|
+
// Negative dimensions are nonsense geometry, not a collapsed host (seal r12, F3): they
|
|
519
|
+
// used to sail through and publish NEGATIVE scales into the descendant projection math
|
|
520
|
+
// (the reviewer's scaleX: -1 probe). Current dims may be zero (legal collapse — the
|
|
521
|
+
// whole-node freeze owns that path, seal r9 F1); target dims divide, so they must be
|
|
522
|
+
// positive (seal r11, F2a's law). Refuse BEFORE any mutation.
|
|
523
|
+
if (!Number.isFinite(currentX) ||
|
|
524
|
+
!Number.isFinite(currentY) ||
|
|
525
|
+
!Number.isFinite(currentWidth) ||
|
|
526
|
+
!Number.isFinite(currentHeight) ||
|
|
527
|
+
!Number.isFinite(targetX) ||
|
|
528
|
+
!Number.isFinite(targetY) ||
|
|
529
|
+
!Number.isFinite(targetWidth) ||
|
|
530
|
+
!Number.isFinite(targetHeight) ||
|
|
531
|
+
currentWidth < 0 ||
|
|
532
|
+
currentHeight < 0 ||
|
|
533
|
+
targetWidth <= 0 ||
|
|
534
|
+
targetHeight <= 0) {
|
|
535
|
+
reportStaleNode('projection-invalid-frame-input', componentId, 'setHostFrame', 'non-finite rect, negative current dimension, or non-positive target dimension');
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
node.layoutCell.x = currentX;
|
|
539
|
+
node.layoutCell.y = currentY;
|
|
540
|
+
node.layoutCell.width = currentWidth;
|
|
541
|
+
node.layoutCell.height = currentHeight;
|
|
542
|
+
node.targetCell.x = targetX;
|
|
543
|
+
node.targetCell.y = targetY;
|
|
544
|
+
node.targetCell.width = targetWidth;
|
|
545
|
+
node.targetCell.height = targetHeight;
|
|
546
|
+
node.target = node.targetCell;
|
|
547
|
+
node.projectionParticipant = true;
|
|
548
|
+
node.hasHostFrame = true;
|
|
549
|
+
node.projectionPaintedByHost = false;
|
|
550
|
+
node.deltaCell.translateX = currentX - targetX;
|
|
551
|
+
node.deltaCell.translateY = currentY - targetY;
|
|
552
|
+
node.deltaCell.scaleX = currentWidth / targetWidth;
|
|
553
|
+
node.deltaCell.scaleY = currentHeight / targetHeight;
|
|
554
|
+
node.delta = node.deltaCell;
|
|
555
|
+
},
|
|
556
|
+
clearProjection(componentId) {
|
|
557
|
+
const node = nodes.get(componentId) ?? staleNode(componentId, 'clearProjection');
|
|
558
|
+
if (node === undefined)
|
|
559
|
+
return;
|
|
560
|
+
node.delta = null;
|
|
561
|
+
node.hasHostFrame = false;
|
|
562
|
+
node.projectionPaintedByHost = false;
|
|
563
|
+
},
|
|
564
|
+
setBorderRadius(componentId, radius) {
|
|
565
|
+
const node = nodes.get(componentId) ?? staleNode(componentId, 'setBorderRadius');
|
|
566
|
+
if (node === undefined)
|
|
567
|
+
return;
|
|
568
|
+
if (radius !== null && (!Number.isFinite(radius) || radius < 0)) {
|
|
569
|
+
reportStaleNode('projection-invalid-frame-input', componentId, 'setBorderRadius', 'not a finite non-negative number');
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (node.borderRadius !== radius) {
|
|
573
|
+
// A CHANGED authored radius invalidates the stale correction (read-current ownership,
|
|
574
|
+
// REQ-LAYOUT-013 — seal r7, F5): the cell answers the new input, so publish the null
|
|
575
|
+
// sentinel until a valid correction exists for it — never the obsolete pair.
|
|
576
|
+
node.radiusCorrected = false;
|
|
577
|
+
}
|
|
578
|
+
node.borderRadius = radius;
|
|
579
|
+
},
|
|
580
|
+
project(visitor) {
|
|
581
|
+
// No orderDirty/reorder/sort edge here (packet §6): register() already performed the stable
|
|
582
|
+
// depth sort; unregister() removes by index, preserving the order.
|
|
583
|
+
// Index loop: for..of allocates an iterator per frame on Hermes (REQ-DRIVER-021).
|
|
584
|
+
for (let index = 0; index < ordered.length; index += 1) {
|
|
585
|
+
const node = ordered[index];
|
|
586
|
+
const parent = node.parentId === null ? undefined : nodes.get(node.parentId);
|
|
587
|
+
if (node.parentId !== null && parent === undefined) {
|
|
588
|
+
// React passive cleanup is parent-first, so a child outliving its parent mid-teardown
|
|
589
|
+
// is routine: skip the orphaned node for THIS frame (its output stays frozen at the
|
|
590
|
+
// last valid value, and descendants keep the frozen inherited tree* values) and record
|
|
591
|
+
// one orphaned-path report per id per episode — never a UI-thread throw (Phase 1.5).
|
|
592
|
+
reportStaleNode('projection-orphaned-path', node.componentId, node.parentId, 'unregistered mid-teardown');
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
const inheritedScaleX = parent?.treeScaleX ?? 1;
|
|
596
|
+
const inheritedScaleY = parent?.treeScaleY ?? 1;
|
|
597
|
+
const inheritedTranslateX = parent?.treeTranslateX ?? 0;
|
|
598
|
+
const inheritedTranslateY = parent?.treeTranslateY ?? 0;
|
|
599
|
+
const inheritedFrameCenterOffsetX = parent?.frameTreeCenterOffsetX ?? 0;
|
|
600
|
+
const inheritedFrameCenterOffsetY = parent?.frameTreeCenterOffsetY ?? 0;
|
|
601
|
+
const ownDelta = node.delta;
|
|
602
|
+
// Seal r9, F1: refuse undefined geometry BEFORE any division. A zero/non-finite
|
|
603
|
+
// inherited (accumulated) scale, a zero/non-finite current host-frame dimension, or a
|
|
604
|
+
// non-finite delta makes this node's output divisions undefined — at HEAD those
|
|
605
|
+
// produced Infinity/NaN outputs and published them into native shared transform values
|
|
606
|
+
// with no report (the review's zero-width-parent and nested-zero-current probes). The
|
|
607
|
+
// whole node freezes at its last valid finite state for this frame (skip the reset AND
|
|
608
|
+
// the recompute; descendants inherit the frozen — finite — tree* values) and records
|
|
609
|
+
// one escalated report per id per episode: every node here is registered and live by
|
|
610
|
+
// construction, so this is malformed live geometry, never a teardown race. A FINITE
|
|
611
|
+
// zero delta scale still publishes (a collapsed host is a legal measurement) — the
|
|
612
|
+
// descendant's inherited-zero check is what refuses it.
|
|
613
|
+
if (!Number.isFinite(inheritedScaleX) ||
|
|
614
|
+
inheritedScaleX === 0 ||
|
|
615
|
+
!Number.isFinite(inheritedScaleY) ||
|
|
616
|
+
inheritedScaleY === 0 ||
|
|
617
|
+
(node.hasHostFrame &&
|
|
618
|
+
(!Number.isFinite(node.layoutCell.width) ||
|
|
619
|
+
node.layoutCell.width === 0 ||
|
|
620
|
+
!Number.isFinite(node.layoutCell.height) ||
|
|
621
|
+
node.layoutCell.height === 0)) ||
|
|
622
|
+
(ownDelta !== null &&
|
|
623
|
+
(!Number.isFinite(ownDelta.translateX) ||
|
|
624
|
+
!Number.isFinite(ownDelta.translateY) ||
|
|
625
|
+
!Number.isFinite(ownDelta.scaleX) ||
|
|
626
|
+
!Number.isFinite(ownDelta.scaleY)))) {
|
|
627
|
+
reportStaleNode('projection-non-finite-output', node.componentId, 'project', 'zero/non-finite scale geometry');
|
|
628
|
+
// The freeze preserves the TRANSFORM at its last valid state, but the radius publish
|
|
629
|
+
// still follows read-current ownership (seal r7, F5): the cell answers only the
|
|
630
|
+
// CURRENT authored radius — a changed or removed radius publishes the null sentinel
|
|
631
|
+
// even on a frozen frame, never the obsolete corrected pair.
|
|
632
|
+
node.output.borderRadius =
|
|
633
|
+
node.borderRadius !== null && node.radiusCorrected ? node.radiusCell : null;
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
// Outside host frames, radius is painted inside the raw projection transform before this
|
|
637
|
+
// node's correction is composed. Preserve that scale separately: `treeScale*output.scale` below is the
|
|
638
|
+
// effective scale inherited by descendants, whereas the radius must cancel the scale
|
|
639
|
+
// Reanimated actually applies to this host's border paint.
|
|
640
|
+
const rawTreeScaleX = node.projectionPaintedByHost
|
|
641
|
+
? (ownDelta?.scaleX ?? 1)
|
|
642
|
+
: inheritedScaleX * (ownDelta?.scaleX ?? 1);
|
|
643
|
+
const rawTreeScaleY = node.projectionPaintedByHost
|
|
644
|
+
? (ownDelta?.scaleY ?? 1)
|
|
645
|
+
: inheritedScaleY * (ownDelta?.scaleY ?? 1);
|
|
646
|
+
resetOutput(node);
|
|
647
|
+
if (node.hasHostFrame) {
|
|
648
|
+
const target = node.target;
|
|
649
|
+
if (target === null) {
|
|
650
|
+
// ui-throw-ok: internal invariant — setHostFrame is the ONLY production writer of
|
|
651
|
+
// hasHostFrame and it always assigns the target cell in the same edge; setTarget has
|
|
652
|
+
// no production caller. A null target here is tree corruption (a programmer error),
|
|
653
|
+
// never an operating error — the audited assert stays loud.
|
|
654
|
+
throw new Error(`projection tree: '${node.componentId}' has a host frame without a target rect; host-frame projection requires coherent geometry.`);
|
|
655
|
+
}
|
|
656
|
+
// Reanimated's layout lane already paints `layoutCell` — the host's real current box,
|
|
657
|
+
// parent-relative — for roots and nested hosts, which is the pin's nested layout box
|
|
658
|
+
// (relativeTarget = mix(relativeTargetOrigin, relativeLayout, p)). The tree never
|
|
659
|
+
// replays this host's FLIP or rescales its lane: the host keeps its painted size.
|
|
660
|
+
// The one correction is positional, when the lane's own parent was committed by Yoga at
|
|
661
|
+
// its final place inside a flying frame: the lane then rides that misplaced parent, so
|
|
662
|
+
// the host is re-centred on its target centre in the ancestor's painted frame.
|
|
663
|
+
if (parent?.committedInHostFrame === true) {
|
|
664
|
+
node.output.translateX =
|
|
665
|
+
target.x +
|
|
666
|
+
target.width / 2 +
|
|
667
|
+
inheritedFrameCenterOffsetX -
|
|
668
|
+
(node.layoutCell.x + node.layoutCell.width / 2);
|
|
669
|
+
node.output.translateY =
|
|
670
|
+
target.y +
|
|
671
|
+
target.height / 2 +
|
|
672
|
+
inheritedFrameCenterOffsetY -
|
|
673
|
+
(node.layoutCell.y + node.layoutCell.height / 2);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
else if (ownDelta !== null && node.projectionPaintedByHost) {
|
|
677
|
+
// layoutId already paints its own FLIP transform in the host style. The tree's output
|
|
678
|
+
// cancels only ancestor geometry; `tree*` below still publishes the live direct delta
|
|
679
|
+
// to targetless descendants through every muggle node.
|
|
680
|
+
node.output.translateX = -inheritedTranslateX / inheritedScaleX;
|
|
681
|
+
node.output.translateY = -inheritedTranslateY / inheritedScaleY;
|
|
682
|
+
node.output.scaleX = 1 / inheritedScaleX;
|
|
683
|
+
node.output.scaleY = 1 / inheritedScaleY;
|
|
684
|
+
}
|
|
685
|
+
else if (ownDelta !== null) {
|
|
686
|
+
// Deltas are expressed in viewport coordinates, but a nested host's transform runs
|
|
687
|
+
// inside its parent's already-projected coordinate system. Convert every leg through
|
|
688
|
+
// that parent frame: scale-only cancellation makes a targetless child drift by the
|
|
689
|
+
// ancestor translation (the M3.5 two-leg registration failure).
|
|
690
|
+
node.output.translateX = (ownDelta.translateX - inheritedTranslateX) / inheritedScaleX;
|
|
691
|
+
node.output.translateY = (ownDelta.translateY - inheritedTranslateY) / inheritedScaleY;
|
|
692
|
+
// A projecting child still renders inside its parent's transformed projection frame.
|
|
693
|
+
// Cancel that inherited scale before applying the child's own box delta, exactly as a
|
|
694
|
+
// targetless child does below. Without this, a child FLIP of 0.8 under a parent scale of
|
|
695
|
+
// 0.5 renders at 0.4 and passes that distortion on to every descendant.
|
|
696
|
+
node.output.scaleX = ownDelta.scaleX / inheritedScaleX;
|
|
697
|
+
node.output.scaleY = ownDelta.scaleY / inheritedScaleY;
|
|
698
|
+
}
|
|
699
|
+
else if (node.projectionParticipant) {
|
|
700
|
+
// A targetless node has no independently measured box delta. Under pure FLIP
|
|
701
|
+
// (frameTreeCenterOffset == 0) its host Yoga position is already parent-relative, so
|
|
702
|
+
// only counter-scale is faithful — replaying ancestor FLIP translation displaces the
|
|
703
|
+
// flex centre (the M3.5 two-leg registration failure). Under native host-layout frames
|
|
704
|
+
// the ancestor is paint-relative identity and half-size centre movement is carried in
|
|
705
|
+
// `frameTreeCenterOffset`: Yoga still commits children against the TARGET box, so a
|
|
706
|
+
// targetless (or non-interpolating) descendant freezes at the final centre unless it
|
|
707
|
+
// receives that offset (Android residual ≈ (targetSize − currentSize)/2).
|
|
708
|
+
node.output.scaleX = 1 / inheritedScaleX;
|
|
709
|
+
node.output.scaleY = 1 / inheritedScaleY;
|
|
710
|
+
node.output.translateX = inheritedFrameCenterOffsetX / inheritedScaleX;
|
|
711
|
+
node.output.translateY = inheritedFrameCenterOffsetY / inheritedScaleY;
|
|
712
|
+
}
|
|
713
|
+
// Descendants inherit the transform that is actually rendered, not this node's raw box
|
|
714
|
+
// delta. A projecting child therefore carries its own scale forward after cancelling its
|
|
715
|
+
// ancestor's scale; an ordinary Motion intermediary remains an exact identity conduit.
|
|
716
|
+
node.treeScaleX = node.projectionPaintedByHost
|
|
717
|
+
? (ownDelta?.scaleX ?? 1)
|
|
718
|
+
: inheritedScaleX * node.output.scaleX;
|
|
719
|
+
node.treeScaleY = node.projectionPaintedByHost
|
|
720
|
+
? (ownDelta?.scaleY ?? 1)
|
|
721
|
+
: inheritedScaleY * node.output.scaleY;
|
|
722
|
+
node.treeTranslateX = node.projectionPaintedByHost
|
|
723
|
+
? (ownDelta?.translateX ?? 0)
|
|
724
|
+
: inheritedTranslateX + inheritedScaleX * node.output.translateX;
|
|
725
|
+
node.treeTranslateY = node.projectionPaintedByHost
|
|
726
|
+
? (ownDelta?.translateY ?? 0)
|
|
727
|
+
: inheritedTranslateY + inheritedScaleY * node.output.translateY;
|
|
728
|
+
if (node.hasHostFrame) {
|
|
729
|
+
const target = node.target;
|
|
730
|
+
if (target === null) {
|
|
731
|
+
// ui-throw-ok: internal invariant — identical to the same guard earlier in this walk:
|
|
732
|
+
// hasHostFrame implies an assigned target by construction (setHostFrame writes both
|
|
733
|
+
// in one edge), so a null target here is a tree-corruption assert, never an
|
|
734
|
+
// operating error.
|
|
735
|
+
throw new Error(`projection tree: '${node.componentId}' lost its target rect during host-frame projection.`);
|
|
736
|
+
}
|
|
737
|
+
// Yoga commits this host's children against its TARGET box while the lane paints its
|
|
738
|
+
// current box; the half-size change shifts a child's target centre into the painted
|
|
739
|
+
// frame (500 → 324 yields -88), so an already-centred child remains centred. Ancestor
|
|
740
|
+
// offsets are not added: this host's painted frame (lane plus output) already carries
|
|
741
|
+
// them to everything inside it.
|
|
742
|
+
node.frameTreeCenterOffsetX = (node.layoutCell.width - target.width) / 2;
|
|
743
|
+
node.frameTreeCenterOffsetY = (node.layoutCell.height - target.height) / 2;
|
|
744
|
+
node.committedInHostFrame = false;
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
// A muggle node carries ancestor centre movement untouched. Nodes without measured
|
|
748
|
+
// host geometry remain on the targetless path above, but host-framed descendants still
|
|
749
|
+
// correct through any number of ordinary Motion intermediaries.
|
|
750
|
+
node.frameTreeCenterOffsetX = inheritedFrameCenterOffsetX;
|
|
751
|
+
node.frameTreeCenterOffsetY = inheritedFrameCenterOffsetY;
|
|
752
|
+
node.committedInHostFrame =
|
|
753
|
+
parent !== undefined && (parent.hasHostFrame || parent.committedInHostFrame);
|
|
754
|
+
}
|
|
755
|
+
if (node.borderRadius !== null) {
|
|
756
|
+
// The radius cancels the scale painted on this host so the on-screen corner stays the
|
|
757
|
+
// authored circle (pin scale-border-radius.ts). A host frame is painted by Reanimated's
|
|
758
|
+
// layout lane at its real current size, so its own box delta is NOT a painted scale:
|
|
759
|
+
// only the inherited paint and this node's output transform are.
|
|
760
|
+
const radiusScaleX = node.hasHostFrame
|
|
761
|
+
? inheritedScaleX * node.output.scaleX
|
|
762
|
+
: rawTreeScaleX;
|
|
763
|
+
const radiusScaleY = node.hasHostFrame
|
|
764
|
+
? inheritedScaleY * node.output.scaleY
|
|
765
|
+
: rawTreeScaleY;
|
|
766
|
+
if (!Number.isFinite(radiusScaleX) ||
|
|
767
|
+
!Number.isFinite(radiusScaleY) ||
|
|
768
|
+
radiusScaleX === 0 ||
|
|
769
|
+
radiusScaleY === 0) {
|
|
770
|
+
// A zero/non-finite accumulated scale makes the correction undefined — an OPERATING
|
|
771
|
+
// condition (setHostFrame accepts a zero current width; only target dims are
|
|
772
|
+
// validated), never an invariant. Report once per id per episode through the
|
|
773
|
+
// stale-report channel and skip the correction, preserving the last valid radius —
|
|
774
|
+
// a throw here escapes the UI frame (iOS AUTO SIGABRT seal r2, F3).
|
|
775
|
+
reportStaleNode('projection-invalid-radius-correction', node.componentId, '', 'zero/non-finite accumulated tree scale');
|
|
776
|
+
}
|
|
777
|
+
else {
|
|
778
|
+
writeCorrectedRadius(node.radiusCell, node.borderRadius, radiusScaleX, radiusScaleY);
|
|
779
|
+
node.radiusCorrected = true;
|
|
780
|
+
}
|
|
781
|
+
// The sentinel law: publish the cell only once a valid correction exists — null
|
|
782
|
+
// otherwise (authored radius retained); a skipped frame keeps the last valid state.
|
|
783
|
+
node.output.borderRadius = node.radiusCorrected ? node.radiusCell : null;
|
|
784
|
+
}
|
|
785
|
+
const targetDeltaScaleX = ownDelta?.scaleX ?? 1;
|
|
786
|
+
const targetDeltaScaleY = ownDelta?.scaleY ?? 1;
|
|
787
|
+
node.output.targetDeltaScaleX = targetDeltaScaleX;
|
|
788
|
+
node.output.targetDeltaScaleY = targetDeltaScaleY;
|
|
789
|
+
node.output.treeScaleX =
|
|
790
|
+
targetDeltaScaleX === 0 ? rawTreeScaleX : rawTreeScaleX / targetDeltaScaleX;
|
|
791
|
+
node.output.treeScaleY =
|
|
792
|
+
targetDeltaScaleY === 0 ? rawTreeScaleY : rawTreeScaleY / targetDeltaScaleY;
|
|
793
|
+
visitor?.(node);
|
|
794
|
+
}
|
|
795
|
+
},
|
|
796
|
+
output(componentId) {
|
|
797
|
+
const node = nodes.get(componentId) ?? staleNode(componentId, 'output');
|
|
798
|
+
// A stale read returns the shared frozen identity output (last safe visual state), never a
|
|
799
|
+
// per-call allocation and never a throw (Phase 1.5).
|
|
800
|
+
if (node === undefined)
|
|
801
|
+
return STALE_IDENTITY_OUTPUT;
|
|
802
|
+
return node.output;
|
|
803
|
+
},
|
|
804
|
+
// alloc-ok: lifecycle-edge — a quiet frame returns the shared frozen empty array; draining
|
|
805
|
+
// actual reports is a bounded per-episode operation, never per-frame work.
|
|
806
|
+
drainStaleReports() {
|
|
807
|
+
if (staleReports.length === 0)
|
|
808
|
+
return EMPTY_STALE_REPORTS;
|
|
809
|
+
const drained = staleReports.slice();
|
|
810
|
+
staleReports.length = 0;
|
|
811
|
+
// The inter-drain window rolls (seal r10, F1): class counters, the overflow marker, and
|
|
812
|
+
// the per-id loss ledger all reset.
|
|
813
|
+
containedInRing = 0;
|
|
814
|
+
escalatedInRing = 0;
|
|
815
|
+
overflowPending = false;
|
|
816
|
+
overflowRecordIndex = -1;
|
|
817
|
+
droppedContainedCounts.clear();
|
|
818
|
+
droppedEscalatedCounts.clear();
|
|
819
|
+
extraContainedDrops = 0;
|
|
820
|
+
extraEscalatedDrops = 0;
|
|
821
|
+
return drained;
|
|
822
|
+
},
|
|
823
|
+
};
|
|
824
|
+
}
|