@pilates/core 1.0.0-rc.2 → 1.0.1

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.
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Layout-engine caches.
3
+ *
4
+ * Phase 1: `MeasureCache` — caches the result of a leaf node's
5
+ * `MeasureFunc` so repeated calls with the same available-space
6
+ * inputs don't reinvoke the measurer. Wired into every measure-func
7
+ * call site in `main-axis.ts` via the `callMeasureFunc` helper.
8
+ *
9
+ * Caches are owned by `Node` (`_measureCache` field) and cleared
10
+ * automatically by `markDirty()`. Consumers never interact with the
11
+ * cache directly — it is `@internal`.
12
+ *
13
+ * Phase 2 added `LayoutCache` here (P2-T1).
14
+ *
15
+ * @internal
16
+ */
17
+ import type { ComputedLayout } from '../layout.js';
18
+ import type { MeasureMode } from '../measure-func.js';
19
+ import type { Node } from '../node.js';
20
+ /** @internal */
21
+ export interface MeasureCacheKey {
22
+ availableWidth: number;
23
+ widthMode: MeasureMode;
24
+ availableHeight: number;
25
+ heightMode: MeasureMode;
26
+ }
27
+ /** @internal */
28
+ export interface MeasureCacheValue {
29
+ width: number;
30
+ height: number;
31
+ }
32
+ /**
33
+ * Up to eight slots — matches Yoga's `LayoutResults::MaxCachedMeasurements`
34
+ * with the documented rationale "98% of analyzed layouts require less
35
+ * than 8 entries" (see facebook/yoga `node/LayoutResults.h`). Covers the
36
+ * hypothetical-vs-final pattern (single leaf measured twice per pass)
37
+ * plus broader reuse patterns where the same leaf is measured at multiple
38
+ * cross-axis sizes during line packing or across consecutive layout calls.
39
+ *
40
+ * Linear scan over 8 slots is a few-cycle hot-path cost; slots are
41
+ * lazy-allocated only on leaves with a `MeasureFunc` installed.
42
+ *
43
+ * @internal
44
+ */
45
+ export declare class MeasureCache {
46
+ private static readonly MAX_ENTRIES;
47
+ private slots;
48
+ /**
49
+ * Hit/miss counters for diagnostics (bench output, debugging). Always
50
+ * on — the spec originally proposed `__DEV__`-gating these, but the
51
+ * project has no build-tooling `__DEV__` define and the counters cost
52
+ * two property writes per `lookup()` (negligible at any realistic
53
+ * call rate). Counts are incremented only by `lookup()`; `store()`
54
+ * assumes `lookup()` has already been called for the same key on the
55
+ * same call site, so it does not double-count.
56
+ *
57
+ * @internal
58
+ */
59
+ hits: number;
60
+ /** See {@link hits}. @internal */
61
+ misses: number;
62
+ lookup(key: MeasureCacheKey): MeasureCacheValue | undefined;
63
+ /**
64
+ * Store `value` for `key`. If `key` is already present, overwrite in
65
+ * place without growing the slot array; otherwise append, evicting
66
+ * the oldest slot when the array is at `MAX_ENTRIES` capacity.
67
+ *
68
+ * Callers should always invoke `lookup(key)` first and only call
69
+ * `store()` on a miss — the `misses` counter is incremented by
70
+ * `lookup()`, not here, so calling `store()` without a prior lookup
71
+ * would cause silent miss undercounting in diagnostics.
72
+ */
73
+ store(key: MeasureCacheKey, value: MeasureCacheValue): void;
74
+ /**
75
+ * Drop every cached entry. The `hits`/`misses` counters are
76
+ * deliberately preserved across `clear()` calls — they're lifetime
77
+ * diagnostics, not per-pass metrics, and zeroing them would mask the
78
+ * total-pressure picture during bench runs.
79
+ */
80
+ clear(): void;
81
+ }
82
+ /** @internal */
83
+ export interface LayoutCacheKey {
84
+ availableWidth: number;
85
+ widthMode: MeasureMode;
86
+ availableHeight: number;
87
+ heightMode: MeasureMode;
88
+ }
89
+ /** @internal */
90
+ export interface CachedChildLayout {
91
+ left: number;
92
+ top: number;
93
+ width: number;
94
+ height: number;
95
+ scrollWidth: number;
96
+ scrollHeight: number;
97
+ /**
98
+ * Pre-rounding (float) left position of this child, captured before
99
+ * `roundLayout` converts positions to integers. Used by
100
+ * `roundLayoutSubtree` to compute the correct float absolute coordinate
101
+ * when re-laying out a dirty boundary node under a cache-hit root.
102
+ * See `Node._floatLeft` for the full explanation.
103
+ */
104
+ floatLeft: number;
105
+ /** See {@link floatLeft}. */
106
+ floatTop: number;
107
+ }
108
+ /** @internal */
109
+ export interface LayoutCacheValue {
110
+ width: number;
111
+ height: number;
112
+ scrollWidth: number;
113
+ scrollHeight: number;
114
+ childLayouts: CachedChildLayout[];
115
+ }
116
+ /**
117
+ * Single-slot per-node layout cache. Matches Yoga's `cachedLayout`
118
+ * (single overwrite-on-write) and Taffy's 1-slot layout-cache. Internal
119
+ * nodes' final-pass keys converge to one stable input once the parent's
120
+ * flex distribution has settled, so additional slots would be dead memory.
121
+ *
122
+ * Lazy-allocated by the algorithm in `algorithm/main-axis.ts` and
123
+ * `algorithm/index.ts` only on nodes that actually go through the
124
+ * `layoutChildren` recursion. Cleared by `Node.markDirty()` (which fires
125
+ * on every style/tree mutation).
126
+ *
127
+ * Hit/miss counters are always-on (same rationale as `MeasureCache`).
128
+ *
129
+ * @internal
130
+ */
131
+ export declare class LayoutCache {
132
+ private slot;
133
+ /** @internal */
134
+ hits: number;
135
+ /** @internal */
136
+ misses: number;
137
+ lookup(key: LayoutCacheKey): LayoutCacheValue | undefined;
138
+ /**
139
+ * Store `value` for `key`. Single-slot — replaces the previous entry
140
+ * unconditionally. Caller must construct a fresh `LayoutCacheValue` per
141
+ * store; this method does NOT deep-clone for performance, so any
142
+ * subsequent mutation of `value` is visible through `lookup`. The
143
+ * algorithm builds new values per layout pass so the contract holds.
144
+ */
145
+ store(key: LayoutCacheKey, value: LayoutCacheValue): void;
146
+ clear(): void;
147
+ }
148
+ /**
149
+ * Pre-order traversal returning a flat array of layout snapshots, one per
150
+ * node. Used by differential mode to capture and compare the entire tree's
151
+ * layout state cheaply.
152
+ *
153
+ * Captures only the six `ComputedLayout` fields. `_dirty` flags, cache
154
+ * contents, and other ancillary state are NOT in scope. Phase 2's
155
+ * layout-cache work should add `_dirty` validation to the harness if
156
+ * dirty-flag semantics ever become load-bearing for cache correctness.
157
+ *
158
+ * @internal
159
+ */
160
+ export declare function snapshotTreeLayouts(root: Node): ComputedLayout[];
161
+ /**
162
+ * Recursively clear `_measureCache` and `_layoutCache` on every node in
163
+ * the subtree. Used by differential mode and the fuzzer to force the cold
164
+ * path.
165
+ *
166
+ * @internal
167
+ */
168
+ export declare function clearAllCaches(root: Node): void;
169
+ /**
170
+ * Mark every node in the subtree dirty. Used after `clearAllCaches` to
171
+ * force `calculateLayout` down the full cold path.
172
+ *
173
+ * Uses `_forceDirty()` rather than `markDirty()` so the propagation
174
+ * walks through layout boundaries (Phase 3). Differential-mode and
175
+ * fuzzer validation rely on dirtying the entire tree to force the
176
+ * cold path; without this, boundaries would short-circuit the walk
177
+ * and leave clean ancestors that would never re-run the algorithm.
178
+ *
179
+ * Note: `Node._forceDirty()` also calls `_measureCache?.clear()` and
180
+ * `_layoutCache?.clear()` as a side-effect of dirtying. So calling
181
+ * `clearAllCaches(root)` then `markDirtyDeep(root)` clears each node's
182
+ * caches twice. The second clear is harmless (no-op on an empty
183
+ * structure), but differential-mode callers should know the
184
+ * redundancy is intentional — we want both invariants explicit at the
185
+ * call site.
186
+ *
187
+ * @internal
188
+ */
189
+ export declare function markDirtyDeep(root: Node): void;
190
+ /**
191
+ * Build a `LayoutCacheValue` from `node`'s current `_layout` + its direct
192
+ * children's `_layout`. Captures only direct children; deeper descendants
193
+ * are reconstituted via their own caches during `restoreFromCache`.
194
+ *
195
+ * Called by the algorithm AFTER `roundLayout` and `computeScrollSizes`
196
+ * have populated all `_layout` fields, so the captured values are
197
+ * already integer-rounded and scroll-aware.
198
+ *
199
+ * @internal
200
+ */
201
+ export declare function snapshotForCache(node: Node): LayoutCacheValue;
202
+ /**
203
+ * Restore `node`'s own size + scroll metrics, plus its direct children's
204
+ * left/top/width/height/scroll. The caller is responsible for handling
205
+ * the recursion into deeper descendants via per-child cache lookups (or
206
+ * a `layoutChildren` fallback on miss).
207
+ *
208
+ * Pre-conditions: `node`'s child list at this call must match the child
209
+ * list captured at cache-store time. The cache invalidation on
210
+ * `insertChild`/`removeChild` (via `markDirty`) guarantees this — a
211
+ * mismatch indicates a cache-correctness bug. We assert in differential
212
+ * mode.
213
+ *
214
+ * @internal
215
+ */
216
+ export declare function restoreFromCache(node: Node, value: LayoutCacheValue): void;
217
+ /**
218
+ * Produce a human-readable diff of two tree-layout snapshots. Returns
219
+ * an empty string if they match. Used by differential mode to surface
220
+ * cache bugs with enough context to debug them.
221
+ *
222
+ * @internal
223
+ */
224
+ export declare function diffLayouts(a: ComputedLayout[], b: ComputedLayout[]): string;
225
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/algorithm/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAEvC,gBAAgB;AAChB,MAAM,WAAW,eAAe;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,WAAW,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,WAAW,CAAC;CACzB;AAED,gBAAgB;AAChB,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAK;IACxC,OAAO,CAAC,KAAK,CAAkD;IAE/D;;;;;;;;;;OAUG;IACH,IAAI,SAAK;IACT,kCAAkC;IAClC,MAAM,SAAK;IAEX,MAAM,CAAC,GAAG,EAAE,eAAe,GAAG,iBAAiB,GAAG,SAAS;IAgB3D;;;;;;;;;OASG;IACH,KAAK,CAAC,GAAG,EAAE,eAAe,EAAE,KAAK,EAAE,iBAAiB,GAAG,IAAI;IAqB3D;;;;;OAKG;IACH,KAAK,IAAI,IAAI;CAGd;AAED,gBAAgB;AAChB,MAAM,WAAW,cAAc;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,WAAW,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,WAAW,CAAC;CAKzB;AAED,gBAAgB;AAChB,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;;OAMG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,gBAAgB;AAChB,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,iBAAiB,EAAE,CAAC;CACnC;AAED;;;;;;;;;;;;;;GAcG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,IAAI,CAAyE;IAErF,gBAAgB;IAChB,IAAI,SAAK;IACT,gBAAgB;IAChB,MAAM,SAAK;IAEX,MAAM,CAAC,GAAG,EAAE,cAAc,GAAG,gBAAgB,GAAG,SAAS;IAgBzD;;;;;;OAMG;IACH,KAAK,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAIzD,KAAK,IAAI,IAAI;CAGd;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,IAAI,GAAG,cAAc,EAAE,CAgBhE;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAI/C;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAG9C;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,GAAG,gBAAgB,CAuB7D;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,gBAAgB,GAAG,IAAI,CA2B1E;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,cAAc,EAAE,EAAE,CAAC,EAAE,cAAc,EAAE,GAAG,MAAM,CAe5E"}
@@ -0,0 +1,312 @@
1
+ /**
2
+ * Layout-engine caches.
3
+ *
4
+ * Phase 1: `MeasureCache` — caches the result of a leaf node's
5
+ * `MeasureFunc` so repeated calls with the same available-space
6
+ * inputs don't reinvoke the measurer. Wired into every measure-func
7
+ * call site in `main-axis.ts` via the `callMeasureFunc` helper.
8
+ *
9
+ * Caches are owned by `Node` (`_measureCache` field) and cleared
10
+ * automatically by `markDirty()`. Consumers never interact with the
11
+ * cache directly — it is `@internal`.
12
+ *
13
+ * Phase 2 added `LayoutCache` here (P2-T1).
14
+ *
15
+ * @internal
16
+ */
17
+ /**
18
+ * Up to eight slots — matches Yoga's `LayoutResults::MaxCachedMeasurements`
19
+ * with the documented rationale "98% of analyzed layouts require less
20
+ * than 8 entries" (see facebook/yoga `node/LayoutResults.h`). Covers the
21
+ * hypothetical-vs-final pattern (single leaf measured twice per pass)
22
+ * plus broader reuse patterns where the same leaf is measured at multiple
23
+ * cross-axis sizes during line packing or across consecutive layout calls.
24
+ *
25
+ * Linear scan over 8 slots is a few-cycle hot-path cost; slots are
26
+ * lazy-allocated only on leaves with a `MeasureFunc` installed.
27
+ *
28
+ * @internal
29
+ */
30
+ export class MeasureCache {
31
+ static MAX_ENTRIES = 8;
32
+ slots = [];
33
+ /**
34
+ * Hit/miss counters for diagnostics (bench output, debugging). Always
35
+ * on — the spec originally proposed `__DEV__`-gating these, but the
36
+ * project has no build-tooling `__DEV__` define and the counters cost
37
+ * two property writes per `lookup()` (negligible at any realistic
38
+ * call rate). Counts are incremented only by `lookup()`; `store()`
39
+ * assumes `lookup()` has already been called for the same key on the
40
+ * same call site, so it does not double-count.
41
+ *
42
+ * @internal
43
+ */
44
+ hits = 0;
45
+ /** See {@link hits}. @internal */
46
+ misses = 0;
47
+ lookup(key) {
48
+ for (const slot of this.slots) {
49
+ if (slot.availableWidth === key.availableWidth &&
50
+ slot.widthMode === key.widthMode &&
51
+ slot.availableHeight === key.availableHeight &&
52
+ slot.heightMode === key.heightMode) {
53
+ this.hits++;
54
+ return { width: slot.width, height: slot.height };
55
+ }
56
+ }
57
+ this.misses++;
58
+ return undefined;
59
+ }
60
+ /**
61
+ * Store `value` for `key`. If `key` is already present, overwrite in
62
+ * place without growing the slot array; otherwise append, evicting
63
+ * the oldest slot when the array is at `MAX_ENTRIES` capacity.
64
+ *
65
+ * Callers should always invoke `lookup(key)` first and only call
66
+ * `store()` on a miss — the `misses` counter is incremented by
67
+ * `lookup()`, not here, so calling `store()` without a prior lookup
68
+ * would cause silent miss undercounting in diagnostics.
69
+ */
70
+ store(key, value) {
71
+ for (const slot of this.slots) {
72
+ if (slot.availableWidth === key.availableWidth &&
73
+ slot.widthMode === key.widthMode &&
74
+ slot.availableHeight === key.availableHeight &&
75
+ slot.heightMode === key.heightMode) {
76
+ slot.width = value.width;
77
+ slot.height = value.height;
78
+ return;
79
+ }
80
+ }
81
+ if (this.slots.length < MeasureCache.MAX_ENTRIES) {
82
+ this.slots.push({ ...key, ...value });
83
+ }
84
+ else {
85
+ this.slots.shift();
86
+ this.slots.push({ ...key, ...value });
87
+ }
88
+ }
89
+ /**
90
+ * Drop every cached entry. The `hits`/`misses` counters are
91
+ * deliberately preserved across `clear()` calls — they're lifetime
92
+ * diagnostics, not per-pass metrics, and zeroing them would mask the
93
+ * total-pressure picture during bench runs.
94
+ */
95
+ clear() {
96
+ this.slots.length = 0;
97
+ }
98
+ }
99
+ /**
100
+ * Single-slot per-node layout cache. Matches Yoga's `cachedLayout`
101
+ * (single overwrite-on-write) and Taffy's 1-slot layout-cache. Internal
102
+ * nodes' final-pass keys converge to one stable input once the parent's
103
+ * flex distribution has settled, so additional slots would be dead memory.
104
+ *
105
+ * Lazy-allocated by the algorithm in `algorithm/main-axis.ts` and
106
+ * `algorithm/index.ts` only on nodes that actually go through the
107
+ * `layoutChildren` recursion. Cleared by `Node.markDirty()` (which fires
108
+ * on every style/tree mutation).
109
+ *
110
+ * Hit/miss counters are always-on (same rationale as `MeasureCache`).
111
+ *
112
+ * @internal
113
+ */
114
+ export class LayoutCache {
115
+ slot = undefined;
116
+ /** @internal */
117
+ hits = 0;
118
+ /** @internal */
119
+ misses = 0;
120
+ lookup(key) {
121
+ const slot = this.slot;
122
+ if (slot !== undefined &&
123
+ slot.availableWidth === key.availableWidth &&
124
+ slot.widthMode === key.widthMode &&
125
+ slot.availableHeight === key.availableHeight &&
126
+ slot.heightMode === key.heightMode) {
127
+ this.hits++;
128
+ return slot.value;
129
+ }
130
+ this.misses++;
131
+ return undefined;
132
+ }
133
+ /**
134
+ * Store `value` for `key`. Single-slot — replaces the previous entry
135
+ * unconditionally. Caller must construct a fresh `LayoutCacheValue` per
136
+ * store; this method does NOT deep-clone for performance, so any
137
+ * subsequent mutation of `value` is visible through `lookup`. The
138
+ * algorithm builds new values per layout pass so the contract holds.
139
+ */
140
+ store(key, value) {
141
+ this.slot = { ...key, value };
142
+ }
143
+ clear() {
144
+ this.slot = undefined;
145
+ }
146
+ }
147
+ /**
148
+ * Pre-order traversal returning a flat array of layout snapshots, one per
149
+ * node. Used by differential mode to capture and compare the entire tree's
150
+ * layout state cheaply.
151
+ *
152
+ * Captures only the six `ComputedLayout` fields. `_dirty` flags, cache
153
+ * contents, and other ancillary state are NOT in scope. Phase 2's
154
+ * layout-cache work should add `_dirty` validation to the harness if
155
+ * dirty-flag semantics ever become load-bearing for cache correctness.
156
+ *
157
+ * @internal
158
+ */
159
+ export function snapshotTreeLayouts(root) {
160
+ const out = [];
161
+ visit(root);
162
+ return out;
163
+ function visit(n) {
164
+ out.push({
165
+ left: n.layout.left,
166
+ top: n.layout.top,
167
+ width: n.layout.width,
168
+ height: n.layout.height,
169
+ scrollWidth: n.layout.scrollWidth,
170
+ scrollHeight: n.layout.scrollHeight,
171
+ });
172
+ for (let i = 0; i < n.getChildCount(); i++)
173
+ visit(n.getChild(i));
174
+ }
175
+ }
176
+ /**
177
+ * Recursively clear `_measureCache` and `_layoutCache` on every node in
178
+ * the subtree. Used by differential mode and the fuzzer to force the cold
179
+ * path.
180
+ *
181
+ * @internal
182
+ */
183
+ export function clearAllCaches(root) {
184
+ root._measureCache?.clear();
185
+ root._layoutCache?.clear();
186
+ for (let i = 0; i < root.getChildCount(); i++)
187
+ clearAllCaches(root.getChild(i));
188
+ }
189
+ /**
190
+ * Mark every node in the subtree dirty. Used after `clearAllCaches` to
191
+ * force `calculateLayout` down the full cold path.
192
+ *
193
+ * Uses `_forceDirty()` rather than `markDirty()` so the propagation
194
+ * walks through layout boundaries (Phase 3). Differential-mode and
195
+ * fuzzer validation rely on dirtying the entire tree to force the
196
+ * cold path; without this, boundaries would short-circuit the walk
197
+ * and leave clean ancestors that would never re-run the algorithm.
198
+ *
199
+ * Note: `Node._forceDirty()` also calls `_measureCache?.clear()` and
200
+ * `_layoutCache?.clear()` as a side-effect of dirtying. So calling
201
+ * `clearAllCaches(root)` then `markDirtyDeep(root)` clears each node's
202
+ * caches twice. The second clear is harmless (no-op on an empty
203
+ * structure), but differential-mode callers should know the
204
+ * redundancy is intentional — we want both invariants explicit at the
205
+ * call site.
206
+ *
207
+ * @internal
208
+ */
209
+ export function markDirtyDeep(root) {
210
+ root._forceDirty();
211
+ for (let i = 0; i < root.getChildCount(); i++)
212
+ markDirtyDeep(root.getChild(i));
213
+ }
214
+ /**
215
+ * Build a `LayoutCacheValue` from `node`'s current `_layout` + its direct
216
+ * children's `_layout`. Captures only direct children; deeper descendants
217
+ * are reconstituted via their own caches during `restoreFromCache`.
218
+ *
219
+ * Called by the algorithm AFTER `roundLayout` and `computeScrollSizes`
220
+ * have populated all `_layout` fields, so the captured values are
221
+ * already integer-rounded and scroll-aware.
222
+ *
223
+ * @internal
224
+ */
225
+ export function snapshotForCache(node) {
226
+ const childLayouts = [];
227
+ const count = node.getChildCount();
228
+ for (let i = 0; i < count; i++) {
229
+ const c = node.getChild(i);
230
+ childLayouts.push({
231
+ left: c.layout.left,
232
+ top: c.layout.top,
233
+ width: c.layout.width,
234
+ height: c.layout.height,
235
+ scrollWidth: c.layout.scrollWidth,
236
+ scrollHeight: c.layout.scrollHeight,
237
+ floatLeft: c._floatLeft,
238
+ floatTop: c._floatTop,
239
+ });
240
+ }
241
+ return {
242
+ width: node.layout.width,
243
+ height: node.layout.height,
244
+ scrollWidth: node.layout.scrollWidth,
245
+ scrollHeight: node.layout.scrollHeight,
246
+ childLayouts,
247
+ };
248
+ }
249
+ /**
250
+ * Restore `node`'s own size + scroll metrics, plus its direct children's
251
+ * left/top/width/height/scroll. The caller is responsible for handling
252
+ * the recursion into deeper descendants via per-child cache lookups (or
253
+ * a `layoutChildren` fallback on miss).
254
+ *
255
+ * Pre-conditions: `node`'s child list at this call must match the child
256
+ * list captured at cache-store time. The cache invalidation on
257
+ * `insertChild`/`removeChild` (via `markDirty`) guarantees this — a
258
+ * mismatch indicates a cache-correctness bug. We assert in differential
259
+ * mode.
260
+ *
261
+ * @internal
262
+ */
263
+ export function restoreFromCache(node, value) {
264
+ if (process.env.PILATES_DIFFERENTIAL_LAYOUT === '1') {
265
+ if (node.getChildCount() !== value.childLayouts.length) {
266
+ throw new Error(`[pilates layout cache] restored value has ${value.childLayouts.length} children but node has ${node.getChildCount()} — cache invalidation bug`);
267
+ }
268
+ }
269
+ node._layout.width = value.width;
270
+ node._layout.height = value.height;
271
+ node._layout.scrollWidth = value.scrollWidth;
272
+ node._layout.scrollHeight = value.scrollHeight;
273
+ // node._layout.left/top are set by the caller before recursion starts
274
+ // (root sets to 0; child positions come from this restore via the
275
+ // childLayouts array below).
276
+ for (let i = 0; i < node.getChildCount(); i++) {
277
+ const c = node.getChild(i);
278
+ const cl = value.childLayouts[i];
279
+ c._layout.left = cl.left;
280
+ c._layout.top = cl.top;
281
+ c._layout.width = cl.width;
282
+ c._layout.height = cl.height;
283
+ c._layout.scrollWidth = cl.scrollWidth;
284
+ c._layout.scrollHeight = cl.scrollHeight;
285
+ c._floatLeft = cl.floatLeft;
286
+ c._floatTop = cl.floatTop;
287
+ }
288
+ }
289
+ /**
290
+ * Produce a human-readable diff of two tree-layout snapshots. Returns
291
+ * an empty string if they match. Used by differential mode to surface
292
+ * cache bugs with enough context to debug them.
293
+ *
294
+ * @internal
295
+ */
296
+ export function diffLayouts(a, b) {
297
+ if (a.length !== b.length) {
298
+ return `tree length mismatch: cached has ${a.length} nodes, cold has ${b.length}`;
299
+ }
300
+ const fields = ['left', 'top', 'width', 'height', 'scrollWidth', 'scrollHeight'];
301
+ for (let i = 0; i < a.length; i++) {
302
+ const av = a[i];
303
+ const bv = b[i];
304
+ for (const f of fields) {
305
+ if (av[f] !== bv[f]) {
306
+ return `node[${i}].${f}: cached=${av[f]} cold=${bv[f]}`;
307
+ }
308
+ }
309
+ }
310
+ return '';
311
+ }
312
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/algorithm/cache.ts"],"names":[],"mappings":"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"}
@@ -6,6 +6,13 @@
6
6
  * 2. Recursively lay out children in floating-point coordinates.
7
7
  * 3. Round the whole tree to integer cells.
8
8
  * 4. Mark every node clean.
9
+ *
10
+ * When `PILATES_DIFFERENTIAL_LAYOUT=1` is set in the environment, the
11
+ * exported `calculateLayout` runs the algorithm twice — once normally
12
+ * (with caches active), then again with all caches cleared and every
13
+ * node forcibly re-dirtied — and asserts the two snapshots are byte
14
+ * identical. Used by the test suite (`pnpm test:differential`) to
15
+ * catch any cache-correctness regression as soon as it lands.
9
16
  */
10
17
  import type { Node } from '../node.js';
11
18
  export declare function calculateLayout(root: Node, availableWidth: number | undefined, availableHeight: number | undefined): void;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/algorithm/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAIvC,wBAAgB,eAAe,CAC7B,IAAI,EAAE,IAAI,EACV,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,eAAe,EAAE,MAAM,GAAG,SAAS,GAClC,IAAI,CAUN"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/algorithm/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAevC,wBAAgB,eAAe,CAC7B,IAAI,EAAE,IAAI,EACV,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,eAAe,EAAE,MAAM,GAAG,SAAS,GAClC,IAAI,CAsBN"}