@pilates/core 1.1.0 → 2.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.
Files changed (57) hide show
  1. package/dist/algorithm/cache.d.ts +7 -19
  2. package/dist/algorithm/cache.d.ts.map +1 -1
  3. package/dist/algorithm/cache.js +31 -27
  4. package/dist/algorithm/cache.js.map +1 -1
  5. package/dist/algorithm/index.js +22 -14
  6. package/dist/algorithm/index.js.map +1 -1
  7. package/dist/algorithm/round.d.ts.map +1 -1
  8. package/dist/algorithm/round.js +19 -19
  9. package/dist/algorithm/round.js.map +1 -1
  10. package/dist/algorithm/spineless/classify.d.ts +47 -0
  11. package/dist/algorithm/spineless/classify.d.ts.map +1 -0
  12. package/dist/algorithm/spineless/classify.js +109 -0
  13. package/dist/algorithm/spineless/classify.js.map +1 -0
  14. package/dist/algorithm/spineless/field-id-pool.d.ts +28 -0
  15. package/dist/algorithm/spineless/field-id-pool.d.ts.map +1 -0
  16. package/dist/algorithm/spineless/field-id-pool.js +35 -0
  17. package/dist/algorithm/spineless/field-id-pool.js.map +1 -0
  18. package/dist/algorithm/spineless/flex-grammar.d.ts.map +1 -1
  19. package/dist/algorithm/spineless/flex-grammar.js +194 -58
  20. package/dist/algorithm/spineless/flex-grammar.js.map +1 -1
  21. package/dist/algorithm/spineless/grammar.d.ts +6 -0
  22. package/dist/algorithm/spineless/grammar.d.ts.map +1 -1
  23. package/dist/algorithm/spineless/grammar.js +2 -1
  24. package/dist/algorithm/spineless/grammar.js.map +1 -1
  25. package/dist/algorithm/spineless/layout.d.ts +52 -15
  26. package/dist/algorithm/spineless/layout.d.ts.map +1 -1
  27. package/dist/algorithm/spineless/layout.js +450 -312
  28. package/dist/algorithm/spineless/layout.js.map +1 -1
  29. package/dist/algorithm/spineless/order-maintenance.d.ts +9 -0
  30. package/dist/algorithm/spineless/order-maintenance.d.ts.map +1 -1
  31. package/dist/algorithm/spineless/order-maintenance.js +6 -0
  32. package/dist/algorithm/spineless/order-maintenance.js.map +1 -1
  33. package/dist/algorithm/spineless/runtime.d.ts +61 -8
  34. package/dist/algorithm/spineless/runtime.d.ts.map +1 -1
  35. package/dist/algorithm/spineless/runtime.js +201 -50
  36. package/dist/algorithm/spineless/runtime.js.map +1 -1
  37. package/dist/algorithm/spineless/style-dirty.d.ts +8 -6
  38. package/dist/algorithm/spineless/style-dirty.d.ts.map +1 -1
  39. package/dist/algorithm/spineless/style-dirty.js +10 -11
  40. package/dist/algorithm/spineless/style-dirty.js.map +1 -1
  41. package/dist/dirty-flags.d.ts +30 -0
  42. package/dist/dirty-flags.d.ts.map +1 -0
  43. package/dist/dirty-flags.js +35 -0
  44. package/dist/dirty-flags.js.map +1 -0
  45. package/dist/index.d.ts +2 -1
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +4 -1
  48. package/dist/index.js.map +1 -1
  49. package/dist/layout-pool.d.ts +49 -0
  50. package/dist/layout-pool.d.ts.map +1 -0
  51. package/dist/layout-pool.js +75 -0
  52. package/dist/layout-pool.js.map +1 -0
  53. package/dist/node.d.ts +20 -3
  54. package/dist/node.d.ts.map +1 -1
  55. package/dist/node.js +63 -42
  56. package/dist/node.js.map +1 -1
  57. package/package.json +1 -1
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Monotonic integer ID allocator for Spineless `Field` objects.
3
+ * Phase 15I indexes the runtime's typed-array storage by field.id.
4
+ *
5
+ * IDs are never recycled — Fields are interned per (node, name) and
6
+ * live as long as their node. The max id is the live-field high-water
7
+ * mark, matching LayoutPool's accepted tradeoff.
8
+ *
9
+ * @internal
10
+ */
11
+ let _nextFieldId = 0;
12
+ /** Allocate a fresh unique integer id for a Field. */
13
+ export function allocateFieldId() {
14
+ return _nextFieldId++;
15
+ }
16
+ /** Current id count — the exclusive upper bound of allocated ids.
17
+ * Runtime typed arrays size to at least this. @internal */
18
+ export function fieldIdCount() {
19
+ return _nextFieldId;
20
+ }
21
+ /** Reset for tests. @internal
22
+ *
23
+ * WARNING: Only safe to call when no live `Field` objects exist
24
+ * anywhere — the `FIELD_REGISTRY` WeakMap in `grammar.ts` is NOT
25
+ * cleared, so any `Field` retained across this call still holds
26
+ * its old id. Newly-allocated fields will then collide with retained
27
+ * ids, aliasing entries in the runtime's typed-array storage. The
28
+ * standard test harness creates fresh `Node`s per test, so retained
29
+ * cross-test references are the only way to hit this; don't introduce
30
+ * them.
31
+ */
32
+ export function _resetFieldIdsForTesting() {
33
+ _nextFieldId = 0;
34
+ }
35
+ //# sourceMappingURL=field-id-pool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field-id-pool.js","sourceRoot":"","sources":["../../../src/algorithm/spineless/field-id-pool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,IAAI,YAAY,GAAG,CAAC,CAAC;AAErB,sDAAsD;AACtD,MAAM,UAAU,eAAe;IAC7B,OAAO,YAAY,EAAE,CAAC;AACxB,CAAC;AAED;4DAC4D;AAC5D,MAAM,UAAU,YAAY;IAC1B,OAAO,YAAY,CAAC;AACtB,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,wBAAwB;IACtC,YAAY,GAAG,CAAC,CAAC;AACnB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"flex-grammar.d.ts","sourceRoot":"","sources":["../../../src/algorithm/spineless/flex-grammar.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"flex-grammar.d.ts","sourceRoot":"","sources":["../../../src/algorithm/spineless/flex-grammar.ts"],"names":[],"mappings":"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"}
@@ -395,6 +395,43 @@ function makeEmitter(ctx) {
395
395
  }
396
396
  return f;
397
397
  }
398
+ /** Fold `minWidth`/`minHeight` (default 0) or `maxWidth`/`maxHeight`
399
+ * (default `undefined` → ∞). Returns a constant when at default;
400
+ * emits a leaf Field (via `minMaxInput`) otherwise. */
401
+ function foldMinMax(n, prop) {
402
+ const isMax = prop === 'maxWidth' || prop === 'maxHeight';
403
+ const raw = n.style[prop];
404
+ // Default sentinels MUST match nodeSig exactly (layout.ts):
405
+ // minWidth/minHeight default === 0
406
+ // maxWidth/maxHeight default === undefined
407
+ if (isMax ? raw === undefined : raw === 0) {
408
+ return { kind: 'const', value: isMax ? Number.POSITIVE_INFINITY : 0 };
409
+ }
410
+ return { kind: 'field', field: minMaxInput(n, prop) };
411
+ }
412
+ /** Fold one margin `edge` (default 0). Returns a constant when margin
413
+ * is 0; emits a leaf Field (via `marginInput`) otherwise. */
414
+ function foldMargin(n, edge) {
415
+ // Default sentinel MUST match nodeSig exactly (layout.ts):
416
+ // margin[edge] default === 0
417
+ if ((n.style.margin[edge] ?? 0) === 0)
418
+ return { kind: 'const', value: 0 };
419
+ return { kind: 'field', field: marginInput(n, edge) };
420
+ }
421
+ /** Read a FoldedInput inside a compute callback. */
422
+ function readFolded(fi, read) {
423
+ return fi.kind === 'field' ? read(fi.field) : fi.value;
424
+ }
425
+ /** Collect only the Field entries from a FoldedInput list. */
426
+ function foldedDeps(fis) {
427
+ const deps = [];
428
+ for (const fi of fis) {
429
+ if (fi.kind === 'field')
430
+ deps.push(fi.field);
431
+ }
432
+ return deps;
433
+ }
434
+ // ──────────────────────────────────────────────────────────────────────
398
435
  function visit(node, parent, indexInParent, priorSiblings) {
399
436
  const width = field(node, 'width');
400
437
  const height = field(node, 'height');
@@ -448,6 +485,7 @@ function makeEmitter(ctx) {
448
485
  const mainPosField = parentDirection === 'column' ? top : left;
449
486
  const crossPosField = parentDirection === 'column' ? left : top;
450
487
  const mainSizeName = parentDirection === 'column' ? 'height' : 'width';
488
+ const mainPosName = parentDirection === 'column' ? 'top' : 'left';
451
489
  // Spacing inputs for this child. Both the parent's padding and
452
490
  // this child's own margin are modelled as leaf input Fields (see
453
491
  // `paddingInput` / `marginInput`) — one per [top,right,bottom,
@@ -465,9 +503,42 @@ function makeEmitter(ctx) {
465
503
  const padMainEndF = parent === null ? null : paddingInput(parent, mainEndEdge(parentDirection));
466
504
  const padCrossStartF = parent === null ? null : paddingInput(parent, crossStartEdge(parentDirection));
467
505
  const padCrossEndF = parent === null ? null : paddingInput(parent, crossEndEdge(parentDirection));
468
- const myMarginMainStartF = parent === null ? null : marginInput(node, mainStartEdge(parentDirection));
469
- const myMarginCrossStartF = parent === null ? null : marginInput(node, crossStartEdge(parentDirection));
470
- const myMarginCrossEndF = parent === null ? null : marginInput(node, crossEndEdge(parentDirection));
506
+ // Phase 17: margin fields are resolved lazily at each rule site via
507
+ // foldMargin() (fold-eligible rules) or marginInput() (always-needed).
508
+ // Pre-compute edge indices once to avoid repeating edge-name calls.
509
+ const mainStartEdgeIdx = parent === null ? -1 : mainStartEdge(parentDirection);
510
+ const crossStartEdgeIdx = parent === null ? -1 : crossStartEdge(parentDirection);
511
+ const crossEndEdgeIdx = parent === null ? -1 : crossEndEdge(parentDirection);
512
+ // Rules that always require the full tracked Field (e.g. stretch
513
+ // crossSizeField, flex-end/center crossPos, reverse cumulative-sum)
514
+ // use these pre-resolved fields so the idempotent marginInput is called
515
+ // at most once per edge.
516
+ // NOTE: calling marginInput here forces Field creation for these edges
517
+ // regardless of whether they end up in a fold path. This is acceptable
518
+ // because the rules that read them (stretch/flex-end/center/reverse) are
519
+ // not fold-eligible — they always need tracked incremental propagation.
520
+ // For nodes on fully fold-eligible paths (flex-start, no-stretch, no-
521
+ // reverse), the Field is created if the code-path reaches it below;
522
+ // the fold-eligible rules that invoke foldMargin() bypass this and
523
+ // create no Field when margin is at default.
524
+ let myMarginMainStartF = null;
525
+ let myMarginCrossStartF = null;
526
+ let myMarginCrossEndF = null;
527
+ function getMarginMainStart() {
528
+ if (myMarginMainStartF === null)
529
+ myMarginMainStartF = marginInput(node, mainStartEdgeIdx);
530
+ return myMarginMainStartF;
531
+ }
532
+ function getMarginCrossStart() {
533
+ if (myMarginCrossStartF === null)
534
+ myMarginCrossStartF = marginInput(node, crossStartEdgeIdx);
535
+ return myMarginCrossStartF;
536
+ }
537
+ function getMarginCrossEnd() {
538
+ if (myMarginCrossEndF === null)
539
+ myMarginCrossEndF = marginInput(node, crossEndEdgeIdx);
540
+ return myMarginCrossEndF;
541
+ }
471
542
  // Alignment for this child: justify-content lives on the parent,
472
543
  // applies along the main axis once per line. align-items lives
473
544
  // on the parent; align-self overrides per child (with 'auto'
@@ -492,24 +563,29 @@ function makeEmitter(ctx) {
492
563
  const crossKey = parentDirection === 'column' ? 'width' : 'height';
493
564
  const crossIsContentAuto = typeof node.style[crossKey] !== 'number' && !aspectDerivable(node, crossKey);
494
565
  const crossSizeInput = preferredSizeInput(node, crossKey, 'cross', parent);
495
- const minCrossInput = minMaxInput(node, crossKey === 'width' ? 'minWidth' : 'minHeight');
496
- const maxCrossInput = minMaxInput(node, crossKey === 'width' ? 'maxWidth' : 'maxHeight');
566
+ // Phase 17: fold min/max cross inputs when at default (0 / undefined→∞).
567
+ const fMinCross = foldMinMax(node, crossKey === 'width' ? 'minWidth' : 'minHeight');
568
+ const fMaxCross = foldMinMax(node, crossKey === 'width' ? 'maxWidth' : 'maxHeight');
497
569
  if (crossIsContentAuto && parent !== null && align === 'stretch') {
570
+ // Stretch: resize to fill the parent's inner cross minus margins.
571
+ // The margins cannot be folded here (they appear in a subtraction
572
+ // expression), so use the always-needed field accessors.
573
+ const mcs = getMarginCrossStart();
574
+ const mce = getMarginCrossEnd();
498
575
  const parentCrossF = field(parent, crossKey);
499
576
  grammar.set(crossSizeField, {
500
577
  deps: [
501
578
  parentCrossF,
502
579
  padCrossStartF,
503
580
  padCrossEndF,
504
- myMarginCrossStartF,
505
- myMarginCrossEndF,
506
- minCrossInput,
507
- maxCrossInput,
581
+ mcs,
582
+ mce,
583
+ ...foldedDeps([fMinCross, fMaxCross]),
508
584
  ],
509
585
  compute: (read) => {
510
586
  const innerCross = Math.max(0, read(parentCrossF) - read(padCrossStartF) - read(padCrossEndF));
511
- const lineInner = innerCross - read(myMarginCrossStartF) - read(myMarginCrossEndF);
512
- return clampMinMax(Math.max(0, lineInner), read(minCrossInput), read(maxCrossInput));
587
+ const lineInner = innerCross - read(mcs) - read(mce);
588
+ return clampMinMax(Math.max(0, lineInner), readFolded(fMinCross, read), readFolded(fMaxCross, read));
513
589
  },
514
590
  });
515
591
  }
@@ -522,13 +598,10 @@ function makeEmitter(ctx) {
522
598
  });
523
599
  }
524
600
  else {
601
+ // Phase 17: build deps only from non-default inputs.
525
602
  grammar.set(crossSizeField, {
526
- deps: [
527
- crossSizeInput,
528
- minCrossInput,
529
- maxCrossInput,
530
- ],
531
- compute: (read) => clampMinMax(read(crossSizeInput), read(minCrossInput), read(maxCrossInput)),
603
+ deps: [crossSizeInput, ...foldedDeps([fMinCross, fMaxCross])],
604
+ compute: (read) => clampMinMax(read(crossSizeInput), readFolded(fMinCross, read), readFolded(fMaxCross, read)),
532
605
  });
533
606
  }
534
607
  // When the parent has flex-wrap='wrap', all three position fields
@@ -670,8 +743,9 @@ function makeEmitter(ctx) {
670
743
  // never consults it — so the root's main size is its preferred
671
744
  // size directly, no `flexBasis` short-circuit.
672
745
  const mainInput = preferredSizeInput(node, mainSizeName, 'main', parent);
673
- const minMainInput = minMaxInput(node, mainSizeName === 'width' ? 'minWidth' : 'minHeight');
674
- const maxMainInput = minMaxInput(node, mainSizeName === 'width' ? 'maxWidth' : 'maxHeight');
746
+ // Phase 17: fold min/max main inputs when at default (0 / undefined→∞).
747
+ const fMinMain = foldMinMax(node, mainSizeName === 'width' ? 'minWidth' : 'minHeight');
748
+ const fMaxMain = foldMinMax(node, mainSizeName === 'width' ? 'maxWidth' : 'maxHeight');
675
749
  if (parent === null) {
676
750
  if (rootAxisIsBareZero(node, mainSizeName)) {
677
751
  // `'auto'` root, no `available` → bare 0, unclamped.
@@ -681,28 +755,38 @@ function makeEmitter(ctx) {
681
755
  });
682
756
  }
683
757
  else {
758
+ // Phase 17: deps only include non-default min/max.
759
+ grammar.set(mainSizeField, {
760
+ deps: [mainInput, ...foldedDeps([fMinMain, fMaxMain])],
761
+ compute: (read) => clampMinMax(read(mainInput), readFolded(fMinMain, read), readFolded(fMaxMain, read)),
762
+ });
763
+ }
764
+ }
765
+ else {
766
+ // Phase 17: flexBasis 'auto' is folded — when flexBasis === 'auto',
767
+ // resolveBasisFromRead degenerates to read(mainInput). nodeSig already
768
+ // captures typeof s.flexBasis (the 'auto' vs numeric boundary), so a
769
+ // change from 'auto' to a numeric basis triggers a full rebuild.
770
+ const flexBasisIsAuto = node.style.flexBasis === 'auto';
771
+ if (flexBasisIsAuto) {
772
+ // Degenerate form: basis auto → just use mainInput (no flexBasisInput field).
773
+ grammar.set(mainSizeField, {
774
+ deps: [mainInput, ...foldedDeps([fMinMain, fMaxMain])],
775
+ compute: (read) => clampMinMax(read(mainInput), readFolded(fMinMain, read), readFolded(fMaxMain, read)),
776
+ });
777
+ }
778
+ else {
779
+ const flexBasisInput = styleSizeInput(node, 'flexBasis');
684
780
  grammar.set(mainSizeField, {
685
781
  deps: [
782
+ flexBasisInput,
686
783
  mainInput,
687
- minMainInput,
688
- maxMainInput,
784
+ ...foldedDeps([fMinMain, fMaxMain]),
689
785
  ],
690
- compute: (read) => clampMinMax(read(mainInput), read(minMainInput), read(maxMainInput)),
786
+ compute: (read) => clampMinMax(resolveBasisFromRead(read, flexBasisInput, mainInput), readFolded(fMinMain, read), readFolded(fMaxMain, read)),
691
787
  });
692
788
  }
693
789
  }
694
- else {
695
- const flexBasisInput = styleSizeInput(node, 'flexBasis');
696
- grammar.set(mainSizeField, {
697
- deps: [
698
- flexBasisInput,
699
- mainInput,
700
- minMainInput,
701
- maxMainInput,
702
- ],
703
- compute: (read) => clampMinMax(resolveBasisFromRead(read, flexBasisInput, mainInput), read(minMainInput), read(maxMainInput)),
704
- });
705
- }
706
790
  }
707
791
  else {
708
792
  // Flex distribution. Capture the in-flow siblings + this
@@ -815,9 +899,11 @@ function makeEmitter(ctx) {
815
899
  });
816
900
  }
817
901
  else {
902
+ // Phase 17: fold myMarginMainStart when at default (0).
903
+ const fMyMarginMainStart = foldMargin(node, mainStartEdgeIdx);
818
904
  grammar.set(mainPosField, {
819
- deps: [padMainStartF, myMarginMainStartF],
820
- compute: (read) => read(padMainStartF) + read(myMarginMainStartF),
905
+ deps: [padMainStartF, ...foldedDeps([fMyMarginMainStart])],
906
+ compute: (read) => read(padMainStartF) + readFolded(fMyMarginMainStart, read),
821
907
  });
822
908
  }
823
909
  }
@@ -840,25 +926,63 @@ function makeEmitter(ctx) {
840
926
  compute: (read) => read(parentMainDist).positions[myIndexCapture],
841
927
  });
842
928
  }
929
+ else if (!parentReverse) {
930
+ // Phase 16: linear recurrence. This child's main position is the
931
+ // immediate predecessor's position + its box + one gap. O(1) deps
932
+ // regardless of sibling count (was O(N) cumulative-sum). Unrolls
933
+ // to the same total — see the Phase 16 design doc.
934
+ // Note: only applies to forward directions; reverse directions use
935
+ // the cumulative-sum below because applyReverseMainPos overwrites
936
+ // each sibling's mainPosField with a reflected value, breaking the
937
+ // chain (the predecessor's field holds its reflected position, not
938
+ // the forward cursor this recurrence relies on).
939
+ const prevSibling = priorSiblings[priorSiblings.length - 1];
940
+ const prevMainPos = field(prevSibling, mainPosName);
941
+ const prevMainSize = field(prevSibling, mainSizeName);
942
+ const mainEndEdgeIdx = mainEndEdge(parentDirection);
943
+ // Phase 17: fold prevMarginEnd and myMarginMainStart when at default (0).
944
+ const fPrevMarginEnd = foldMargin(prevSibling, mainEndEdgeIdx);
945
+ const fMyMarginMainStart = foldMargin(node, mainStartEdgeIdx);
946
+ const mainGapInput = gapInput(parent, parentDirection === 'column' ? 'row' : 'column');
947
+ grammar.set(mainPosField, {
948
+ deps: [
949
+ prevMainPos,
950
+ prevMainSize,
951
+ ...foldedDeps([fPrevMarginEnd, fMyMarginMainStart]),
952
+ mainGapInput,
953
+ ],
954
+ compute: (read) => read(prevMainPos) +
955
+ read(prevMainSize) +
956
+ readFolded(fPrevMarginEnd, read) +
957
+ readFolded(fMyMarginMainStart, read) +
958
+ read(mainGapInput),
959
+ });
960
+ }
843
961
  else {
844
- // Non-qualifying regime (wrap): keep today's prior-siblings-sum
845
- // rule size / spacing mutation on any prior sibling propagates here.
962
+ // Reverse direction: keep the cumulative-sum rule. The recurrence
963
+ // cannot chain through the predecessor's mainPosField here because
964
+ // applyReverseMainPos (applied per-sibling below) overwrites that
965
+ // field with a reflected value — the predecessor's field no longer
966
+ // carries the forward cursor the recurrence depends on.
846
967
  const priorMainSizes = priorSiblings.map((s) => field(s, mainSizeName));
847
968
  const priorMargins = priorSiblings.map((s) => ({
848
969
  start: marginInput(s, mainStartEdge(parentDirection)),
849
970
  end: marginInput(s, mainEndEdge(parentDirection)),
850
971
  }));
851
972
  const mainGapInput = gapInput(parent, parentDirection === 'column' ? 'row' : 'column');
973
+ // Reverse cumulative-sum: cannot fold margins — the expression
974
+ // accumulates all sibling margins and this node's own marginMainStart.
975
+ const myMMS = getMarginMainStart();
852
976
  grammar.set(mainPosField, {
853
977
  deps: [
854
978
  mainGapInput,
855
979
  padMainStartF,
856
- myMarginMainStartF,
980
+ myMMS,
857
981
  ...priorMainSizes,
858
982
  ...priorMargins.flatMap((m) => [m.start, m.end]),
859
983
  ],
860
984
  compute: (read) => {
861
- let sum = read(padMainStartF) + read(myMarginMainStartF) + indexInParent * read(mainGapInput);
985
+ let sum = read(padMainStartF) + read(myMMS) + indexInParent * read(mainGapInput);
862
986
  for (const m of priorMargins)
863
987
  sum += read(m.start) + read(m.end);
864
988
  for (const m of priorMainSizes)
@@ -885,13 +1009,14 @@ function makeEmitter(ctx) {
885
1009
  if (parent === null || align === 'flex-end') {
886
1010
  if (parent !== null && align === 'flex-end') {
887
1011
  const parentCrossField = field(parent, parentDirection === 'column' ? 'width' : 'height');
1012
+ const mce = getMarginCrossEnd();
888
1013
  grammar.set(crossPosField, {
889
1014
  deps: [
890
1015
  parentCrossField,
891
1016
  crossSizeField,
892
1017
  padCrossStartF,
893
1018
  padCrossEndF,
894
- myMarginCrossEndF,
1019
+ mce,
895
1020
  ],
896
1021
  // Anchor against the line's inner cross, which the
897
1022
  // imperative `crossAlignItemsInLine` clamps to >= 0 — a
@@ -900,10 +1025,7 @@ function makeEmitter(ctx) {
900
1025
  compute: (read) => {
901
1026
  const padStart = read(padCrossStartF);
902
1027
  const innerCross = Math.max(0, read(parentCrossField) - padStart - read(padCrossEndF));
903
- return (padStart +
904
- innerCross -
905
- read(crossSizeField) -
906
- read(myMarginCrossEndF));
1028
+ return padStart + innerCross - read(crossSizeField) - read(mce);
907
1029
  },
908
1030
  });
909
1031
  }
@@ -917,20 +1039,22 @@ function makeEmitter(ctx) {
917
1039
  }
918
1040
  else if (align === 'center') {
919
1041
  const parentCrossField = field(parent, parentDirection === 'column' ? 'width' : 'height');
1042
+ const mcs2 = getMarginCrossStart();
1043
+ const mce2 = getMarginCrossEnd();
920
1044
  grammar.set(crossPosField, {
921
1045
  deps: [
922
1046
  parentCrossField,
923
1047
  crossSizeField,
924
1048
  padCrossStartF,
925
1049
  padCrossEndF,
926
- myMarginCrossStartF,
927
- myMarginCrossEndF,
1050
+ mcs2,
1051
+ mce2,
928
1052
  ],
929
1053
  compute: (read) => {
930
1054
  const padStart = read(padCrossStartF);
931
1055
  const innerCross = Math.max(0, read(parentCrossField) - padStart - read(padCrossEndF));
932
- const marginStart = read(myMarginCrossStartF);
933
- const innerLine = innerCross - marginStart - read(myMarginCrossEndF);
1056
+ const marginStart = read(mcs2);
1057
+ const innerLine = innerCross - marginStart - read(mce2);
934
1058
  const myCross = read(crossSizeField);
935
1059
  return padStart + marginStart + Math.max(0, (innerLine - myCross) / 2);
936
1060
  },
@@ -940,11 +1064,13 @@ function makeEmitter(ctx) {
940
1064
  // flex-start, stretch (with explicit cross size — no resize),
941
1065
  // and any other value (the imperative falls through to
942
1066
  // flex-start) all share this offset: the parent's cross-start
943
- // padding plus this child's cross-start margin, both declared
944
- // input deps.
1067
+ // padding plus this child's cross-start margin.
1068
+ // Phase 17: fold myMarginCrossStart when at default (0) — no Field
1069
+ // created, constant 0 inlined.
1070
+ const fMyMarginCrossStart = foldMargin(node, crossStartEdgeIdx);
945
1071
  grammar.set(crossPosField, {
946
- deps: [padCrossStartF, myMarginCrossStartF],
947
- compute: (read) => read(padCrossStartF) + read(myMarginCrossStartF),
1072
+ deps: [padCrossStartF, ...foldedDeps([fMyMarginCrossStart])],
1073
+ compute: (read) => read(padCrossStartF) + readFolded(fMyMarginCrossStart, read),
948
1074
  });
949
1075
  }
950
1076
  allFields.push({ node, width, height, left, top });
@@ -1049,8 +1175,11 @@ function mergeStyleInputs(a, b) {
1049
1175
  * which gains a main-end margin input when it acquires a follower —
1050
1176
  * has its `StyleInputs` deep-merged rather than overwritten.
1051
1177
  */
1052
- function mergeStyleInputsMap(base, extra) {
1053
- const merged = new Map(base);
1178
+ function mergeStyleInputsMap(base, extra, mutateBase) {
1179
+ // When mutateBase is true the caller owns `base` exclusively (it is
1180
+ // the previous grammar output, swapped out by the caller right after
1181
+ // this returns) — mutate it directly and skip the ~1,100-entry clone.
1182
+ const merged = mutateBase ? base : new Map(base);
1054
1183
  for (const [node, entry] of extra) {
1055
1184
  const existing = merged.get(node);
1056
1185
  merged.set(node, existing === undefined ? entry : mergeStyleInputs(existing, entry));
@@ -1151,7 +1280,7 @@ export function buildAppendFragment(prev, root, parent, child, available = {}) {
1151
1280
  grammar: prev.grammar,
1152
1281
  rootFields: prev.rootFields,
1153
1282
  allFields: [...prev.allFields, ...ctx.allFields],
1154
- styleInputs: mergeStyleInputsMap(prev.styleInputs, ctx.styleInputs),
1283
+ styleInputs: mergeStyleInputsMap(prev.styleInputs, ctx.styleInputs, true),
1155
1284
  availableInputs: prev.availableInputs,
1156
1285
  mainDistributionByParent: prev.mainDistributionByParent,
1157
1286
  };
@@ -1337,13 +1466,20 @@ export function buildRemoveFragment(prev, root, parent, child, available = {}) {
1337
1466
  removed.push(f);
1338
1467
  }
1339
1468
  }
1469
+ // Mutate prev.styleInputs and prev.mainDistributionByParent in place:
1470
+ // `prev` is the old grammar output, single-use — the caller swaps
1471
+ // built.output to the new fragment immediately after this returns.
1472
+ for (const n of removedNodes) {
1473
+ prev.styleInputs.delete(n);
1474
+ prev.mainDistributionByParent.delete(n);
1475
+ }
1340
1476
  const next = {
1341
1477
  grammar: prev.grammar,
1342
1478
  rootFields: prev.rootFields,
1343
1479
  allFields: prev.allFields.filter((e) => !removedNodes.has(e.node)),
1344
- styleInputs: new Map([...prev.styleInputs].filter(([n]) => !removedNodes.has(n))),
1480
+ styleInputs: prev.styleInputs,
1345
1481
  availableInputs: prev.availableInputs,
1346
- mainDistributionByParent: new Map([...prev.mainDistributionByParent].filter(([n]) => !removedNodes.has(n))),
1482
+ mainDistributionByParent: prev.mainDistributionByParent,
1347
1483
  };
1348
1484
  return { removed, rebinds: [], next };
1349
1485
  }