@grahlnn/comps 0.1.8 → 0.1.9

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/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  // torph/src/components/Torph.tsx
2
2
  import {
3
- useLayoutEffect,
3
+ useLayoutEffect as useLayoutEffect2,
4
4
  useMemo,
5
- useRef,
6
- useState
5
+ useRef as useRef2,
6
+ useState as useState2
7
7
  } from "react";
8
8
 
9
9
  // torph/src/vendor/pretext/bidi.ts
@@ -2516,8 +2516,12 @@ function measureMorphSnapshotWithPretext(text, layoutContext) {
2516
2516
  };
2517
2517
  }
2518
2518
 
2519
- // torph/src/components/Torph.tsx
2520
- import { jsxDEV } from "react/jsx-dev-runtime";
2519
+ // torph/src/core/math.ts
2520
+ function nearlyEqual(a, b, epsilon = 0.5) {
2521
+ return Math.abs(a - b) <= epsilon;
2522
+ }
2523
+
2524
+ // torph/src/core/types.ts
2521
2525
  var MORPH = {
2522
2526
  durationMs: 280,
2523
2527
  maxFadeMs: 150,
@@ -2526,8 +2530,6 @@ var MORPH = {
2526
2530
  contentWidthLockEpsilon: 2,
2527
2531
  lineGroupingEpsilon: 1
2528
2532
  };
2529
- var MORPH_SEGMENT_CACHE_LIMIT = 256;
2530
- var DOM_MEASUREMENT_SNAPSHOT_CACHE_LIMIT = 8;
2531
2533
  var EMPTY_STATE = {
2532
2534
  stage: "idle",
2533
2535
  measurement: null,
@@ -2548,22 +2550,19 @@ var ZERO_BRIDGE = {
2548
2550
  offsetY: 0
2549
2551
  };
2550
2552
  var EMPTY_SEGMENTS = [];
2551
- var morphSegmentCache = new Map;
2552
- var pretextMorphTrustCache = new Map;
2553
- var morphMeasurementEpoch = 1;
2554
- var activeMorphMeasurementConsumers = 0;
2555
- var detachMorphMeasurementInvalidationListeners = null;
2556
- var SCREEN_READER_ONLY_STYLE = {
2553
+ function resolveContentWidthLockInlineSize(layoutHint) {
2554
+ if (layoutHint.flowInlineSize !== null) {
2555
+ return layoutHint.flowInlineSize;
2556
+ }
2557
+ return layoutHint.snapshot.width;
2558
+ }
2559
+
2560
+ // torph/src/core/render.ts
2561
+ var OVERLAY_STYLE = {
2557
2562
  position: "absolute",
2558
- width: "1px",
2559
- height: "1px",
2560
- margin: "-1px",
2561
- padding: 0,
2562
- border: 0,
2563
- clip: "rect(0 0 0 0)",
2564
- clipPath: "inset(50%)",
2565
- overflow: "hidden",
2566
- whiteSpace: "nowrap"
2563
+ inset: 0,
2564
+ minWidth: 0,
2565
+ pointerEvents: "none"
2567
2566
  };
2568
2567
  var MEASUREMENT_LAYER_STYLE = {
2569
2568
  pointerEvents: "none",
@@ -2574,299 +2573,708 @@ var MEASUREMENT_LAYER_STYLE = {
2574
2573
  right: 0,
2575
2574
  display: "block"
2576
2575
  };
2577
- var FALLBACK_TEXT_STYLE = {
2578
- display: "block",
2579
- gridArea: "1 / 1"
2580
- };
2581
- var OVERLAY_STYLE = {
2582
- position: "absolute",
2583
- inset: 0,
2584
- minWidth: 0,
2585
- pointerEvents: "none"
2586
- };
2587
- var SHARED_GLYPH_TYPOGRAPHY_STYLE = {
2588
- font: "inherit",
2589
- fontKerning: "inherit",
2590
- fontFeatureSettings: "inherit",
2591
- fontOpticalSizing: "inherit",
2592
- fontStretch: "inherit",
2593
- fontStyle: "inherit",
2594
- fontVariant: "inherit",
2595
- fontVariantNumeric: "inherit",
2596
- fontVariationSettings: "inherit",
2597
- fontWeight: "inherit",
2598
- letterSpacing: "inherit",
2599
- textTransform: "inherit",
2600
- wordSpacing: "inherit",
2601
- direction: "inherit"
2602
- };
2603
- var ABSOLUTE_GLYPH_STYLE = {
2604
- position: "absolute",
2605
- display: "block",
2606
- overflow: "hidden",
2607
- transformOrigin: "left top"
2608
- };
2609
- var CONTEXT_SLICE_TEXT_STYLE = {
2610
- ...SHARED_GLYPH_TYPOGRAPHY_STYLE,
2611
- position: "absolute",
2612
- display: "block",
2613
- minWidth: 0,
2614
- whiteSpace: "inherit"
2615
- };
2616
- var graphemeSegmenter = null;
2617
- var domMeasurementService = null;
2618
- var torphDebugInstanceOrdinal = 0;
2619
- var TORPH_TRACE_MAX_BYTES = 4 * 1024 * 1024;
2620
- var TORPH_TRACE_MAX_LINES = 4000;
2621
- var TORPH_TRACE_SCHEMA_VERSION = 2;
2622
- function nextTorphDebugInstanceId() {
2623
- torphDebugInstanceOrdinal += 1;
2624
- return torphDebugInstanceOrdinal;
2625
- }
2626
- function readTorphDebugConfig() {
2627
- const scope = globalThis;
2628
- return scope.__TORPH_DEBUG__ ?? null;
2576
+ function getFadeDuration(fraction) {
2577
+ return Math.min(MORPH.durationMs * fraction, MORPH.maxFadeMs);
2629
2578
  }
2630
- function shouldCaptureTorphTrace(config) {
2631
- if (config === null) {
2632
- return true;
2579
+ function getOverlayStyle(stage, plan) {
2580
+ if (stage === "idle") {
2581
+ return OVERLAY_STYLE;
2633
2582
  }
2634
- if (typeof config === "boolean") {
2635
- return config;
2583
+ return {
2584
+ ...OVERLAY_STYLE,
2585
+ right: "auto",
2586
+ bottom: "auto",
2587
+ width: plan.frameWidth,
2588
+ height: plan.frameHeight
2589
+ };
2590
+ }
2591
+ function getLiveTransform(item, stage, visualBridge) {
2592
+ if (stage !== "prepare") {
2593
+ return "translate(0px, 0px)";
2636
2594
  }
2637
- if (config.capture === false) {
2638
- return false;
2595
+ if (item.kind === "move") {
2596
+ return `translate(${(item.fromLeft ?? item.left) - item.left + visualBridge.offsetX}px, ${(item.fromTop ?? item.top) - item.top + visualBridge.offsetY}px)`;
2639
2597
  }
2640
- return true;
2598
+ return `translate(${visualBridge.offsetX}px, ${visualBridge.offsetY}px)`;
2641
2599
  }
2642
- function isTorphDebugEnabled(config) {
2643
- if (config === null) {
2644
- return false;
2600
+ function getLiveTransition(item, stage) {
2601
+ if (stage !== "animate") {
2602
+ return;
2645
2603
  }
2646
- if (typeof config === "boolean") {
2647
- return config;
2604
+ if (item.kind === "enter") {
2605
+ return `opacity ${getFadeDuration(0.5)}ms linear ${getFadeDuration(0.25)}ms`;
2648
2606
  }
2649
- if (config.console !== undefined) {
2650
- return config.console;
2607
+ return `transform ${MORPH.durationMs}ms ${MORPH.ease}, opacity ${getFadeDuration(0.25)}ms linear`;
2608
+ }
2609
+ function getExitTransform(visualBridge) {
2610
+ return `translate(${visualBridge.offsetX}px, ${visualBridge.offsetY}px)`;
2611
+ }
2612
+ function getExitTransition(stage) {
2613
+ if (stage !== "animate") {
2614
+ return;
2651
2615
  }
2652
- return config.enabled !== false;
2616
+ return `transform ${MORPH.durationMs}ms ${MORPH.ease}, opacity ${getFadeDuration(0.25)}ms linear`;
2653
2617
  }
2654
- function shouldRunTorphInstrumentation(config) {
2655
- if (shouldCaptureTorphTrace(config)) {
2618
+ function supportsIntrinsicWidthLock(display, parentDisplay) {
2619
+ let parentNeedsReservation = false;
2620
+ if (parentDisplay === "flex" || parentDisplay === "inline-flex" || parentDisplay === "grid" || parentDisplay === "inline-grid") {
2621
+ parentNeedsReservation = true;
2622
+ }
2623
+ if (display === "inline" || display === "inline-block" || display === "inline-flex" || display === "inline-grid") {
2656
2624
  return true;
2657
2625
  }
2658
- return isTorphDebugEnabled(config);
2626
+ return parentNeedsReservation;
2659
2627
  }
2660
- function getTorphTraceStore() {
2661
- const scope = globalThis;
2662
- let store = scope.__TORPH_TRACE_STORE__;
2663
- if (store !== undefined) {
2664
- return store;
2628
+ function getRootDisplay(layoutContext) {
2629
+ if (layoutContext === null) {
2630
+ return "grid";
2665
2631
  }
2666
- store = {
2667
- lines: [],
2668
- nextSeq: 1,
2669
- totalBytes: 0
2670
- };
2671
- scope.__TORPH_TRACE_STORE__ = store;
2672
- return store;
2673
- }
2674
- function getTorphTraceText() {
2675
- return getTorphTraceStore().lines.join("");
2676
- }
2677
- function clearTorphTrace() {
2678
- const store = getTorphTraceStore();
2679
- store.lines = [];
2680
- store.nextSeq = 1;
2681
- store.totalBytes = 0;
2632
+ if (supportsIntrinsicWidthLock(layoutContext.display, layoutContext.parentDisplay)) {
2633
+ return "inline-grid";
2634
+ }
2635
+ return "grid";
2682
2636
  }
2683
- function downloadTorphTrace(filename) {
2684
- if (typeof document === "undefined") {
2685
- return null;
2637
+ function getRootStyle(stage, plan, measurement, layoutContext) {
2638
+ let width = measurement?.reservedInlineSize;
2639
+ if (measurement !== null && measurement.flowInlineSize !== null) {
2640
+ width = measurement.layoutInlineSize;
2686
2641
  }
2687
- const text = getTorphTraceText();
2688
- const blob = new Blob([text], { type: "application/x-ndjson;charset=utf-8" });
2689
- const href = URL.createObjectURL(blob);
2690
- const anchor = document.createElement("a");
2691
- let resolvedFilename = filename;
2692
- if (resolvedFilename === undefined) {
2693
- resolvedFilename = `torph-trace-${new Date().toISOString().replaceAll(":", "-")}.jsonl`;
2642
+ if (plan !== null) {
2643
+ width = plan.layoutInlineSizeTo;
2694
2644
  }
2695
- anchor.href = href;
2696
- anchor.download = resolvedFilename;
2697
- anchor.click();
2698
- window.setTimeout(() => {
2699
- URL.revokeObjectURL(href);
2700
- }, 0);
2701
- return resolvedFilename;
2702
- }
2703
- function ensureTorphTraceApi() {
2704
- const scope = globalThis;
2705
- if (scope.__TORPH_TRACE__ !== undefined) {
2706
- return scope.__TORPH_TRACE__;
2645
+ let height;
2646
+ if (plan !== null) {
2647
+ height = plan.frameHeight;
2707
2648
  }
2708
- const api = {
2709
- clear: clearTorphTrace,
2710
- count: () => getTorphTraceStore().lines.length,
2711
- download: downloadTorphTrace,
2712
- text: getTorphTraceText
2649
+ const style = {
2650
+ position: "relative",
2651
+ display: getRootDisplay(layoutContext)
2713
2652
  };
2714
- scope.__TORPH_TRACE__ = api;
2715
- return api;
2653
+ if (width !== null && width !== undefined) {
2654
+ style.width = width;
2655
+ }
2656
+ if (height !== undefined) {
2657
+ style.height = height;
2658
+ }
2659
+ return style;
2716
2660
  }
2717
- function appendTorphTrace(instanceId, event, payload) {
2718
- ensureTorphTraceApi();
2719
- const store = getTorphTraceStore();
2720
- const entry = {
2721
- instanceId,
2722
- event,
2723
- payload,
2724
- seq: store.nextSeq,
2725
- time: new Date().toISOString()
2726
- };
2727
- store.nextSeq += 1;
2728
- const line = `${JSON.stringify(entry)}
2729
- `;
2730
- store.lines.push(line);
2731
- store.totalBytes += line.length;
2732
- while (store.lines.length > TORPH_TRACE_MAX_LINES || store.totalBytes > TORPH_TRACE_MAX_BYTES) {
2733
- const removed = store.lines.shift();
2734
- if (removed === undefined) {
2735
- break;
2661
+ function getMeasurementLayerStyle(layoutContext, useContentInlineSize = false) {
2662
+ let intrinsicWidthLock = false;
2663
+ if (layoutContext !== null) {
2664
+ if (useContentInlineSize) {
2665
+ intrinsicWidthLock = true;
2666
+ } else if (supportsIntrinsicWidthLock(layoutContext.display, layoutContext.parentDisplay)) {
2667
+ intrinsicWidthLock = true;
2736
2668
  }
2737
- store.totalBytes = Math.max(0, store.totalBytes - removed.length);
2738
2669
  }
2670
+ if (!intrinsicWidthLock) {
2671
+ return MEASUREMENT_LAYER_STYLE;
2672
+ }
2673
+ return {
2674
+ ...MEASUREMENT_LAYER_STYLE,
2675
+ right: "auto",
2676
+ width: "max-content"
2677
+ };
2739
2678
  }
2740
- function roundDebugValue(value) {
2741
- if (value === null || value === undefined) {
2742
- return value;
2679
+ function resolveFlowText(committedMeasurement, stateMeasurement, text) {
2680
+ return stateMeasurement?.snapshot.text ?? committedMeasurement?.snapshot.text ?? text;
2681
+ }
2682
+ function shouldRenderGlyphLayer(stage, plan, measurement) {
2683
+ if (stage === "idle") {
2684
+ return measurement !== null;
2743
2685
  }
2744
- return Math.round(value * 100) / 100;
2686
+ return plan !== null;
2745
2687
  }
2746
- function summarizeDebugSnapshot(snapshot) {
2688
+ function resolveGlyphSliceWhiteSpace(snapshot) {
2747
2689
  if (snapshot === null) {
2748
- return null;
2690
+ return "inherit";
2749
2691
  }
2692
+ return "nowrap";
2693
+ }
2694
+ function toSteadyLiveItem(grapheme) {
2750
2695
  return {
2751
- text: snapshot.text,
2752
- renderText: snapshot.renderText,
2753
- width: roundDebugValue(snapshot.width),
2754
- height: roundDebugValue(snapshot.height),
2755
- graphemes: snapshot.graphemes.length
2696
+ ...grapheme,
2697
+ kind: "move",
2698
+ fromLeft: grapheme.left,
2699
+ fromTop: grapheme.top
2756
2700
  };
2757
2701
  }
2758
- function summarizeDebugMeasurement(measurement) {
2759
- if (measurement === null) {
2760
- return null;
2761
- }
2702
+ function createSteadyGlyphPlan(measurement) {
2703
+ const snapshot = measurement.snapshot;
2762
2704
  return {
2763
- layoutInlineSize: roundDebugValue(measurement.layoutInlineSize),
2764
- reservedInlineSize: roundDebugValue(measurement.reservedInlineSize),
2765
- flowInlineSize: roundDebugValue(measurement.flowInlineSize),
2766
- rootOrigin: {
2767
- left: roundDebugValue(measurement.rootOrigin.left),
2768
- top: roundDebugValue(measurement.rootOrigin.top)
2769
- },
2770
- snapshot: summarizeDebugSnapshot(measurement.snapshot)
2705
+ frameWidth: snapshot.width,
2706
+ frameHeight: snapshot.height,
2707
+ layoutInlineSizeFrom: measurement.layoutInlineSize,
2708
+ layoutInlineSizeTo: measurement.layoutInlineSize,
2709
+ sourceRenderText: snapshot.renderText,
2710
+ targetRenderText: snapshot.renderText,
2711
+ sourceRootOrigin: measurement.rootOrigin,
2712
+ visualBridge: ZERO_BRIDGE,
2713
+ liveItems: snapshot.graphemes.map(toSteadyLiveItem),
2714
+ exitItems: []
2771
2715
  };
2772
2716
  }
2773
- function summarizeDebugRect(rect) {
2774
- if (rect === null) {
2717
+
2718
+ // torph/src/core/snapshot.ts
2719
+ function isSingleLineSnapshot(snapshot) {
2720
+ if (snapshot.graphemes.length <= 1) {
2721
+ return true;
2722
+ }
2723
+ const firstTop = snapshot.graphemes[0].top;
2724
+ return snapshot.graphemes.every((grapheme) => nearlyEqual(grapheme.top, firstTop, MORPH.lineGroupingEpsilon));
2725
+ }
2726
+ function assertSingleLineSnapshot(snapshot) {
2727
+ if (isSingleLineSnapshot(snapshot)) {
2728
+ return snapshot;
2729
+ }
2730
+ throw new Error(`Torph only supports single-line text layout. Received wrapped text: "${snapshot.renderText}"`);
2731
+ }
2732
+
2733
+ // torph/src/core/measurement-policy.ts
2734
+ var MORPH_SEGMENT_CACHE_LIMIT = 256;
2735
+ var morphSegmentCache = new Map;
2736
+ var pretextMorphTrustCache = new Map;
2737
+ var morphMeasurementEpoch = 1;
2738
+ var graphemeSegmenter = null;
2739
+ function getGraphemeSegmenter() {
2740
+ if (graphemeSegmenter !== null) {
2741
+ return graphemeSegmenter;
2742
+ }
2743
+ graphemeSegmenter = new Intl.Segmenter(undefined, {
2744
+ granularity: "grapheme"
2745
+ });
2746
+ return graphemeSegmenter;
2747
+ }
2748
+ function clearPretextMorphTrustCache() {
2749
+ pretextMorphTrustCache.clear();
2750
+ }
2751
+ function clearMorphMeasurementCaches() {
2752
+ morphSegmentCache.clear();
2753
+ clearPretextMorphTrustCache();
2754
+ clearPretextMorphCaches();
2755
+ }
2756
+ function bumpMorphMeasurementEpoch() {
2757
+ morphMeasurementEpoch += 1;
2758
+ }
2759
+ function getMorphMeasurementEpoch() {
2760
+ return morphMeasurementEpoch;
2761
+ }
2762
+ function readCachedMorphSegments(text) {
2763
+ const cached = morphSegmentCache.get(text);
2764
+ if (cached !== undefined) {
2765
+ morphSegmentCache.delete(text);
2766
+ morphSegmentCache.set(text, cached);
2767
+ return cached;
2768
+ }
2769
+ const segments = Array.from(getGraphemeSegmenter().segment(text), (segment, index) => ({
2770
+ glyph: segment.segment,
2771
+ key: `${segment.segment}:${index}`
2772
+ }));
2773
+ morphSegmentCache.set(text, segments);
2774
+ if (morphSegmentCache.size > MORPH_SEGMENT_CACHE_LIMIT) {
2775
+ const oldest = morphSegmentCache.keys().next();
2776
+ if (!oldest.done) {
2777
+ morphSegmentCache.delete(oldest.value);
2778
+ }
2779
+ }
2780
+ return segments;
2781
+ }
2782
+ function shouldMeasureUsingContentInlineSize(layoutContext, layoutHint) {
2783
+ if (supportsIntrinsicWidthLock(layoutContext.display, layoutContext.parentDisplay)) {
2784
+ return true;
2785
+ }
2786
+ if (layoutContext.whiteSpace === "nowrap") {
2787
+ return true;
2788
+ }
2789
+ if (layoutHint === null) {
2790
+ return false;
2791
+ }
2792
+ if (!isSingleLineSnapshot(layoutHint.snapshot)) {
2793
+ return false;
2794
+ }
2795
+ return nearlyEqual(layoutHint.layoutInlineSize, resolveContentWidthLockInlineSize(layoutHint), MORPH.contentWidthLockEpsilon);
2796
+ }
2797
+ function getTrustedPretextMeasurementBackend(text, renderText, layoutContext, useContentInlineSize) {
2798
+ const backend = getPretextMorphMeasurementBackend(text, layoutContext);
2799
+ if (backend !== "probe") {
2800
+ return backend;
2801
+ }
2802
+ const signature = getPretextMorphTrustSignature({
2803
+ renderText,
2804
+ layoutContext,
2805
+ useContentInlineSize
2806
+ });
2807
+ if (signature === null) {
2808
+ return "dom";
2809
+ }
2810
+ const trusted = pretextMorphTrustCache.get(signature);
2811
+ if (trusted === undefined) {
2812
+ return "probe";
2813
+ }
2814
+ if (trusted) {
2815
+ return "pretext";
2816
+ }
2817
+ return "dom";
2818
+ }
2819
+ function getDomMeasurementRequestKey(text, renderText, layoutContext, useContentInlineSize) {
2820
+ let inlineSizeMode = "container";
2821
+ if (useContentInlineSize) {
2822
+ inlineSizeMode = "content";
2823
+ }
2824
+ return `dom\x00${inlineSizeMode}\x00${text}\x00${renderText}\x00${layoutContext.measurementVersion}\x00${getMorphMeasurementEpoch()}`;
2825
+ }
2826
+ function needsMeasurementLayer(measurementBackend, renderText) {
2827
+ if (measurementBackend === "pretext") {
2828
+ return false;
2829
+ }
2830
+ return renderText.length > 0;
2831
+ }
2832
+ function canCacheMeasurementLayerSnapshot(measurementBackend) {
2833
+ return measurementBackend === "dom";
2834
+ }
2835
+ function createMorphMeasurementRequest({
2836
+ text,
2837
+ layoutContext,
2838
+ layoutHint
2839
+ }) {
2840
+ if (layoutContext === null) {
2775
2841
  return null;
2776
2842
  }
2843
+ const renderText = getPretextMorphRenderedText(text, layoutContext);
2844
+ const useContentInlineSize = shouldMeasureUsingContentInlineSize(layoutContext, layoutHint);
2845
+ const measurementBackend = getTrustedPretextMeasurementBackend(text, renderText, layoutContext, useContentInlineSize);
2846
+ let segments = readCachedMorphSegments(renderText);
2847
+ if (measurementBackend === "pretext") {
2848
+ segments = EMPTY_SEGMENTS;
2849
+ }
2850
+ let domMeasurementKey = null;
2851
+ if (needsMeasurementLayer(measurementBackend, renderText)) {
2852
+ domMeasurementKey = getDomMeasurementRequestKey(text, renderText, layoutContext, useContentInlineSize);
2853
+ }
2777
2854
  return {
2778
- left: roundDebugValue(rect.left),
2779
- top: roundDebugValue(rect.top),
2780
- width: roundDebugValue(rect.width),
2781
- height: roundDebugValue(rect.height)
2855
+ text,
2856
+ renderText,
2857
+ segments,
2858
+ measurementBackend,
2859
+ useContentInlineSize,
2860
+ domMeasurementKey
2782
2861
  };
2783
2862
  }
2784
- function collectDebugAnchorIndices(length) {
2785
- const indices = new Set;
2786
- if (length <= 0) {
2787
- return [];
2863
+ function rememberPretextMeasurementTrust({
2864
+ renderText,
2865
+ layoutContext,
2866
+ useContentInlineSize,
2867
+ trusted
2868
+ }) {
2869
+ const signature = getPretextMorphTrustSignature({
2870
+ renderText,
2871
+ layoutContext,
2872
+ useContentInlineSize
2873
+ });
2874
+ if (signature === null) {
2875
+ return;
2788
2876
  }
2789
- indices.add(0);
2790
- if (length > 1) {
2791
- indices.add(1);
2792
- indices.add(length - 2);
2877
+ pretextMorphTrustCache.set(signature, trusted);
2878
+ }
2879
+ function areSnapshotsEquivalentForPretextTrust(left, right) {
2880
+ if (left.renderText !== right.renderText || left.graphemes.length !== right.graphemes.length) {
2881
+ return false;
2793
2882
  }
2794
- if (length > 2) {
2795
- indices.add(Math.floor((length - 1) / 2));
2883
+ if (Math.abs(left.width - right.width) > MORPH.geometryEpsilon) {
2884
+ return false;
2796
2885
  }
2797
- indices.add(length - 1);
2798
- return Array.from(indices).sort((left, right) => left - right);
2886
+ if (Math.abs(left.height - right.height) > MORPH.geometryEpsilon) {
2887
+ return false;
2888
+ }
2889
+ for (let index = 0;index < left.graphemes.length; index += 1) {
2890
+ const from = left.graphemes[index];
2891
+ const to = right.graphemes[index];
2892
+ if (from.glyph !== to.glyph) {
2893
+ return false;
2894
+ }
2895
+ if (Math.abs(from.left - to.left) > MORPH.geometryEpsilon) {
2896
+ return false;
2897
+ }
2898
+ if (Math.abs(from.top - to.top) > MORPH.geometryEpsilon) {
2899
+ return false;
2900
+ }
2901
+ if (Math.abs(from.width - to.width) > MORPH.geometryEpsilon) {
2902
+ return false;
2903
+ }
2904
+ if (Math.abs(from.height - to.height) > MORPH.geometryEpsilon) {
2905
+ return false;
2906
+ }
2907
+ }
2908
+ return true;
2799
2909
  }
2800
- function summarizeDebugViewportAnchors(snapshot, rootRect) {
2801
- if (snapshot === null || rootRect === null) {
2802
- return null;
2910
+
2911
+ // torph/src/core/finalize-barrier.ts
2912
+ function createMorphFinalizeBarrier(hasMoveTransitions) {
2913
+ return {
2914
+ waitForLiveTransform: hasMoveTransitions,
2915
+ sawLiveTransform: false
2916
+ };
2917
+ }
2918
+ function recordMorphFinalizeSignal(barrier, signal) {
2919
+ return {
2920
+ ...barrier,
2921
+ sawLiveTransform: signal === "live-transform"
2922
+ };
2923
+ }
2924
+ function isMorphFinalizeBarrierSatisfied(barrier) {
2925
+ if (barrier.waitForLiveTransform && !barrier.sawLiveTransform) {
2926
+ return false;
2803
2927
  }
2804
- const anchors = [];
2805
- const anchorIndices = collectDebugAnchorIndices(snapshot.graphemes.length);
2806
- for (const index of anchorIndices) {
2807
- const grapheme = snapshot.graphemes[index];
2808
- if (grapheme === undefined) {
2928
+ return true;
2929
+ }
2930
+ function summarizeMorphFinalizeBarrier(barrier) {
2931
+ return {
2932
+ waitForLiveTransform: barrier.waitForLiveTransform,
2933
+ sawLiveTransform: barrier.sawLiveTransform,
2934
+ satisfied: isMorphFinalizeBarrierSatisfied(barrier)
2935
+ };
2936
+ }
2937
+
2938
+ // torph/src/core/session.ts
2939
+ function bucketByGlyph(graphemes) {
2940
+ const buckets = new Map;
2941
+ for (const grapheme of graphemes) {
2942
+ const bucket = buckets.get(grapheme.glyph);
2943
+ if (bucket !== undefined) {
2944
+ bucket.push(grapheme);
2809
2945
  continue;
2810
2946
  }
2811
- anchors.push({
2812
- index,
2813
- glyph: grapheme.glyph,
2814
- left: roundDebugValue(rootRect.left + grapheme.left),
2815
- top: roundDebugValue(rootRect.top + grapheme.top),
2816
- width: roundDebugValue(grapheme.width),
2817
- height: roundDebugValue(grapheme.height)
2818
- });
2947
+ buckets.set(grapheme.glyph, [grapheme]);
2819
2948
  }
2820
- return anchors;
2949
+ return buckets;
2950
+ }
2951
+ function pairMorphCharacters(previous, next) {
2952
+ const previousBuckets = bucketByGlyph(previous);
2953
+ const nextBuckets = bucketByGlyph(next);
2954
+ const pairings = [];
2955
+ for (const [glyph, previousItems] of previousBuckets) {
2956
+ const nextItems = nextBuckets.get(glyph) ?? [];
2957
+ const shared = Math.min(previousItems.length, nextItems.length);
2958
+ for (let index = 0;index < shared; index += 1) {
2959
+ pairings.push({
2960
+ kind: "move",
2961
+ from: previousItems[index],
2962
+ to: nextItems[index]
2963
+ });
2964
+ }
2965
+ for (let index = shared;index < previousItems.length; index += 1) {
2966
+ pairings.push({
2967
+ kind: "exit",
2968
+ from: previousItems[index]
2969
+ });
2970
+ }
2971
+ }
2972
+ for (const [glyph, nextItems] of nextBuckets) {
2973
+ const previousItems = previousBuckets.get(glyph) ?? [];
2974
+ const shared = Math.min(previousItems.length, nextItems.length);
2975
+ for (let index = shared;index < nextItems.length; index += 1) {
2976
+ pairings.push({
2977
+ kind: "enter",
2978
+ to: nextItems[index]
2979
+ });
2980
+ }
2981
+ }
2982
+ return pairings;
2983
+ }
2984
+ function resolveMorphFrameBounds(previous, next) {
2985
+ return {
2986
+ width: Math.max(previous.width, next.width),
2987
+ height: Math.max(previous.height, next.height)
2988
+ };
2989
+ }
2990
+ function buildMorphPlan(previous, next, visualBridge = ZERO_BRIDGE) {
2991
+ const pairings = pairMorphCharacters(previous.snapshot.graphemes, next.snapshot.graphemes);
2992
+ const movesByDestinationKey = new Map;
2993
+ const exitItems = [];
2994
+ for (const pairing of pairings) {
2995
+ if (pairing.kind === "move") {
2996
+ movesByDestinationKey.set(pairing.to.key, pairing);
2997
+ continue;
2998
+ }
2999
+ if (pairing.kind === "exit") {
3000
+ exitItems.push(pairing.from);
3001
+ }
3002
+ }
3003
+ const frame = resolveMorphFrameBounds(previous.snapshot, next.snapshot);
3004
+ return {
3005
+ frameWidth: frame.width,
3006
+ frameHeight: frame.height,
3007
+ layoutInlineSizeFrom: previous.layoutInlineSize,
3008
+ layoutInlineSizeTo: next.layoutInlineSize,
3009
+ sourceRenderText: previous.snapshot.renderText,
3010
+ targetRenderText: next.snapshot.renderText,
3011
+ sourceRootOrigin: previous.rootOrigin,
3012
+ visualBridge,
3013
+ liveItems: next.snapshot.graphemes.map((grapheme) => {
3014
+ const move = movesByDestinationKey.get(grapheme.key);
3015
+ if (move !== undefined) {
3016
+ return {
3017
+ ...grapheme,
3018
+ kind: "move",
3019
+ fromLeft: move.from.left,
3020
+ fromTop: move.from.top
3021
+ };
3022
+ }
3023
+ return {
3024
+ ...grapheme,
3025
+ kind: "enter",
3026
+ fromLeft: null,
3027
+ fromTop: null
3028
+ };
3029
+ }),
3030
+ exitItems
3031
+ };
3032
+ }
3033
+ function sameSnapshot(a, b) {
3034
+ if (a === b) {
3035
+ return true;
3036
+ }
3037
+ if (a.text !== b.text || a.renderText !== b.renderText || a.graphemes.length !== b.graphemes.length) {
3038
+ return false;
3039
+ }
3040
+ if (!nearlyEqual(a.width, b.width, MORPH.geometryEpsilon)) {
3041
+ return false;
3042
+ }
3043
+ if (!nearlyEqual(a.height, b.height, MORPH.geometryEpsilon)) {
3044
+ return false;
3045
+ }
3046
+ for (let index = 0;index < a.graphemes.length; index += 1) {
3047
+ const left = a.graphemes[index];
3048
+ const right = b.graphemes[index];
3049
+ if (left.glyph !== right.glyph || left.key !== right.key) {
3050
+ return false;
3051
+ }
3052
+ if (!nearlyEqual(left.left, right.left, MORPH.geometryEpsilon)) {
3053
+ return false;
3054
+ }
3055
+ if (!nearlyEqual(left.top, right.top, MORPH.geometryEpsilon)) {
3056
+ return false;
3057
+ }
3058
+ if (!nearlyEqual(left.width, right.width, MORPH.geometryEpsilon)) {
3059
+ return false;
3060
+ }
3061
+ if (!nearlyEqual(left.height, right.height, MORPH.geometryEpsilon)) {
3062
+ return false;
3063
+ }
3064
+ }
3065
+ return true;
3066
+ }
3067
+ function sameMeasurement(a, b) {
3068
+ const sameReservedInlineSize = a.reservedInlineSize === null && b.reservedInlineSize === null || a.reservedInlineSize !== null && b.reservedInlineSize !== null && nearlyEqual(a.reservedInlineSize, b.reservedInlineSize, MORPH.geometryEpsilon);
3069
+ const sameFlowInlineSize = a.flowInlineSize === null && b.flowInlineSize === null || a.flowInlineSize !== null && b.flowInlineSize !== null && nearlyEqual(a.flowInlineSize, b.flowInlineSize, MORPH.geometryEpsilon);
3070
+ return sameSnapshot(a.snapshot, b.snapshot) && nearlyEqual(a.layoutInlineSize, b.layoutInlineSize, MORPH.geometryEpsilon) && sameReservedInlineSize && sameFlowInlineSize && nearlyEqual(a.rootOrigin.left, b.rootOrigin.left, MORPH.geometryEpsilon) && nearlyEqual(a.rootOrigin.top, b.rootOrigin.top, MORPH.geometryEpsilon);
3071
+ }
3072
+ function selectMorphLayoutHint(session) {
3073
+ if (session.animating && session.target !== null) {
3074
+ return session.target;
3075
+ }
3076
+ return session.committed;
3077
+ }
3078
+ function decideMorphSessionUpdate({
3079
+ committed,
3080
+ target,
3081
+ animating,
3082
+ nextMeasurement,
3083
+ fontsReady
3084
+ }) {
3085
+ let source = committed;
3086
+ if (animating && target !== null) {
3087
+ source = target;
3088
+ }
3089
+ if (source === null) {
3090
+ return {
3091
+ kind: "commit-static",
3092
+ measurement: nextMeasurement
3093
+ };
3094
+ }
3095
+ if (!fontsReady) {
3096
+ return {
3097
+ kind: "commit-static",
3098
+ measurement: nextMeasurement
3099
+ };
3100
+ }
3101
+ if (animating && target !== null) {
3102
+ if (nextMeasurement.snapshot.renderText === target.snapshot.renderText) {
3103
+ return {
3104
+ kind: "freeze-animating-target",
3105
+ target
3106
+ };
3107
+ }
3108
+ }
3109
+ if (committed !== null) {
3110
+ if (committed.snapshot.renderText === nextMeasurement.snapshot.renderText) {
3111
+ if (sameMeasurement(committed, nextMeasurement)) {
3112
+ return {
3113
+ kind: "commit-static",
3114
+ measurement: committed
3115
+ };
3116
+ }
3117
+ return {
3118
+ kind: "commit-static",
3119
+ measurement: nextMeasurement
3120
+ };
3121
+ }
3122
+ }
3123
+ return {
3124
+ kind: "start-morph",
3125
+ source,
3126
+ target: nextMeasurement
3127
+ };
2821
3128
  }
2822
- function summarizeDebugRootOriginDrift(measurement, rootRect) {
2823
- if (measurement === null || rootRect === null) {
2824
- return null;
3129
+ function pinMeasurementToCurrentOrigin(measurement, origin) {
3130
+ if (nearlyEqual(measurement.rootOrigin.left, origin.left, MORPH.geometryEpsilon) && nearlyEqual(measurement.rootOrigin.top, origin.top, MORPH.geometryEpsilon)) {
3131
+ return measurement;
2825
3132
  }
2826
3133
  return {
2827
- expectedLeft: roundDebugValue(measurement.rootOrigin.left),
2828
- expectedTop: roundDebugValue(measurement.rootOrigin.top),
2829
- actualLeft: roundDebugValue(rootRect.left),
2830
- actualTop: roundDebugValue(rootRect.top),
2831
- deltaLeft: roundDebugValue(rootRect.left - measurement.rootOrigin.left),
2832
- deltaTop: roundDebugValue(rootRect.top - measurement.rootOrigin.top)
3134
+ snapshot: measurement.snapshot,
3135
+ layoutInlineSize: measurement.layoutInlineSize,
3136
+ reservedInlineSize: measurement.reservedInlineSize,
3137
+ flowInlineSize: measurement.flowInlineSize,
3138
+ rootOrigin: origin
2833
3139
  };
2834
3140
  }
2835
- function summarizeSnapshotDrift(drift) {
3141
+ function createStaticState(measurement) {
2836
3142
  return {
2837
- comparedGlyphs: drift.comparedGlyphs,
2838
- expectedGlyphs: drift.expectedGlyphs,
2839
- actualGlyphs: drift.actualGlyphs,
2840
- snapshotWidthDelta: roundDebugValue(drift.snapshotWidthDelta),
2841
- snapshotHeightDelta: roundDebugValue(drift.snapshotHeightDelta),
2842
- maxAbsLeftDelta: roundDebugValue(drift.maxAbsLeftDelta),
2843
- maxAbsTopDelta: roundDebugValue(drift.maxAbsTopDelta),
2844
- maxAbsWidthDelta: roundDebugValue(drift.maxAbsWidthDelta),
2845
- maxAbsHeightDelta: roundDebugValue(drift.maxAbsHeightDelta),
2846
- mismatches: drift.mismatches.map((mismatch) => ({
2847
- index: mismatch.index,
2848
- glyph: mismatch.glyph,
2849
- leftDelta: roundDebugValue(mismatch.leftDelta),
2850
- topDelta: roundDebugValue(mismatch.topDelta),
2851
- widthDelta: roundDebugValue(mismatch.widthDelta),
2852
- heightDelta: roundDebugValue(mismatch.heightDelta)
2853
- }))
3143
+ stage: "idle",
3144
+ measurement,
3145
+ plan: null
2854
3146
  };
2855
3147
  }
2856
- function logTorphDebug(instanceId, event, payload) {
2857
- const config = readTorphDebugConfig();
2858
- const captureTrace = shouldCaptureTorphTrace(config);
2859
- const logToConsole = isTorphDebugEnabled(config);
2860
- if (!captureTrace && !logToConsole) {
3148
+ function areFontsReady() {
3149
+ return document.fonts.status === "loaded";
3150
+ }
3151
+ function cancelTimeline(timeline) {
3152
+ if (timeline.prepareFrame !== null) {
3153
+ cancelAnimationFrame(timeline.prepareFrame);
3154
+ timeline.prepareFrame = null;
3155
+ }
3156
+ if (timeline.animateFrame !== null) {
3157
+ cancelAnimationFrame(timeline.animateFrame);
3158
+ timeline.animateFrame = null;
3159
+ }
3160
+ if (timeline.finalizeTimer !== null) {
3161
+ window.clearTimeout(timeline.finalizeTimer);
3162
+ timeline.finalizeTimer = null;
3163
+ }
3164
+ }
3165
+ function resetMorph(session, timeline, setState) {
3166
+ cancelTimeline(timeline);
3167
+ session.committed = null;
3168
+ session.target = null;
3169
+ session.animating = false;
3170
+ setState(EMPTY_STATE);
3171
+ }
3172
+ function commitStaticMeasurement(session, measurement, setState) {
3173
+ if (!session.animating && session.target === null && session.committed !== null && sameMeasurement(session.committed, measurement)) {
2861
3174
  return;
2862
3175
  }
2863
- if (captureTrace) {
2864
- appendTorphTrace(instanceId, event, payload);
3176
+ session.committed = measurement;
3177
+ session.target = null;
3178
+ session.animating = false;
3179
+ setState((current) => {
3180
+ if (current.stage === "idle" && current.plan === null && current.measurement !== null && sameMeasurement(current.measurement, measurement)) {
3181
+ return current;
3182
+ }
3183
+ return createStaticState(measurement);
3184
+ });
3185
+ }
3186
+ function applyMorphSessionDecision({
3187
+ decision,
3188
+ session,
3189
+ timeline,
3190
+ setState
3191
+ }) {
3192
+ if (decision.kind === "freeze-animating-target") {
3193
+ return decision.target;
2865
3194
  }
2866
- if (logToConsole) {
2867
- console.log(`[Torph#${instanceId}] ${event}`, payload);
3195
+ cancelTimeline(timeline);
3196
+ if (decision.kind === "commit-static") {
3197
+ commitStaticMeasurement(session, decision.measurement, setState);
3198
+ return decision.measurement;
3199
+ }
3200
+ startMorph({
3201
+ source: decision.source,
3202
+ target: decision.target,
3203
+ session,
3204
+ timeline,
3205
+ setState
3206
+ });
3207
+ return decision.target;
3208
+ }
3209
+ function reconcileMorphSessionUpdate({
3210
+ session,
3211
+ timeline,
3212
+ nextMeasurement,
3213
+ fontsReady,
3214
+ setState
3215
+ }) {
3216
+ const decision = decideMorphSessionUpdate({
3217
+ committed: session.committed,
3218
+ target: session.target,
3219
+ animating: session.animating,
3220
+ nextMeasurement,
3221
+ fontsReady
3222
+ });
3223
+ return applyMorphSessionDecision({
3224
+ decision,
3225
+ session,
3226
+ timeline,
3227
+ setState
3228
+ });
3229
+ }
3230
+ function finalizeMorphTransition({
3231
+ session,
3232
+ timeline,
3233
+ measurement,
3234
+ setState
3235
+ }) {
3236
+ cancelTimeline(timeline);
3237
+ commitStaticMeasurement(session, session.target ?? measurement, setState);
3238
+ }
3239
+ function resolvePreparedMeasurementOrigin(measurement, origin) {
3240
+ return pinMeasurementToCurrentOrigin(measurement, origin);
3241
+ }
3242
+ function resolvePreparedPlanVisualBridge(plan, origin) {
3243
+ const offsetX = plan.sourceRootOrigin.left - origin.left;
3244
+ const offsetY = plan.sourceRootOrigin.top - origin.top;
3245
+ if (nearlyEqual(plan.visualBridge.offsetX, offsetX, MORPH.geometryEpsilon) && nearlyEqual(plan.visualBridge.offsetY, offsetY, MORPH.geometryEpsilon)) {
3246
+ return plan;
2868
3247
  }
3248
+ return {
3249
+ ...plan,
3250
+ visualBridge: {
3251
+ offsetX,
3252
+ offsetY
3253
+ }
3254
+ };
3255
+ }
3256
+ function startMorph({
3257
+ source,
3258
+ target,
3259
+ session,
3260
+ timeline,
3261
+ setState
3262
+ }) {
3263
+ const plan = buildMorphPlan(source, target, ZERO_BRIDGE);
3264
+ session.target = target;
3265
+ session.animating = true;
3266
+ setState({
3267
+ stage: "prepare",
3268
+ measurement: target,
3269
+ plan
3270
+ });
2869
3271
  }
3272
+
3273
+ // torph/src/core/layout-observer.ts
3274
+ import { useLayoutEffect, useRef, useState } from "react";
3275
+ var activeMorphMeasurementConsumers = 0;
3276
+ var detachMorphMeasurementInvalidationListeners = null;
3277
+ var morphMeasurementInvalidationSubscribers = new Set;
2870
3278
  function parsePx(value) {
2871
3279
  const parsed = Number.parseFloat(value);
2872
3280
  if (Number.isFinite(parsed)) {
@@ -2933,41 +3341,16 @@ function sameLayoutContext(a, b) {
2933
3341
  }
2934
3342
  return a.display === b.display && a.direction === b.direction && a.font === b.font && a.fontFeatureSettings === b.fontFeatureSettings && a.fontVariationSettings === b.fontVariationSettings && Math.abs(a.letterSpacingPx - b.letterSpacingPx) < MORPH.geometryEpsilon && Math.abs(a.lineHeightPx - b.lineHeightPx) < MORPH.geometryEpsilon && a.parentDisplay === b.parentDisplay && a.textTransform === b.textTransform && a.whiteSpace === b.whiteSpace && Math.abs(a.width - b.width) < MORPH.geometryEpsilon && Math.abs(a.wordSpacingPx - b.wordSpacingPx) < MORPH.geometryEpsilon;
2935
3343
  }
2936
- function clearPretextMorphTrustCache() {
2937
- pretextMorphTrustCache.clear();
2938
- }
2939
- function clearMorphMeasurementCaches() {
2940
- morphSegmentCache.clear();
2941
- clearPretextMorphTrustCache();
2942
- clearPretextMorphCaches();
2943
- }
2944
- function bumpMorphMeasurementEpoch() {
2945
- morphMeasurementEpoch += 1;
2946
- }
2947
- function getMorphMeasurementEpoch() {
2948
- return morphMeasurementEpoch;
2949
- }
2950
- function isSingleLineSnapshot(snapshot) {
2951
- if (snapshot.graphemes.length <= 1) {
2952
- return true;
3344
+ function notifyMorphMeasurementInvalidationSubscribers() {
3345
+ for (const subscriber of morphMeasurementInvalidationSubscribers) {
3346
+ subscriber();
2953
3347
  }
2954
- const firstTop = snapshot.graphemes[0].top;
2955
- return snapshot.graphemes.every((grapheme) => nearlyEqual(grapheme.top, firstTop, MORPH.lineGroupingEpsilon));
2956
3348
  }
2957
- function countSnapshotLines(snapshot) {
2958
- if (snapshot.graphemes.length === 0) {
2959
- return 0;
2960
- }
2961
- let lineCount = 1;
2962
- let currentTop = snapshot.graphemes[0].top;
2963
- for (let index = 1;index < snapshot.graphemes.length; index += 1) {
2964
- const top = snapshot.graphemes[index].top;
2965
- if (!nearlyEqual(top, currentTop, MORPH.lineGroupingEpsilon)) {
2966
- currentTop = top;
2967
- lineCount += 1;
2968
- }
2969
- }
2970
- return lineCount;
3349
+ function subscribeMorphMeasurementInvalidation(subscriber) {
3350
+ morphMeasurementInvalidationSubscribers.add(subscriber);
3351
+ return () => {
3352
+ morphMeasurementInvalidationSubscribers.delete(subscriber);
3353
+ };
2971
3354
  }
2972
3355
  function acquireMorphMeasurementInvalidationListeners() {
2973
3356
  activeMorphMeasurementConsumers += 1;
@@ -2975,6 +3358,7 @@ function acquireMorphMeasurementInvalidationListeners() {
2975
3358
  const handleFontChange = () => {
2976
3359
  clearMorphMeasurementCaches();
2977
3360
  bumpMorphMeasurementEpoch();
3361
+ notifyMorphMeasurementInvalidationSubscribers();
2978
3362
  };
2979
3363
  document.fonts.ready.then(handleFontChange);
2980
3364
  if (typeof document.fonts.addEventListener === "function") {
@@ -2997,6 +3381,7 @@ function releaseMorphMeasurementInvalidationListeners() {
2997
3381
  function useObservedLayoutContext(deps) {
2998
3382
  const ref = useRef(null);
2999
3383
  const [layoutContext, setLayoutContext] = useState(null);
3384
+ const syncLayoutContextRef = useRef(null);
3000
3385
  useLayoutEffect(() => {
3001
3386
  const node = ref.current;
3002
3387
  if (node === null) {
@@ -3025,9 +3410,15 @@ function useObservedLayoutContext(deps) {
3025
3410
  const next = readLayoutContext(node, width);
3026
3411
  commitLayoutContext(next, refreshMeasurements);
3027
3412
  };
3413
+ syncLayoutContextRef.current = syncLayoutContext;
3028
3414
  const initialLayoutContext = readLayoutContext(node);
3029
3415
  const shouldObserveWrappingWidth = initialLayoutContext.whiteSpace !== "nowrap" && !supportsIntrinsicWidthLock(initialLayoutContext.display, initialLayoutContext.parentDisplay);
3030
3416
  commitLayoutContext(initialLayoutContext, true);
3417
+ const unsubscribeInvalidation = subscribeMorphMeasurementInvalidation(() => {
3418
+ syncLayoutContext({
3419
+ refreshMeasurements: true
3420
+ });
3421
+ });
3031
3422
  let resizeObserver = null;
3032
3423
  if (shouldObserveWrappingWidth) {
3033
3424
  resizeObserver = new ResizeObserver(([entry]) => {
@@ -3040,24 +3431,27 @@ function useObservedLayoutContext(deps) {
3040
3431
  acquireMorphMeasurementInvalidationListeners();
3041
3432
  return () => {
3042
3433
  disposed = true;
3434
+ syncLayoutContextRef.current = null;
3435
+ unsubscribeInvalidation();
3043
3436
  resizeObserver?.disconnect();
3044
3437
  releaseMorphMeasurementInvalidationListeners();
3045
3438
  };
3046
3439
  }, deps);
3047
3440
  return { ref, layoutContext };
3048
3441
  }
3049
- function getSegmenter() {
3050
- if (graphemeSegmenter !== null) {
3051
- return graphemeSegmenter;
3442
+
3443
+ // torph/src/core/dom-measurement.ts
3444
+ var domMeasurementService = null;
3445
+ function readFirstTextNode(node) {
3446
+ if (node === null) {
3447
+ return null;
3052
3448
  }
3053
- const segmenterConstructor = Intl.Segmenter;
3054
- if (segmenterConstructor === undefined) {
3055
- throw new Error("Torph requires Intl.Segmenter for grapheme-safe pairing.");
3449
+ for (const childNode of node.childNodes) {
3450
+ if (childNode.nodeType === Node.TEXT_NODE) {
3451
+ return childNode;
3452
+ }
3056
3453
  }
3057
- graphemeSegmenter = new segmenterConstructor(undefined, {
3058
- granularity: "grapheme"
3059
- });
3060
- return graphemeSegmenter;
3454
+ return null;
3061
3455
  }
3062
3456
  function getDomMeasurementService() {
3063
3457
  if (domMeasurementService !== null) {
@@ -3129,51 +3523,6 @@ function applyMeasurementHostStyle({
3129
3523
  host.style.width = "max-content";
3130
3524
  }
3131
3525
  }
3132
- function segmentTorphText(text) {
3133
- const segments = [];
3134
- let ordinal = 0;
3135
- for (const segment of getSegmenter().segment(text)) {
3136
- segments.push({
3137
- glyph: segment.segment,
3138
- key: `${segment.segment}:${ordinal}`
3139
- });
3140
- ordinal += 1;
3141
- }
3142
- return segments;
3143
- }
3144
- function readCachedMorphSegments(text) {
3145
- const cached = morphSegmentCache.get(text);
3146
- if (cached !== undefined) {
3147
- morphSegmentCache.delete(text);
3148
- morphSegmentCache.set(text, cached);
3149
- return cached;
3150
- }
3151
- const segments = segmentTorphText(text);
3152
- morphSegmentCache.set(text, segments);
3153
- if (morphSegmentCache.size > MORPH_SEGMENT_CACHE_LIMIT) {
3154
- const oldest = morphSegmentCache.keys().next();
3155
- if (!oldest.done) {
3156
- morphSegmentCache.delete(oldest.value);
3157
- }
3158
- }
3159
- return segments;
3160
- }
3161
- function getDomMeasurementRequestKey(text, renderText, layoutContext, useContentInlineSize) {
3162
- let inlineSizeMode = "container";
3163
- if (useContentInlineSize) {
3164
- inlineSizeMode = "content";
3165
- }
3166
- return `dom\x00${inlineSizeMode}\x00${text}\x00${renderText}\x00${layoutContext.measurementVersion}\x00${getMorphMeasurementEpoch()}`;
3167
- }
3168
- function needsMeasurementLayer(measurementBackend, renderText) {
3169
- if (measurementBackend === "pretext") {
3170
- return false;
3171
- }
3172
- return renderText.length > 0;
3173
- }
3174
- function canCacheMeasurementLayerSnapshot(measurementBackend) {
3175
- return measurementBackend === "dom";
3176
- }
3177
3526
  function readCachedMorphSnapshot(cache, cacheKey) {
3178
3527
  const cached = cache.get(cacheKey);
3179
3528
  if (cached === undefined) {
@@ -3186,7 +3535,7 @@ function readCachedMorphSnapshot(cache, cacheKey) {
3186
3535
  function rememberCachedMorphSnapshot(cache, cacheKey, snapshot) {
3187
3536
  cache.delete(cacheKey);
3188
3537
  cache.set(cacheKey, snapshot);
3189
- if (cache.size > DOM_MEASUREMENT_SNAPSHOT_CACHE_LIMIT) {
3538
+ if (cache.size > 8) {
3190
3539
  const oldest = cache.keys().next();
3191
3540
  if (!oldest.done) {
3192
3541
  cache.delete(oldest.value);
@@ -3267,65 +3616,27 @@ function measureMorphSnapshotWithDomService({
3267
3616
  useContentInlineSize
3268
3617
  }) {
3269
3618
  if (renderText.length === 0) {
3270
- return {
3271
- text,
3272
- renderText,
3273
- width: 0,
3274
- height: 0,
3275
- graphemes: []
3276
- };
3277
- }
3278
- const service = getDomMeasurementService();
3279
- applyMeasurementHostStyle({
3280
- host: service.host,
3281
- root,
3282
- layoutContext,
3283
- useContentInlineSize
3284
- });
3285
- service.host.textContent = renderText;
3286
- return measureMorphSnapshotFromLayer(text, renderText, segments, service.host);
3287
- }
3288
- function readRootOrigin(node) {
3289
- const rect = node.getBoundingClientRect();
3290
- return { left: rect.left, top: rect.top };
3291
- }
3292
- function readFlowInlineSize(node) {
3293
- if (node === null) {
3294
- return null;
3295
- }
3296
- return node.getBoundingClientRect().width;
3297
- }
3298
- function readFlowLineCount(node) {
3299
- if (node === null) {
3300
- return null;
3301
- }
3302
- const rects = Array.from(node.getClientRects());
3303
- if (rects.length === 0) {
3304
- if ((node.textContent ?? "").length === 0) {
3305
- return 0;
3306
- }
3307
- return 1;
3308
- }
3309
- let lineCount = 0;
3310
- let currentTop = null;
3311
- for (const rect of rects) {
3312
- if (currentTop === null || !nearlyEqual(rect.top, currentTop, MORPH.lineGroupingEpsilon)) {
3313
- currentTop = rect.top;
3314
- lineCount += 1;
3315
- }
3316
- }
3317
- return lineCount;
3318
- }
3319
- function readFirstTextNode(node) {
3320
- if (node === null) {
3321
- return null;
3322
- }
3323
- for (const childNode of node.childNodes) {
3324
- if (childNode.nodeType === Node.TEXT_NODE) {
3325
- return childNode;
3326
- }
3619
+ return {
3620
+ text,
3621
+ renderText,
3622
+ width: 0,
3623
+ height: 0,
3624
+ graphemes: []
3625
+ };
3327
3626
  }
3328
- return null;
3627
+ const service = getDomMeasurementService();
3628
+ applyMeasurementHostStyle({
3629
+ host: service.host,
3630
+ root,
3631
+ layoutContext,
3632
+ useContentInlineSize
3633
+ });
3634
+ service.host.textContent = renderText;
3635
+ return measureMorphSnapshotFromLayer(text, renderText, segments, service.host);
3636
+ }
3637
+ function readRootOrigin(node) {
3638
+ const rect = node.getBoundingClientRect();
3639
+ return { left: rect.left, top: rect.top };
3329
3640
  }
3330
3641
  function measureLiveFlowSnapshot(root, flowTextNode) {
3331
3642
  const textNode = readFirstTextNode(flowTextNode);
@@ -3458,141 +3769,6 @@ function measureOverlayBoxSnapshot(root, overlayRoot, role) {
3458
3769
  graphemes
3459
3770
  };
3460
3771
  }
3461
- function getTrustedPretextMeasurementBackend(text, renderText, layoutContext, useContentInlineSize) {
3462
- const backend = getPretextMorphMeasurementBackend(text, layoutContext);
3463
- if (backend !== "probe") {
3464
- return backend;
3465
- }
3466
- const signature = getPretextMorphTrustSignature({
3467
- renderText,
3468
- layoutContext,
3469
- useContentInlineSize
3470
- });
3471
- if (signature === null) {
3472
- return "dom";
3473
- }
3474
- const trusted = pretextMorphTrustCache.get(signature);
3475
- if (trusted === undefined) {
3476
- return "probe";
3477
- }
3478
- if (trusted) {
3479
- return "pretext";
3480
- }
3481
- return "dom";
3482
- }
3483
- function resolveContentWidthLockInlineSize(layoutHint) {
3484
- if (layoutHint.flowInlineSize !== null) {
3485
- return layoutHint.flowInlineSize;
3486
- }
3487
- return layoutHint.snapshot.width;
3488
- }
3489
- function shouldMeasureUsingContentInlineSize(layoutContext, layoutHint) {
3490
- if (supportsIntrinsicWidthLock(layoutContext.display, layoutContext.parentDisplay)) {
3491
- return true;
3492
- }
3493
- if (layoutContext.whiteSpace === "nowrap") {
3494
- return true;
3495
- }
3496
- if (layoutHint === null || !isSingleLineSnapshot(layoutHint.snapshot)) {
3497
- return false;
3498
- }
3499
- return nearlyEqual(layoutHint.layoutInlineSize, resolveContentWidthLockInlineSize(layoutHint), MORPH.contentWidthLockEpsilon);
3500
- }
3501
- function shouldHealIdleMeasurementFromFlow(measurement, flowLineCount) {
3502
- if (flowLineCount !== 1) {
3503
- return false;
3504
- }
3505
- return countSnapshotLines(measurement.snapshot) > 1;
3506
- }
3507
- function refineMeasurementWithLiveGeometry(measurement, liveGeometry) {
3508
- const sameFlowInlineSize = measurement.flowInlineSize === null && liveGeometry.flowInlineSize === null || measurement.flowInlineSize !== null && liveGeometry.flowInlineSize !== null && nearlyEqual(measurement.flowInlineSize, liveGeometry.flowInlineSize);
3509
- if (sameFlowInlineSize && nearlyEqual(measurement.rootOrigin.left, liveGeometry.rootOrigin.left) && nearlyEqual(measurement.rootOrigin.top, liveGeometry.rootOrigin.top)) {
3510
- return measurement;
3511
- }
3512
- return {
3513
- snapshot: measurement.snapshot,
3514
- layoutInlineSize: measurement.layoutInlineSize,
3515
- reservedInlineSize: measurement.reservedInlineSize,
3516
- flowInlineSize: liveGeometry.flowInlineSize,
3517
- rootOrigin: liveGeometry.rootOrigin
3518
- };
3519
- }
3520
- function refineMeasurementFromLiveNodes(measurement, root, flowTextNode) {
3521
- if (root === null) {
3522
- return measurement;
3523
- }
3524
- return refineMeasurementWithLiveGeometry(measurement, {
3525
- flowInlineSize: readFlowInlineSize(flowTextNode),
3526
- rootOrigin: readRootOrigin(root)
3527
- });
3528
- }
3529
- function createMorphMeasurementRequest({
3530
- text,
3531
- layoutContext,
3532
- layoutHint,
3533
- forceContentInlineSize = false
3534
- }) {
3535
- if (layoutContext === null) {
3536
- return null;
3537
- }
3538
- const renderText = getPretextMorphRenderedText(text, layoutContext);
3539
- let useContentInlineSize = shouldMeasureUsingContentInlineSize(layoutContext, layoutHint);
3540
- if (forceContentInlineSize) {
3541
- useContentInlineSize = true;
3542
- }
3543
- const measurementBackend = getTrustedPretextMeasurementBackend(text, renderText, layoutContext, useContentInlineSize);
3544
- let segments = readCachedMorphSegments(renderText);
3545
- if (measurementBackend === "pretext") {
3546
- segments = EMPTY_SEGMENTS;
3547
- }
3548
- let domMeasurementKey = null;
3549
- if (needsMeasurementLayer(measurementBackend, renderText)) {
3550
- domMeasurementKey = getDomMeasurementRequestKey(text, renderText, layoutContext, useContentInlineSize);
3551
- }
3552
- return {
3553
- text,
3554
- renderText,
3555
- segments,
3556
- measurementBackend,
3557
- useContentInlineSize,
3558
- domMeasurementKey
3559
- };
3560
- }
3561
- function rememberPretextMeasurementTrust({
3562
- renderText,
3563
- layoutContext,
3564
- useContentInlineSize,
3565
- trusted
3566
- }) {
3567
- const signature = getPretextMorphTrustSignature({
3568
- renderText,
3569
- layoutContext,
3570
- useContentInlineSize
3571
- });
3572
- if (signature === null) {
3573
- return;
3574
- }
3575
- pretextMorphTrustCache.set(signature, trusted);
3576
- }
3577
- function areSnapshotsEquivalentForPretextTrust(left, right) {
3578
- if (left.renderText !== right.renderText || left.graphemes.length !== right.graphemes.length) {
3579
- return false;
3580
- }
3581
- if (Math.abs(left.width - right.width) > MORPH.geometryEpsilon || Math.abs(left.height - right.height) > MORPH.geometryEpsilon) {
3582
- return false;
3583
- }
3584
- for (let index = 0;index < left.graphemes.length; index += 1) {
3585
- const from = left.graphemes[index];
3586
- const to = right.graphemes[index];
3587
- if (from.glyph !== to.glyph) {
3588
- return false;
3589
- }
3590
- if (Math.abs(from.left - to.left) > MORPH.geometryEpsilon || Math.abs(from.top - to.top) > MORPH.geometryEpsilon || Math.abs(from.width - to.width) > MORPH.geometryEpsilon || Math.abs(from.height - to.height) > MORPH.geometryEpsilon) {
3591
- return false;
3592
- }
3593
- }
3594
- return true;
3595
- }
3596
3772
  function measureFromNodes({
3597
3773
  root,
3598
3774
  layoutContext,
@@ -3612,7 +3788,7 @@ function measureFromNodes({
3612
3788
  width: Number.MAX_SAFE_INTEGER / 4
3613
3789
  };
3614
3790
  }
3615
- const snapshot = snapshotOverride ?? (() => {
3791
+ const snapshot = assertSingleLineSnapshot(snapshotOverride ?? (() => {
3616
3792
  let pretextSnapshot = null;
3617
3793
  if (measurementBackend !== "dom") {
3618
3794
  pretextSnapshot = measureMorphSnapshotWithPretext(text, measurementLayoutContext);
@@ -3649,294 +3825,378 @@ function measureFromNodes({
3649
3825
  if (resolvedSnapshot === null) {
3650
3826
  throw new Error("Torph failed to resolve a measurement snapshot.");
3651
3827
  }
3652
- return resolvedSnapshot;
3653
- })();
3654
- let layoutInlineSize = layoutContext.width;
3655
- if (useContentInlineSize) {
3656
- layoutInlineSize = snapshot.width;
3828
+ return resolvedSnapshot;
3829
+ })());
3830
+ let layoutInlineSize = layoutContext.width;
3831
+ if (useContentInlineSize) {
3832
+ layoutInlineSize = snapshot.width;
3833
+ }
3834
+ let reservedInlineSize = null;
3835
+ if (supportsIntrinsicWidthLock(layoutContext.display, layoutContext.parentDisplay)) {
3836
+ reservedInlineSize = snapshot.width;
3837
+ }
3838
+ let flowInlineSize = null;
3839
+ if (useContentInlineSize) {
3840
+ flowInlineSize = snapshot.width;
3841
+ }
3842
+ return {
3843
+ snapshot,
3844
+ layoutInlineSize,
3845
+ reservedInlineSize,
3846
+ flowInlineSize,
3847
+ rootOrigin: readRootOrigin(root)
3848
+ };
3849
+ }
3850
+
3851
+ // torph/src/debug/trace.ts
3852
+ var TORPH_TRACE_MAX_BYTES = 4 * 1024 * 1024;
3853
+ var TORPH_TRACE_MAX_LINES = 4000;
3854
+ var TORPH_TRACE_SCHEMA_VERSION = 7;
3855
+ var DEFAULT_TORPH_DEBUG_CONFIG = {
3856
+ capture: false,
3857
+ console: false
3858
+ };
3859
+ var torphDebugInstanceOrdinal = 0;
3860
+ function nextTorphDebugInstanceId() {
3861
+ torphDebugInstanceOrdinal += 1;
3862
+ return torphDebugInstanceOrdinal;
3863
+ }
3864
+ function readTorphDebugConfig() {
3865
+ const scope = globalThis;
3866
+ return scope.__TORPH_DEBUG__ ?? DEFAULT_TORPH_DEBUG_CONFIG;
3867
+ }
3868
+ function shouldCaptureTorphTrace(config) {
3869
+ if (config === null) {
3870
+ return false;
3871
+ }
3872
+ if (typeof config === "boolean") {
3873
+ return config;
3874
+ }
3875
+ if (config.capture === true) {
3876
+ return true;
3877
+ }
3878
+ return false;
3879
+ }
3880
+ function isTorphDebugEnabled(config) {
3881
+ if (config === null) {
3882
+ return false;
3883
+ }
3884
+ if (typeof config === "boolean") {
3885
+ return config;
3886
+ }
3887
+ if (config.console !== undefined) {
3888
+ return config.console;
3889
+ }
3890
+ if (config.enabled === true) {
3891
+ return true;
3892
+ }
3893
+ return false;
3894
+ }
3895
+ function shouldRunTorphInstrumentation(config) {
3896
+ if (shouldCaptureTorphTrace(config)) {
3897
+ return true;
3898
+ }
3899
+ return isTorphDebugEnabled(config);
3900
+ }
3901
+ function getTorphTraceStore() {
3902
+ const scope = globalThis;
3903
+ let store = scope.__TORPH_TRACE_STORE__;
3904
+ if (store !== undefined) {
3905
+ return store;
3906
+ }
3907
+ store = {
3908
+ lines: [],
3909
+ nextSeq: 1,
3910
+ totalBytes: 0
3911
+ };
3912
+ scope.__TORPH_TRACE_STORE__ = store;
3913
+ return store;
3914
+ }
3915
+ function getTorphTraceText() {
3916
+ return getTorphTraceStore().lines.join("");
3917
+ }
3918
+ function clearTorphTrace() {
3919
+ const store = getTorphTraceStore();
3920
+ store.lines = [];
3921
+ store.nextSeq = 1;
3922
+ store.totalBytes = 0;
3923
+ }
3924
+ function downloadTorphTrace(filename) {
3925
+ if (typeof document === "undefined") {
3926
+ return null;
3927
+ }
3928
+ const text = getTorphTraceText();
3929
+ const blob = new Blob([text], { type: "application/x-ndjson;charset=utf-8" });
3930
+ const href = URL.createObjectURL(blob);
3931
+ const anchor = document.createElement("a");
3932
+ let resolvedFilename = filename;
3933
+ if (resolvedFilename === undefined) {
3934
+ resolvedFilename = `torph-trace-${new Date().toISOString().replaceAll(":", "-")}.jsonl`;
3935
+ }
3936
+ anchor.href = href;
3937
+ anchor.download = resolvedFilename;
3938
+ anchor.click();
3939
+ window.setTimeout(() => {
3940
+ URL.revokeObjectURL(href);
3941
+ }, 0);
3942
+ return resolvedFilename;
3943
+ }
3944
+ function ensureTorphTraceApi() {
3945
+ const scope = globalThis;
3946
+ if (scope.__TORPH_TRACE__ !== undefined) {
3947
+ return scope.__TORPH_TRACE__;
3948
+ }
3949
+ const api = {
3950
+ clear: clearTorphTrace,
3951
+ count: () => getTorphTraceStore().lines.length,
3952
+ download: downloadTorphTrace,
3953
+ text: getTorphTraceText
3954
+ };
3955
+ scope.__TORPH_TRACE__ = api;
3956
+ return api;
3957
+ }
3958
+ function appendTorphTrace(instanceId, event, payload) {
3959
+ ensureTorphTraceApi();
3960
+ const store = getTorphTraceStore();
3961
+ const entry = {
3962
+ instanceId,
3963
+ event,
3964
+ payload,
3965
+ seq: store.nextSeq,
3966
+ time: new Date().toISOString()
3967
+ };
3968
+ store.nextSeq += 1;
3969
+ const line = `${JSON.stringify(entry)}
3970
+ `;
3971
+ store.lines.push(line);
3972
+ store.totalBytes += line.length;
3973
+ while (store.lines.length > TORPH_TRACE_MAX_LINES || store.totalBytes > TORPH_TRACE_MAX_BYTES) {
3974
+ const removed = store.lines.shift();
3975
+ if (removed === undefined) {
3976
+ break;
3977
+ }
3978
+ store.totalBytes = Math.max(0, store.totalBytes - removed.length);
3657
3979
  }
3658
- let reservedInlineSize = null;
3659
- if (supportsIntrinsicWidthLock(layoutContext.display, layoutContext.parentDisplay)) {
3660
- reservedInlineSize = snapshot.width;
3980
+ }
3981
+ function roundDebugValue(value) {
3982
+ if (value === null || value === undefined) {
3983
+ return value;
3661
3984
  }
3662
- return {
3663
- snapshot,
3664
- layoutInlineSize,
3665
- reservedInlineSize,
3666
- flowInlineSize: null,
3667
- rootOrigin: readRootOrigin(root)
3668
- };
3985
+ return Math.round(value * 1e4) / 1e4;
3669
3986
  }
3670
- function pinMeasurementToCurrentOrigin(measurement, origin) {
3671
- if (nearlyEqual(measurement.rootOrigin.left, origin.left) && nearlyEqual(measurement.rootOrigin.top, origin.top)) {
3672
- return measurement;
3987
+ function summarizeDebugSnapshot(snapshot) {
3988
+ if (snapshot === null) {
3989
+ return null;
3673
3990
  }
3674
3991
  return {
3675
- snapshot: measurement.snapshot,
3676
- layoutInlineSize: measurement.layoutInlineSize,
3677
- reservedInlineSize: measurement.reservedInlineSize,
3678
- flowInlineSize: measurement.flowInlineSize,
3679
- rootOrigin: origin
3992
+ text: snapshot.text,
3993
+ renderText: snapshot.renderText,
3994
+ width: roundDebugValue(snapshot.width),
3995
+ height: roundDebugValue(snapshot.height),
3996
+ graphemes: snapshot.graphemes.length
3680
3997
  };
3681
3998
  }
3682
- function bucketByGlyph(graphemes) {
3683
- const buckets = new Map;
3684
- for (const grapheme of graphemes) {
3685
- const bucket = buckets.get(grapheme.glyph);
3686
- if (bucket) {
3687
- bucket.push(grapheme);
3688
- } else {
3689
- buckets.set(grapheme.glyph, [grapheme]);
3690
- }
3999
+ function summarizeDebugGlyphs(snapshot) {
4000
+ if (snapshot === null) {
4001
+ return null;
3691
4002
  }
3692
- return buckets;
4003
+ return snapshot.graphemes.map((grapheme, index) => ({
4004
+ index,
4005
+ glyph: grapheme.glyph,
4006
+ key: grapheme.key,
4007
+ left: roundDebugValue(grapheme.left),
4008
+ top: roundDebugValue(grapheme.top),
4009
+ width: roundDebugValue(grapheme.width),
4010
+ height: roundDebugValue(grapheme.height)
4011
+ }));
3693
4012
  }
3694
- function pairMorphCharacters(previous, next) {
3695
- const previousBuckets = bucketByGlyph(previous);
3696
- const nextBuckets = bucketByGlyph(next);
3697
- const pairings = [];
3698
- for (const [glyph, previousItems] of previousBuckets) {
3699
- const nextItems = nextBuckets.get(glyph) ?? [];
3700
- const shared = Math.min(previousItems.length, nextItems.length);
3701
- for (let index = 0;index < shared; index += 1) {
3702
- pairings.push({
3703
- kind: "move",
3704
- from: previousItems[index],
3705
- to: nextItems[index]
3706
- });
3707
- }
3708
- for (let index = shared;index < previousItems.length; index += 1) {
3709
- pairings.push({
3710
- kind: "exit",
3711
- from: previousItems[index]
3712
- });
3713
- }
3714
- }
3715
- for (const [glyph, nextItems] of nextBuckets) {
3716
- const previousItems = previousBuckets.get(glyph) ?? [];
3717
- const shared = Math.min(previousItems.length, nextItems.length);
3718
- for (let index = shared;index < nextItems.length; index += 1) {
3719
- pairings.push({
3720
- kind: "enter",
3721
- to: nextItems[index]
3722
- });
3723
- }
4013
+ function summarizeDebugMeasurement(measurement) {
4014
+ if (measurement === null) {
4015
+ return null;
3724
4016
  }
3725
- return pairings;
3726
- }
3727
- function resolveMorphFrameBounds(previous, next) {
3728
4017
  return {
3729
- width: Math.max(previous.width, next.width),
3730
- height: Math.max(previous.height, next.height)
4018
+ layoutInlineSize: roundDebugValue(measurement.layoutInlineSize),
4019
+ reservedInlineSize: roundDebugValue(measurement.reservedInlineSize),
4020
+ flowInlineSize: roundDebugValue(measurement.flowInlineSize),
4021
+ rootOrigin: {
4022
+ left: roundDebugValue(measurement.rootOrigin.left),
4023
+ top: roundDebugValue(measurement.rootOrigin.top)
4024
+ },
4025
+ snapshot: summarizeDebugSnapshot(measurement.snapshot)
3731
4026
  };
3732
4027
  }
3733
- function buildMorphVisualBridge(previous, next) {
4028
+ function summarizeDebugLayoutContext(layoutContext) {
4029
+ if (layoutContext === null) {
4030
+ return null;
4031
+ }
3734
4032
  return {
3735
- offsetX: previous.rootOrigin.left - next.rootOrigin.left,
3736
- offsetY: previous.rootOrigin.top - next.rootOrigin.top
4033
+ display: layoutContext.display,
4034
+ parentDisplay: layoutContext.parentDisplay,
4035
+ whiteSpace: layoutContext.whiteSpace,
4036
+ width: roundDebugValue(layoutContext.width),
4037
+ measurementVersion: layoutContext.measurementVersion
3737
4038
  };
3738
4039
  }
3739
- function buildMorphPlan(previous, next, visualBridge = ZERO_BRIDGE) {
3740
- const pairings = pairMorphCharacters(previous.snapshot.graphemes, next.snapshot.graphemes);
3741
- const movesByDestinationKey = new Map;
3742
- const exitItems = [];
3743
- for (const pairing of pairings) {
3744
- if (pairing.kind === "move") {
3745
- movesByDestinationKey.set(pairing.to.key, pairing);
3746
- continue;
3747
- }
3748
- if (pairing.kind === "exit") {
3749
- exitItems.push(pairing.from);
3750
- }
4040
+ function summarizeDebugRect(rect) {
4041
+ if (rect === null) {
4042
+ return null;
3751
4043
  }
3752
- const frame = resolveMorphFrameBounds(previous.snapshot, next.snapshot);
3753
4044
  return {
3754
- frameWidth: frame.width,
3755
- frameHeight: frame.height,
3756
- layoutInlineSizeFrom: previous.layoutInlineSize,
3757
- layoutInlineSizeTo: next.layoutInlineSize,
3758
- sourceRenderText: previous.snapshot.renderText,
3759
- targetRenderText: next.snapshot.renderText,
3760
- visualBridge,
3761
- liveItems: next.snapshot.graphemes.map((grapheme) => {
3762
- const move = movesByDestinationKey.get(grapheme.key);
3763
- if (move) {
3764
- return {
3765
- ...grapheme,
3766
- kind: "move",
3767
- fromLeft: move.from.left,
3768
- fromTop: move.from.top
3769
- };
3770
- }
3771
- return {
3772
- ...grapheme,
3773
- kind: "enter",
3774
- fromLeft: null,
3775
- fromTop: null
3776
- };
3777
- }),
3778
- exitItems
4045
+ left: roundDebugValue(rect.left),
4046
+ top: roundDebugValue(rect.top),
4047
+ width: roundDebugValue(rect.width),
4048
+ height: roundDebugValue(rect.height)
3779
4049
  };
3780
4050
  }
3781
- function nearlyEqual(a, b, epsilon = MORPH.geometryEpsilon) {
3782
- return Math.abs(a - b) <= epsilon;
3783
- }
3784
- function sameSnapshot(a, b) {
3785
- if (a === b) {
3786
- return true;
3787
- }
3788
- if (a.text !== b.text || a.renderText !== b.renderText || a.graphemes.length !== b.graphemes.length) {
3789
- return false;
4051
+ function collectDebugAnchorIndices(length) {
4052
+ const indices = new Set;
4053
+ if (length <= 0) {
4054
+ return [];
3790
4055
  }
3791
- if (!nearlyEqual(a.width, b.width) || !nearlyEqual(a.height, b.height)) {
3792
- return false;
4056
+ indices.add(0);
4057
+ if (length > 1) {
4058
+ indices.add(1);
4059
+ indices.add(length - 2);
3793
4060
  }
3794
- for (let index = 0;index < a.graphemes.length; index += 1) {
3795
- const left = a.graphemes[index];
3796
- const right = b.graphemes[index];
3797
- if (left.glyph !== right.glyph || left.key !== right.key) {
3798
- return false;
3799
- }
3800
- if (!nearlyEqual(left.left, right.left) || !nearlyEqual(left.top, right.top) || !nearlyEqual(left.width, right.width) || !nearlyEqual(left.height, right.height)) {
3801
- return false;
3802
- }
4061
+ if (length > 2) {
4062
+ indices.add(Math.floor((length - 1) / 2));
3803
4063
  }
3804
- return true;
4064
+ indices.add(length - 1);
4065
+ return Array.from(indices).sort((left, right) => left - right);
3805
4066
  }
3806
- function sameMeasurement(a, b) {
3807
- if (a === b) {
3808
- return true;
4067
+ function summarizeDebugViewportAnchors(snapshot, rootRect) {
4068
+ if (snapshot === null || rootRect === null) {
4069
+ return null;
3809
4070
  }
3810
- return sameSnapshot(a.snapshot, b.snapshot) && nearlyEqual(a.layoutInlineSize, b.layoutInlineSize) && (a.reservedInlineSize === null && b.reservedInlineSize === null || a.reservedInlineSize !== null && b.reservedInlineSize !== null && nearlyEqual(a.reservedInlineSize, b.reservedInlineSize)) && (a.flowInlineSize === null && b.flowInlineSize === null || a.flowInlineSize !== null && b.flowInlineSize !== null && nearlyEqual(a.flowInlineSize, b.flowInlineSize)) && nearlyEqual(a.rootOrigin.left, b.rootOrigin.left) && nearlyEqual(a.rootOrigin.top, b.rootOrigin.top);
3811
- }
3812
- function refreshAnimatingTarget(activeTarget, measurement) {
3813
- if (sameMeasurement(activeTarget, measurement)) {
3814
- return activeTarget;
4071
+ const anchors = [];
4072
+ const anchorIndices = collectDebugAnchorIndices(snapshot.graphemes.length);
4073
+ for (const index of anchorIndices) {
4074
+ const grapheme = snapshot.graphemes[index];
4075
+ if (grapheme === undefined) {
4076
+ continue;
4077
+ }
4078
+ anchors.push({
4079
+ index,
4080
+ glyph: grapheme.glyph,
4081
+ left: roundDebugValue(rootRect.left + grapheme.left),
4082
+ top: roundDebugValue(rootRect.top + grapheme.top),
4083
+ width: roundDebugValue(grapheme.width),
4084
+ height: roundDebugValue(grapheme.height)
4085
+ });
3815
4086
  }
3816
- return {
3817
- snapshot: activeTarget.snapshot,
3818
- layoutInlineSize: measurement.layoutInlineSize,
3819
- reservedInlineSize: measurement.reservedInlineSize,
3820
- flowInlineSize: activeTarget.flowInlineSize,
3821
- rootOrigin: measurement.rootOrigin
3822
- };
4087
+ return anchors;
3823
4088
  }
3824
- function reuseCommittedMeasurement(committed, measurement) {
3825
- if (sameMeasurement(committed, measurement)) {
3826
- return committed;
4089
+ function summarizeDebugRootOriginDrift(measurement, rootRect) {
4090
+ if (measurement === null || rootRect === null) {
4091
+ return null;
3827
4092
  }
3828
- return measurement;
3829
- }
3830
- function createStaticState(measurement) {
3831
4093
  return {
3832
- stage: "idle",
3833
- measurement,
3834
- plan: null
4094
+ expectedLeft: roundDebugValue(measurement.rootOrigin.left),
4095
+ expectedTop: roundDebugValue(measurement.rootOrigin.top),
4096
+ actualLeft: roundDebugValue(rootRect.left),
4097
+ actualTop: roundDebugValue(rootRect.top),
4098
+ deltaLeft: roundDebugValue(rootRect.left - measurement.rootOrigin.left),
4099
+ deltaTop: roundDebugValue(rootRect.top - measurement.rootOrigin.top)
3835
4100
  };
3836
4101
  }
3837
- function areFontsReady() {
3838
- return document.fonts.status === "loaded";
3839
- }
3840
- function cancelTimeline(timeline) {
3841
- if (timeline.prepareFrame !== null) {
3842
- cancelAnimationFrame(timeline.prepareFrame);
3843
- timeline.prepareFrame = null;
3844
- }
3845
- if (timeline.animateFrame !== null) {
3846
- cancelAnimationFrame(timeline.animateFrame);
3847
- timeline.animateFrame = null;
3848
- }
3849
- if (timeline.finalizeTimer !== null) {
3850
- window.clearTimeout(timeline.finalizeTimer);
3851
- timeline.finalizeTimer = null;
3852
- }
3853
- }
3854
- function resetMorph(session, timeline, setState) {
3855
- cancelTimeline(timeline);
3856
- session.committed = null;
3857
- session.target = null;
3858
- session.animating = false;
3859
- setState(EMPTY_STATE);
4102
+ function summarizeSnapshotDrift(drift) {
4103
+ return {
4104
+ comparedGlyphs: drift.comparedGlyphs,
4105
+ expectedGlyphs: drift.expectedGlyphs,
4106
+ actualGlyphs: drift.actualGlyphs,
4107
+ snapshotWidthDelta: roundDebugValue(drift.snapshotWidthDelta),
4108
+ snapshotHeightDelta: roundDebugValue(drift.snapshotHeightDelta),
4109
+ maxAbsLeftDelta: roundDebugValue(drift.maxAbsLeftDelta),
4110
+ maxAbsTopDelta: roundDebugValue(drift.maxAbsTopDelta),
4111
+ maxAbsWidthDelta: roundDebugValue(drift.maxAbsWidthDelta),
4112
+ maxAbsHeightDelta: roundDebugValue(drift.maxAbsHeightDelta),
4113
+ mismatches: drift.mismatches.map((mismatch) => ({
4114
+ index: mismatch.index,
4115
+ glyph: mismatch.glyph,
4116
+ leftDelta: roundDebugValue(mismatch.leftDelta),
4117
+ topDelta: roundDebugValue(mismatch.topDelta),
4118
+ widthDelta: roundDebugValue(mismatch.widthDelta),
4119
+ heightDelta: roundDebugValue(mismatch.heightDelta)
4120
+ }))
4121
+ };
3860
4122
  }
3861
- function commitStaticMeasurement(session, measurement, setState) {
3862
- if (!session.animating && session.target === null && session.committed !== null && sameMeasurement(session.committed, measurement)) {
4123
+ function logTorphDebug(instanceId, event, payload) {
4124
+ const config = readTorphDebugConfig();
4125
+ const captureTrace = shouldCaptureTorphTrace(config);
4126
+ const logToConsole = isTorphDebugEnabled(config);
4127
+ if (!captureTrace && !logToConsole) {
3863
4128
  return;
3864
4129
  }
3865
- session.committed = measurement;
3866
- session.target = null;
3867
- session.animating = false;
3868
- setState((current) => {
3869
- if (current.stage === "idle" && current.plan === null && current.measurement !== null && sameMeasurement(current.measurement, measurement)) {
3870
- return current;
3871
- }
3872
- return createStaticState(measurement);
3873
- });
3874
- }
3875
- function scheduleMorphTimeline({
3876
- session,
3877
- timeline,
3878
- finalizeMeasurement,
3879
- measurement,
3880
- plan,
3881
- setState
3882
- }) {
3883
- timeline.prepareFrame = requestAnimationFrame(() => {
3884
- timeline.prepareFrame = null;
3885
- setState((current) => {
3886
- if (current.measurement !== measurement || current.plan !== plan) {
3887
- return current;
3888
- }
3889
- return {
3890
- stage: "animate",
3891
- measurement,
3892
- plan
3893
- };
3894
- });
3895
- timeline.animateFrame = requestAnimationFrame(() => {
3896
- timeline.animateFrame = null;
3897
- timeline.finalizeTimer = window.setTimeout(() => {
3898
- timeline.finalizeTimer = null;
3899
- commitStaticMeasurement(session, finalizeMeasurement(session.target ?? measurement), setState);
3900
- }, MORPH.durationMs);
3901
- });
3902
- });
4130
+ if (captureTrace) {
4131
+ appendTorphTrace(instanceId, event, payload);
4132
+ }
4133
+ if (logToConsole) {
4134
+ console.log(`[Torph#${instanceId}] ${event}`, payload);
4135
+ }
3903
4136
  }
3904
- function startMorph({
3905
- finalizeMeasurement,
3906
- nextMeasurement,
3907
- session,
3908
- timeline,
3909
- setState
3910
- }) {
3911
- let sourceMeasurement = session.committed;
3912
- if (session.animating && session.target) {
3913
- sourceMeasurement = session.target;
4137
+
4138
+ // torph/src/components/Torph.tsx
4139
+ import { jsxDEV } from "react/jsx-dev-runtime";
4140
+ var SCREEN_READER_ONLY_STYLE = {
4141
+ position: "absolute",
4142
+ width: "1px",
4143
+ height: "1px",
4144
+ margin: "-1px",
4145
+ padding: 0,
4146
+ border: 0,
4147
+ clip: "rect(0 0 0 0)",
4148
+ clipPath: "inset(50%)",
4149
+ overflow: "hidden",
4150
+ whiteSpace: "nowrap"
4151
+ };
4152
+ var FALLBACK_TEXT_STYLE = {
4153
+ display: "block",
4154
+ gridArea: "1 / 1"
4155
+ };
4156
+ var SHARED_GLYPH_TYPOGRAPHY_STYLE = {
4157
+ font: "inherit",
4158
+ fontKerning: "inherit",
4159
+ fontFeatureSettings: "inherit",
4160
+ fontOpticalSizing: "inherit",
4161
+ fontStretch: "inherit",
4162
+ fontStyle: "inherit",
4163
+ fontVariant: "inherit",
4164
+ fontVariantNumeric: "inherit",
4165
+ fontVariationSettings: "inherit",
4166
+ fontWeight: "inherit",
4167
+ letterSpacing: "inherit",
4168
+ textTransform: "inherit",
4169
+ wordSpacing: "inherit",
4170
+ direction: "inherit"
4171
+ };
4172
+ var ABSOLUTE_GLYPH_STYLE = {
4173
+ position: "absolute",
4174
+ display: "block",
4175
+ overflow: "hidden",
4176
+ transformOrigin: "left top"
4177
+ };
4178
+ var CONTEXT_SLICE_TEXT_STYLE = {
4179
+ ...SHARED_GLYPH_TYPOGRAPHY_STYLE,
4180
+ position: "absolute",
4181
+ display: "block",
4182
+ minWidth: 0,
4183
+ whiteSpace: "inherit"
4184
+ };
4185
+ var debugDomNodeIds = new WeakMap;
4186
+ var debugDomNodeOrdinal = 0;
4187
+ function getDebugDomNodeId(node) {
4188
+ if (node === null) {
4189
+ return null;
3914
4190
  }
3915
- if (sourceMeasurement === null) {
3916
- commitStaticMeasurement(session, nextMeasurement, setState);
3917
- return;
4191
+ const existing = debugDomNodeIds.get(node);
4192
+ if (existing !== undefined) {
4193
+ return existing;
3918
4194
  }
3919
- const previousMeasurement = pinMeasurementToCurrentOrigin(sourceMeasurement, nextMeasurement.rootOrigin);
3920
- const visualBridge = buildMorphVisualBridge(previousMeasurement, nextMeasurement);
3921
- const plan = buildMorphPlan(previousMeasurement, nextMeasurement, visualBridge);
3922
- session.target = nextMeasurement;
3923
- session.animating = true;
3924
- setState({
3925
- stage: "prepare",
3926
- measurement: nextMeasurement,
3927
- plan
3928
- });
3929
- scheduleMorphTimeline({
3930
- session,
3931
- timeline,
3932
- finalizeMeasurement,
3933
- measurement: nextMeasurement,
3934
- plan,
3935
- setState
3936
- });
4195
+ debugDomNodeOrdinal += 1;
4196
+ debugDomNodeIds.set(node, debugDomNodeOrdinal);
4197
+ return debugDomNodeOrdinal;
3937
4198
  }
3938
4199
  function reconcileMorphChange({
3939
- finalizeMeasurement,
3940
4200
  root,
3941
4201
  measurementLayer,
3942
4202
  measurementBackend,
@@ -3956,10 +4216,7 @@ function reconcileMorphChange({
3956
4216
  if (measurementBackend === null) {
3957
4217
  throw new Error("Torph measurement backend is missing.");
3958
4218
  }
3959
- let layoutHint = session.committed;
3960
- if (session.animating && session.target !== null) {
3961
- layoutHint = session.target;
3962
- }
4219
+ const layoutHint = selectMorphLayoutHint(session);
3963
4220
  const nextMeasurement = measureFromNodes({
3964
4221
  root,
3965
4222
  layoutContext,
@@ -3971,73 +4228,13 @@ function reconcileMorphChange({
3971
4228
  renderText,
3972
4229
  segments
3973
4230
  });
3974
- if (session.animating && session.target !== null) {
3975
- if (nextMeasurement.snapshot.renderText === session.target.snapshot.renderText) {
3976
- session.target = refreshAnimatingTarget(session.target, nextMeasurement);
3977
- return nextMeasurement;
3978
- }
3979
- }
3980
- cancelTimeline(timeline);
3981
- if (session.committed === null) {
3982
- commitStaticMeasurement(session, nextMeasurement, setState);
3983
- return nextMeasurement;
3984
- }
3985
- if (!areFontsReady()) {
3986
- commitStaticMeasurement(session, nextMeasurement, setState);
3987
- return nextMeasurement;
3988
- }
3989
- if (session.committed.snapshot.renderText === nextMeasurement.snapshot.renderText) {
3990
- commitStaticMeasurement(session, reuseCommittedMeasurement(session.committed, nextMeasurement), setState);
3991
- return nextMeasurement;
3992
- }
3993
- startMorph({
3994
- finalizeMeasurement,
3995
- nextMeasurement,
4231
+ return reconcileMorphSessionUpdate({
3996
4232
  session,
3997
4233
  timeline,
4234
+ nextMeasurement,
4235
+ fontsReady: areFontsReady(),
3998
4236
  setState
3999
4237
  });
4000
- return nextMeasurement;
4001
- }
4002
- function syncCommittedRootOriginWhenIdle({
4003
- root,
4004
- flowTextRef,
4005
- layoutContext,
4006
- state,
4007
- session
4008
- }) {
4009
- if (root === null || layoutContext === null) {
4010
- return;
4011
- }
4012
- if (state.stage !== "idle" || state.measurement === null) {
4013
- return;
4014
- }
4015
- const nextRootOrigin = readRootOrigin(root);
4016
- const nextFlowInlineSize = readFlowInlineSize(flowTextRef.current);
4017
- const committedMeasurement = state.measurement;
4018
- if (nearlyEqual(committedMeasurement.rootOrigin.left, nextRootOrigin.left) && nearlyEqual(committedMeasurement.rootOrigin.top, nextRootOrigin.top) && (committedMeasurement.flowInlineSize === null && nextFlowInlineSize === null || committedMeasurement.flowInlineSize !== null && nextFlowInlineSize !== null && nearlyEqual(committedMeasurement.flowInlineSize, nextFlowInlineSize))) {
4019
- session.committed = committedMeasurement;
4020
- return;
4021
- }
4022
- session.committed = {
4023
- snapshot: committedMeasurement.snapshot,
4024
- layoutInlineSize: committedMeasurement.layoutInlineSize,
4025
- reservedInlineSize: committedMeasurement.reservedInlineSize,
4026
- flowInlineSize: nextFlowInlineSize,
4027
- rootOrigin: nextRootOrigin
4028
- };
4029
- }
4030
- function getFadeDuration(fraction) {
4031
- return Math.min(MORPH.durationMs * fraction, MORPH.maxFadeMs);
4032
- }
4033
- function getLiveTransform(item, stage, visualBridge) {
4034
- if (stage !== "prepare") {
4035
- return "translate(0px, 0px)";
4036
- }
4037
- if (item.kind === "move") {
4038
- return `translate(${(item.fromLeft ?? item.left) - item.left + visualBridge.offsetX}px, ${(item.fromTop ?? item.top) - item.top + visualBridge.offsetY}px)`;
4039
- }
4040
- return `translate(${visualBridge.offsetX}px, ${visualBridge.offsetY}px)`;
4041
4238
  }
4042
4239
  function getLiveOpacity(item, stage) {
4043
4240
  if (stage === "prepare" && item.kind === "enter") {
@@ -4045,96 +4242,14 @@ function getLiveOpacity(item, stage) {
4045
4242
  }
4046
4243
  return 1;
4047
4244
  }
4048
- function getLiveTransition(item, stage) {
4049
- if (stage !== "animate") {
4050
- return;
4051
- }
4052
- if (item.kind === "enter") {
4053
- return `opacity ${getFadeDuration(0.5)}ms linear ${getFadeDuration(0.25)}ms`;
4054
- }
4055
- return `transform ${MORPH.durationMs}ms ${MORPH.ease}, opacity ${getFadeDuration(0.25)}ms linear`;
4056
- }
4057
4245
  function getExitOpacity(stage) {
4058
4246
  if (stage === "animate") {
4059
4247
  return 0;
4060
4248
  }
4061
4249
  return 1;
4062
4250
  }
4063
- function getExitTransform(visualBridge) {
4064
- return `translate(${visualBridge.offsetX}px, ${visualBridge.offsetY}px)`;
4065
- }
4066
- function getExitTransition(stage) {
4067
- if (stage !== "animate") {
4068
- return;
4069
- }
4070
- return `transform ${MORPH.durationMs}ms ${MORPH.ease}, opacity ${getFadeDuration(0.25)}ms linear`;
4071
- }
4072
- function supportsIntrinsicWidthLock(display, parentDisplay) {
4073
- const parentNeedsReservation = parentDisplay === "flex" || parentDisplay === "inline-flex" || parentDisplay === "grid" || parentDisplay === "inline-grid";
4074
- return display === "inline" || display === "inline-block" || display === "inline-flex" || display === "inline-grid" || parentNeedsReservation;
4075
- }
4076
- function getRootDisplay(layoutContext) {
4077
- if (layoutContext === null) {
4078
- return "grid";
4079
- }
4080
- if (supportsIntrinsicWidthLock(layoutContext.display, layoutContext.parentDisplay)) {
4081
- return "inline-grid";
4082
- }
4083
- return "grid";
4084
- }
4085
- function getRootStyle(stage, plan, measurement, layoutContext) {
4086
- let width = measurement?.reservedInlineSize ?? undefined;
4087
- if (plan !== null) {
4088
- width = plan.layoutInlineSizeTo;
4089
- if (stage === "prepare") {
4090
- width = plan.layoutInlineSizeFrom;
4091
- }
4092
- }
4093
- let height;
4094
- if (plan !== null) {
4095
- height = plan.frameHeight;
4096
- }
4097
- const shouldTransitionWidth = stage === "animate" && plan !== null && !nearlyEqual(plan.layoutInlineSizeFrom, plan.layoutInlineSizeTo);
4098
- const style = {
4099
- position: "relative",
4100
- display: getRootDisplay(layoutContext)
4101
- };
4102
- if (width !== undefined) {
4103
- style.width = width;
4104
- }
4105
- if (height !== undefined) {
4106
- style.height = height;
4107
- }
4108
- if (shouldTransitionWidth) {
4109
- style.transition = `width ${MORPH.durationMs}ms ${MORPH.ease}`;
4110
- }
4111
- return style;
4112
- }
4113
- function getMeasurementLayerStyle(layoutContext, useContentInlineSize = false) {
4114
- const intrinsicWidthLock = layoutContext !== null && (useContentInlineSize || supportsIntrinsicWidthLock(layoutContext.display, layoutContext.parentDisplay));
4115
- if (!intrinsicWidthLock) {
4116
- return MEASUREMENT_LAYER_STYLE;
4117
- }
4118
- return {
4119
- ...MEASUREMENT_LAYER_STYLE,
4120
- right: "auto",
4121
- width: "max-content"
4122
- };
4123
- }
4124
- function resolveFlowText(committedMeasurement, stateMeasurement, text) {
4125
- return stateMeasurement?.snapshot.text ?? committedMeasurement?.snapshot.text ?? text;
4126
- }
4127
- function getOverlayStyle(plan) {
4128
- return {
4129
- ...OVERLAY_STYLE,
4130
- right: "auto",
4131
- bottom: "auto",
4132
- width: plan.frameWidth,
4133
- height: plan.frameHeight
4134
- };
4135
- }
4136
- function getFallbackTextStyle(shouldRenderOverlay) {
4137
- if (!shouldRenderOverlay) {
4251
+ function getFallbackTextStyle(shouldHideFlowText) {
4252
+ if (!shouldHideFlowText) {
4138
4253
  return FALLBACK_TEXT_STYLE;
4139
4254
  }
4140
4255
  return {
@@ -4167,18 +4282,136 @@ function getExitGlyphStyle(item, stage, visualBridge) {
4167
4282
  transition: getExitTransition(stage)
4168
4283
  };
4169
4284
  }
4170
- function getContextSliceStyle(layoutInlineSize, item) {
4285
+ function getContextSliceStyle(layoutInlineSize, item, whiteSpace) {
4286
+ return {
4287
+ ...CONTEXT_SLICE_TEXT_STYLE,
4288
+ left: -item.left,
4289
+ top: -item.top,
4290
+ width: layoutInlineSize,
4291
+ whiteSpace
4292
+ };
4293
+ }
4294
+ function summarizePreciseRect(rect) {
4295
+ if (rect === null) {
4296
+ return null;
4297
+ }
4298
+ return {
4299
+ left: roundDebugValue(rect.left),
4300
+ top: roundDebugValue(rect.top),
4301
+ width: roundDebugValue(rect.width),
4302
+ height: roundDebugValue(rect.height)
4303
+ };
4304
+ }
4305
+ function summarizePreciseGlyphs(snapshot, rootRect) {
4306
+ if (snapshot === null) {
4307
+ return null;
4308
+ }
4309
+ return snapshot.graphemes.map((grapheme, index) => ({
4310
+ index,
4311
+ glyph: grapheme.glyph,
4312
+ key: grapheme.key,
4313
+ left: roundDebugValue(grapheme.left),
4314
+ top: roundDebugValue(grapheme.top),
4315
+ width: roundDebugValue(grapheme.width),
4316
+ height: roundDebugValue(grapheme.height),
4317
+ viewportLeft: rootRect === null ? null : roundDebugValue(rootRect.left + grapheme.left),
4318
+ viewportTop: rootRect === null ? null : roundDebugValue(rootRect.top + grapheme.top)
4319
+ }));
4320
+ }
4321
+ function summarizeLiveNodeStyles(overlayNode) {
4322
+ if (overlayNode === null) {
4323
+ return null;
4324
+ }
4325
+ return Array.from(overlayNode.querySelectorAll("[data-morph-role='live']")).map((node, index) => {
4326
+ const styles = getComputedStyle(node);
4327
+ const sliceNode = node.firstElementChild;
4328
+ let sliceStyles = null;
4329
+ let sliceRect = null;
4330
+ if (sliceNode instanceof HTMLElement) {
4331
+ sliceStyles = getComputedStyle(sliceNode);
4332
+ sliceRect = sliceNode.getBoundingClientRect();
4333
+ }
4334
+ let nodeRect = null;
4335
+ nodeRect = node.getBoundingClientRect();
4336
+ let sliceScrollWidth = null;
4337
+ let sliceClientWidth = null;
4338
+ let sliceOffsetWidth = null;
4339
+ if (sliceNode instanceof HTMLElement) {
4340
+ sliceScrollWidth = sliceNode.scrollWidth;
4341
+ sliceClientWidth = sliceNode.clientWidth;
4342
+ sliceOffsetWidth = sliceNode.offsetWidth;
4343
+ }
4344
+ return {
4345
+ index,
4346
+ nodeId: getDebugDomNodeId(node),
4347
+ key: node.dataset.morphKey ?? null,
4348
+ glyph: node.dataset.morphGlyph ?? null,
4349
+ kind: node.dataset.morphKind ?? null,
4350
+ transform: styles.transform,
4351
+ inlineTransform: node.style.transform,
4352
+ opacity: styles.opacity,
4353
+ transitionProperty: styles.transitionProperty,
4354
+ transitionDuration: styles.transitionDuration,
4355
+ transitionTimingFunction: styles.transitionTimingFunction,
4356
+ nodeRect: summarizePreciseRect(nodeRect),
4357
+ sliceNodeId: getDebugDomNodeId(sliceNode),
4358
+ sliceInlineLeft: node.firstElementChild instanceof HTMLElement ? node.firstElementChild.style.left : null,
4359
+ sliceInlineTop: node.firstElementChild instanceof HTMLElement ? node.firstElementChild.style.top : null,
4360
+ sliceInlineWidth: node.firstElementChild instanceof HTMLElement ? node.firstElementChild.style.width : null,
4361
+ sliceLeft: sliceStyles?.left ?? null,
4362
+ sliceTop: sliceStyles?.top ?? null,
4363
+ sliceWidth: sliceStyles?.width ?? null,
4364
+ sliceWhiteSpace: sliceStyles?.whiteSpace ?? null,
4365
+ sliceText: sliceNode instanceof HTMLElement ? sliceNode.textContent : null,
4366
+ sliceRect: summarizePreciseRect(sliceRect),
4367
+ sliceScrollWidth: roundDebugValue(sliceScrollWidth),
4368
+ sliceClientWidth: roundDebugValue(sliceClientWidth),
4369
+ sliceOffsetWidth: roundDebugValue(sliceOffsetWidth)
4370
+ };
4371
+ });
4372
+ }
4373
+ function summarizeRootRuntimeStyles(root) {
4374
+ if (root === null) {
4375
+ return null;
4376
+ }
4377
+ const styles = getComputedStyle(root);
4378
+ const parent = root.parentElement;
4379
+ let parentStyles = null;
4380
+ let parentRect = null;
4381
+ if (parent instanceof HTMLElement) {
4382
+ parentStyles = getComputedStyle(parent);
4383
+ parentRect = parent.getBoundingClientRect();
4384
+ }
4385
+ let parentSummary = null;
4386
+ if (parent !== null) {
4387
+ parentSummary = {
4388
+ display: parentStyles?.display ?? null,
4389
+ justifyContent: parentStyles?.justifyContent ?? null,
4390
+ alignItems: parentStyles?.alignItems ?? null,
4391
+ placeItems: parentStyles?.placeItems ?? null,
4392
+ textAlign: parentStyles?.textAlign ?? null,
4393
+ rect: summarizePreciseRect(parentRect)
4394
+ };
4395
+ }
4171
4396
  return {
4172
- ...CONTEXT_SLICE_TEXT_STYLE,
4173
- left: -item.left,
4174
- top: -item.top,
4175
- width: layoutInlineSize
4397
+ inlineWidth: root.style.width || null,
4398
+ computedWidth: styles.width,
4399
+ inlineTransition: root.style.transition || null,
4400
+ computedTransitionProperty: styles.transitionProperty,
4401
+ computedTransitionDuration: styles.transitionDuration,
4402
+ computedTransform: styles.transform,
4403
+ offsetWidth: roundDebugValue(root.offsetWidth),
4404
+ clientWidth: roundDebugValue(root.clientWidth),
4405
+ scrollWidth: roundDebugValue(root.scrollWidth),
4406
+ parent: parentSummary
4176
4407
  };
4177
4408
  }
4178
4409
  function MorphOverlay({
4179
4410
  overlayRef,
4180
4411
  stage,
4181
- plan
4412
+ plan,
4413
+ sourceSliceWhiteSpace,
4414
+ targetSliceWhiteSpace
4182
4415
  }) {
4183
4416
  let exitItems = [];
4184
4417
  if (stage !== "idle") {
@@ -4187,7 +4420,7 @@ function MorphOverlay({
4187
4420
  return /* @__PURE__ */ jsxDEV("div", {
4188
4421
  ref: overlayRef,
4189
4422
  "aria-hidden": "true",
4190
- style: getOverlayStyle(plan),
4423
+ style: getOverlayStyle(stage, plan),
4191
4424
  children: [
4192
4425
  exitItems.map((item) => /* @__PURE__ */ jsxDEV("span", {
4193
4426
  "data-morph-role": "exit",
@@ -4195,7 +4428,8 @@ function MorphOverlay({
4195
4428
  "data-morph-glyph": item.glyph,
4196
4429
  style: getExitGlyphStyle(item, stage, plan.visualBridge),
4197
4430
  children: /* @__PURE__ */ jsxDEV("span", {
4198
- style: getContextSliceStyle(plan.layoutInlineSizeFrom, item),
4431
+ "data-morph-slice": "context",
4432
+ style: getContextSliceStyle(plan.layoutInlineSizeFrom, item, sourceSliceWhiteSpace),
4199
4433
  children: plan.sourceRenderText
4200
4434
  }, undefined, false, undefined, this)
4201
4435
  }, `exit-${item.key}`, false, undefined, this)),
@@ -4203,9 +4437,11 @@ function MorphOverlay({
4203
4437
  "data-morph-role": "live",
4204
4438
  "data-morph-key": item.key,
4205
4439
  "data-morph-glyph": item.glyph,
4440
+ "data-morph-kind": item.kind,
4206
4441
  style: getLiveGlyphStyle(item, stage, plan.visualBridge),
4207
4442
  children: /* @__PURE__ */ jsxDEV("span", {
4208
- style: getContextSliceStyle(plan.layoutInlineSizeTo, item),
4443
+ "data-morph-slice": "context",
4444
+ style: getContextSliceStyle(plan.layoutInlineSizeTo, item, targetSliceWhiteSpace),
4209
4445
  children: plan.targetRenderText
4210
4446
  }, undefined, false, undefined, this)
4211
4447
  }, item.key, false, undefined, this))
@@ -4225,54 +4461,92 @@ function MeasurementLayer({
4225
4461
  children: text
4226
4462
  }, undefined, false, undefined, this);
4227
4463
  }
4464
+ function isMorphOverlayTransformFinalizeEvent(event, hasMoveTransitions) {
4465
+ if (!hasMoveTransitions) {
4466
+ return false;
4467
+ }
4468
+ const target = event.target;
4469
+ if (!(target instanceof HTMLElement)) {
4470
+ return false;
4471
+ }
4472
+ if (target.dataset.morphRole !== "live") {
4473
+ return false;
4474
+ }
4475
+ return event.propertyName === "transform";
4476
+ }
4477
+ function resolveMorphFinalizeSignal(event, hasMoveTransitions) {
4478
+ if (isMorphOverlayTransformFinalizeEvent(event, hasMoveTransitions)) {
4479
+ return "live-transform";
4480
+ }
4481
+ return null;
4482
+ }
4228
4483
  function useMorphTransition(text, className) {
4229
- const { ref, layoutContext } = useObservedLayoutContext([className]);
4230
- const debugInstanceIdRef = useRef(null);
4231
- const flowTextRef = useRef(null);
4232
- const measurementLayerRef = useRef(null);
4233
- const completedDomMeasurementKeyRef = useRef(null);
4234
- const domMeasurementSnapshotCacheRef = useRef(new Map);
4235
- const sessionRef = useRef({ ...EMPTY_SESSION });
4236
- const timelineRef = useRef({ ...EMPTY_TIMELINE });
4237
- const debugDriftSignatureRef = useRef(null);
4238
- const [domMeasurementRequestKey, setDomMeasurementRequestKey] = useState(null);
4239
- const [forceContentMeasurementText, setForceContentMeasurementText] = useState(null);
4240
- const [state, setState] = useState(EMPTY_STATE);
4484
+ const [state, setState] = useState2(EMPTY_STATE);
4485
+ const { ref, layoutContext } = useObservedLayoutContext([
4486
+ className
4487
+ ]);
4488
+ const debugInstanceIdRef = useRef2(null);
4489
+ const debugRenderOrdinalRef = useRef2(0);
4490
+ const flowTextRef = useRef2(null);
4491
+ const measurementLayerRef = useRef2(null);
4492
+ const completedDomMeasurementKeyRef = useRef2(null);
4493
+ const domMeasurementSnapshotCacheRef = useRef2(new Map);
4494
+ const sessionRef = useRef2({ ...EMPTY_SESSION });
4495
+ const timelineRef = useRef2({ ...EMPTY_TIMELINE });
4496
+ const debugDriftSignatureRef = useRef2(null);
4497
+ const [domMeasurementRequestKey, setDomMeasurementRequestKey] = useState2(null);
4498
+ debugRenderOrdinalRef.current += 1;
4499
+ const debugRenderOrdinal = debugRenderOrdinalRef.current;
4241
4500
  if (debugInstanceIdRef.current === null) {
4242
4501
  debugInstanceIdRef.current = nextTorphDebugInstanceId();
4243
4502
  }
4244
- let measurementHint = sessionRef.current.committed;
4245
- if (sessionRef.current.animating) {
4246
- measurementHint = sessionRef.current.target ?? sessionRef.current.committed;
4247
- }
4248
- const forceContentInlineSize = forceContentMeasurementText === text;
4503
+ const measurementHint = selectMorphLayoutHint(sessionRef.current);
4249
4504
  const measurementRequest = useMemo(() => createMorphMeasurementRequest({
4250
4505
  text,
4251
4506
  layoutContext,
4252
- layoutHint: measurementHint,
4253
- forceContentInlineSize
4254
- }), [text, layoutContext, measurementHint, forceContentInlineSize]);
4507
+ layoutHint: measurementHint
4508
+ }), [text, layoutContext, measurementHint]);
4255
4509
  const renderText = measurementRequest?.renderText ?? text;
4256
4510
  const useContentInlineSize = measurementRequest?.useContentInlineSize ?? false;
4257
4511
  const measurementBackend = measurementRequest?.measurementBackend ?? null;
4258
4512
  const segments = measurementRequest?.segments ?? EMPTY_SEGMENTS;
4259
4513
  const domMeasurementKey = measurementRequest?.domMeasurementKey ?? null;
4260
- useLayoutEffect(() => {
4514
+ const logTransitionTrace = (event, payload = {}) => {
4261
4515
  const config = readTorphDebugConfig();
4262
- if (!shouldCaptureTorphTrace(config)) {
4516
+ if (!shouldRunTorphInstrumentation(config)) {
4263
4517
  return;
4264
4518
  }
4519
+ logTorphDebug(debugInstanceIdRef.current, event, {
4520
+ traceSchemaVersion: TORPH_TRACE_SCHEMA_VERSION,
4521
+ renderOrdinal: debugRenderOrdinal,
4522
+ text,
4523
+ renderText,
4524
+ stateStage: state.stage,
4525
+ committed: summarizeDebugMeasurement(sessionRef.current.committed),
4526
+ stateMeasurement: summarizeDebugMeasurement(state.measurement),
4527
+ layoutContext: summarizeDebugLayoutContext(layoutContext),
4528
+ measurementBackend,
4529
+ useContentInlineSize,
4530
+ domMeasurementKey,
4531
+ domMeasurementRequestKey,
4532
+ completedDomMeasurementKey: completedDomMeasurementKeyRef.current,
4533
+ ...payload
4534
+ });
4535
+ };
4536
+ useLayoutEffect2(() => {
4265
4537
  ensureTorphTraceApi();
4266
4538
  }, []);
4267
- useLayoutEffect(() => {
4268
- const finalizeMeasurement = (measurement) => refineMeasurementFromLiveNodes(measurement, ref.current, flowTextRef.current);
4539
+ useLayoutEffect2(() => {
4269
4540
  if (ref.current === null || layoutContext === null) {
4270
4541
  completedDomMeasurementKeyRef.current = null;
4271
4542
  if (domMeasurementRequestKey !== null) {
4543
+ logTransitionTrace("effect:dom-measurement-request-update", {
4544
+ reason: "clear-missing-root-or-layout",
4545
+ nextDomMeasurementRequestKey: null
4546
+ });
4272
4547
  setDomMeasurementRequestKey(null);
4273
4548
  }
4274
4549
  reconcileMorphChange({
4275
- finalizeMeasurement,
4276
4550
  root: ref.current,
4277
4551
  measurementLayer: measurementLayerRef.current,
4278
4552
  measurementBackend,
@@ -4295,10 +4569,14 @@ function useMorphTransition(text, className) {
4295
4569
  if (cachedSnapshot !== null) {
4296
4570
  completedDomMeasurementKeyRef.current = domMeasurementKey;
4297
4571
  if (domMeasurementRequestKey !== null) {
4572
+ logTransitionTrace("effect:dom-measurement-request-update", {
4573
+ reason: "clear-after-cache-hit",
4574
+ nextDomMeasurementRequestKey: null,
4575
+ snapshotSource: "cache"
4576
+ });
4298
4577
  setDomMeasurementRequestKey(null);
4299
4578
  }
4300
4579
  reconcileMorphChange({
4301
- finalizeMeasurement,
4302
4580
  root: ref.current,
4303
4581
  measurementLayer: null,
4304
4582
  measurementBackend,
@@ -4315,14 +4593,20 @@ function useMorphTransition(text, className) {
4315
4593
  }
4316
4594
  if (completedDomMeasurementKeyRef.current !== domMeasurementKey) {
4317
4595
  if (domMeasurementRequestKey !== domMeasurementKey) {
4596
+ logTransitionTrace("effect:dom-measurement-request-update", {
4597
+ reason: "request-measurement-layer",
4598
+ nextDomMeasurementRequestKey: domMeasurementKey
4599
+ });
4318
4600
  setDomMeasurementRequestKey(domMeasurementKey);
4319
4601
  return;
4320
4602
  }
4321
4603
  if (measurementLayerRef.current === null) {
4604
+ logTransitionTrace("effect:dom-measurement-await-layer", {
4605
+ reason: "measurement-layer-not-mounted"
4606
+ });
4322
4607
  return;
4323
4608
  }
4324
4609
  const nextMeasurement2 = reconcileMorphChange({
4325
- finalizeMeasurement,
4326
4610
  root: ref.current,
4327
4611
  measurementLayer: measurementLayerRef.current,
4328
4612
  measurementBackend,
@@ -4342,21 +4626,33 @@ function useMorphTransition(text, className) {
4342
4626
  }
4343
4627
  completedDomMeasurementKeyRef.current = domMeasurementKey;
4344
4628
  if (domMeasurementRequestKey !== null) {
4629
+ logTransitionTrace("effect:dom-measurement-request-update", {
4630
+ reason: "clear-after-live-measurement",
4631
+ nextDomMeasurementRequestKey: null,
4632
+ snapshotSource: "layer"
4633
+ });
4345
4634
  setDomMeasurementRequestKey(null);
4346
4635
  }
4347
4636
  return;
4348
4637
  }
4349
4638
  if (domMeasurementRequestKey !== null) {
4639
+ logTransitionTrace("effect:dom-measurement-request-update", {
4640
+ reason: "clear-completed-measurement",
4641
+ nextDomMeasurementRequestKey: null
4642
+ });
4350
4643
  setDomMeasurementRequestKey(null);
4351
4644
  }
4352
4645
  return;
4353
4646
  }
4354
4647
  completedDomMeasurementKeyRef.current = null;
4355
4648
  if (domMeasurementRequestKey !== null) {
4649
+ logTransitionTrace("effect:dom-measurement-request-update", {
4650
+ reason: "clear-no-dom-measurement-needed",
4651
+ nextDomMeasurementRequestKey: null
4652
+ });
4356
4653
  setDomMeasurementRequestKey(null);
4357
4654
  }
4358
4655
  const nextMeasurement = reconcileMorphChange({
4359
- finalizeMeasurement,
4360
4656
  root: ref.current,
4361
4657
  measurementLayer: measurementLayerRef.current,
4362
4658
  measurementBackend,
@@ -4379,36 +4675,7 @@ function useMorphTransition(text, className) {
4379
4675
  domMeasurementKey,
4380
4676
  domMeasurementRequestKey
4381
4677
  ]);
4382
- useLayoutEffect(() => {
4383
- syncCommittedRootOriginWhenIdle({
4384
- root: ref.current,
4385
- flowTextRef,
4386
- layoutContext,
4387
- state,
4388
- session: sessionRef.current
4389
- });
4390
- }, [layoutContext, state]);
4391
- useLayoutEffect(() => {
4392
- if (forceContentMeasurementText !== null && forceContentMeasurementText !== text) {
4393
- setForceContentMeasurementText(null);
4394
- return;
4395
- }
4396
- if (state.stage !== "idle" || state.measurement === null) {
4397
- return;
4398
- }
4399
- const flowLineCount = readFlowLineCount(flowTextRef.current);
4400
- const shouldHeal = shouldHealIdleMeasurementFromFlow(state.measurement, flowLineCount);
4401
- if (shouldHeal) {
4402
- if (forceContentMeasurementText !== text) {
4403
- setForceContentMeasurementText(text);
4404
- }
4405
- return;
4406
- }
4407
- if (forceContentMeasurementText === text) {
4408
- setForceContentMeasurementText(null);
4409
- }
4410
- }, [forceContentMeasurementText, state, text]);
4411
- useLayoutEffect(() => {
4678
+ useLayoutEffect2(() => {
4412
4679
  const config = readTorphDebugConfig();
4413
4680
  if (!shouldRunTorphInstrumentation(config)) {
4414
4681
  debugDriftSignatureRef.current = null;
@@ -4451,55 +4718,171 @@ function useMorphTransition(text, className) {
4451
4718
  drift: summarizeSnapshotDrift(drift)
4452
4719
  });
4453
4720
  }, [state, text]);
4454
- useLayoutEffect(() => {
4721
+ useLayoutEffect2(() => {
4455
4722
  return () => {
4456
4723
  cancelTimeline(timelineRef.current);
4457
4724
  };
4458
4725
  }, []);
4726
+ useLayoutEffect2(() => {
4727
+ if (state.stage !== "prepare" || state.measurement === null || state.plan === null) {
4728
+ return;
4729
+ }
4730
+ const root = ref.current;
4731
+ if (root === null) {
4732
+ return;
4733
+ }
4734
+ const nextOrigin = readRootOrigin(root);
4735
+ const nextMeasurement = resolvePreparedMeasurementOrigin(state.measurement, nextOrigin);
4736
+ const nextPlan = resolvePreparedPlanVisualBridge(state.plan, nextOrigin);
4737
+ if (nextMeasurement !== state.measurement || nextPlan !== state.plan) {
4738
+ sessionRef.current.target = nextMeasurement;
4739
+ logTransitionTrace("effect:prepare-refine", {
4740
+ preparedOrigin: {
4741
+ left: roundDebugValue(nextOrigin.left),
4742
+ top: roundDebugValue(nextOrigin.top)
4743
+ },
4744
+ refinedMeasurement: summarizeDebugMeasurement(nextMeasurement),
4745
+ refinedVisualBridge: {
4746
+ offsetX: roundDebugValue(nextPlan.visualBridge.offsetX),
4747
+ offsetY: roundDebugValue(nextPlan.visualBridge.offsetY)
4748
+ }
4749
+ });
4750
+ setState((current) => {
4751
+ if (current.stage !== "prepare" || current.measurement === null || current.plan === null) {
4752
+ return current;
4753
+ }
4754
+ if (current.measurement === nextMeasurement && current.plan === nextPlan) {
4755
+ return current;
4756
+ }
4757
+ return {
4758
+ stage: "prepare",
4759
+ measurement: nextMeasurement,
4760
+ plan: nextPlan
4761
+ };
4762
+ });
4763
+ return;
4764
+ }
4765
+ timelineRef.current.prepareFrame = requestAnimationFrame(() => {
4766
+ timelineRef.current.prepareFrame = null;
4767
+ timelineRef.current.animateFrame = requestAnimationFrame(() => {
4768
+ timelineRef.current.animateFrame = null;
4769
+ logTransitionTrace("effect:prepare-animate", {
4770
+ preparedOrigin: {
4771
+ left: roundDebugValue(nextOrigin.left),
4772
+ top: roundDebugValue(nextOrigin.top)
4773
+ },
4774
+ visualBridge: {
4775
+ offsetX: roundDebugValue(nextPlan.visualBridge.offsetX),
4776
+ offsetY: roundDebugValue(nextPlan.visualBridge.offsetY)
4777
+ }
4778
+ });
4779
+ setState((current) => {
4780
+ if (current.stage !== "prepare" || current.measurement === null || current.plan === null) {
4781
+ return current;
4782
+ }
4783
+ return {
4784
+ stage: "animate",
4785
+ measurement: current.measurement,
4786
+ plan: current.plan
4787
+ };
4788
+ });
4789
+ });
4790
+ });
4791
+ return () => {
4792
+ if (timelineRef.current.prepareFrame !== null) {
4793
+ cancelAnimationFrame(timelineRef.current.prepareFrame);
4794
+ timelineRef.current.prepareFrame = null;
4795
+ }
4796
+ if (timelineRef.current.animateFrame !== null) {
4797
+ cancelAnimationFrame(timelineRef.current.animateFrame);
4798
+ timelineRef.current.animateFrame = null;
4799
+ }
4800
+ };
4801
+ }, [state.measurement, state.plan, state.stage]);
4802
+ const finalizeMorphTransition2 = (measurement, reason) => {
4803
+ logTransitionTrace("effect:finalize-trigger", {
4804
+ reason,
4805
+ measurement: summarizeDebugMeasurement(measurement)
4806
+ });
4807
+ finalizeMorphTransition({
4808
+ session: sessionRef.current,
4809
+ timeline: timelineRef.current,
4810
+ measurement,
4811
+ setState
4812
+ });
4813
+ };
4459
4814
  return {
4460
4815
  debugInstanceId: debugInstanceIdRef.current,
4816
+ debugRenderOrdinal,
4461
4817
  committedMeasurement: sessionRef.current.committed,
4462
4818
  domMeasurementRequestKey,
4463
4819
  flowTextRef,
4464
4820
  ref,
4465
4821
  measurementLayerRef,
4822
+ measurementBackend,
4823
+ domMeasurementKey,
4466
4824
  renderText,
4467
4825
  segments,
4468
4826
  layoutContext,
4469
4827
  state,
4470
- useContentInlineSize
4828
+ useContentInlineSize,
4829
+ finalizeMorphTransition: finalizeMorphTransition2,
4830
+ timelineRef
4471
4831
  };
4472
4832
  }
4473
4833
  function ActiveTorph({
4474
4834
  text,
4475
4835
  className
4476
4836
  }) {
4477
- const overlayRef = useRef(null);
4478
- const debugFinalizeSignatureRef = useRef(null);
4479
- const debugFrameHandleRef = useRef(null);
4480
- const debugFrameOrdinalRef = useRef(0);
4481
- const debugIdlePostFrameHandleRef = useRef(null);
4482
- const debugIdlePostFrameOrdinalRef = useRef(0);
4483
- const debugPendingIdlePostFramesRef = useRef(false);
4484
- const debugPreviousStageRef = useRef(null);
4837
+ const debugRenderOrdinalRef = useRef2(0);
4838
+ const overlayRef = useRef2(null);
4839
+ const debugFinalizeSignatureRef = useRef2(null);
4840
+ const debugFrameHandleRef = useRef2(null);
4841
+ const debugFrameOrdinalRef = useRef2(0);
4842
+ const debugAnimateTailFramesRef = useRef2([]);
4843
+ const debugIdlePostFrameHandleRef = useRef2(null);
4844
+ const debugIdlePostFrameOrdinalRef = useRef2(0);
4845
+ const debugIdlePostFrameTokenRef = useRef2(0);
4846
+ const debugPendingIdlePostFramesRef = useRef2(false);
4847
+ const debugPreviousStageRef = useRef2(null);
4848
+ debugRenderOrdinalRef.current += 1;
4849
+ const debugRenderOrdinal = debugRenderOrdinalRef.current;
4485
4850
  const {
4486
4851
  debugInstanceId,
4852
+ debugRenderOrdinal: hookRenderOrdinal,
4487
4853
  committedMeasurement,
4488
4854
  domMeasurementRequestKey,
4855
+ domMeasurementKey,
4489
4856
  flowTextRef,
4490
4857
  ref,
4491
4858
  measurementLayerRef,
4859
+ measurementBackend,
4492
4860
  renderText,
4493
4861
  segments,
4494
4862
  layoutContext,
4495
4863
  state,
4496
- useContentInlineSize
4864
+ useContentInlineSize,
4865
+ finalizeMorphTransition: finalizeMorphTransition2,
4866
+ timelineRef
4497
4867
  } = useMorphTransition(text, className);
4498
4868
  const plan = state.plan;
4499
- const shouldRenderOverlay = state.stage !== "idle" && plan !== null;
4869
+ let visibleGlyphPlan = plan;
4870
+ if (state.stage === "idle" && state.measurement !== null) {
4871
+ visibleGlyphPlan = createSteadyGlyphPlan(state.measurement);
4872
+ }
4873
+ let sourceSliceWhiteSpace = "inherit";
4874
+ if (committedMeasurement !== null) {
4875
+ sourceSliceWhiteSpace = resolveGlyphSliceWhiteSpace(committedMeasurement.snapshot);
4876
+ }
4877
+ let targetSliceWhiteSpace = "inherit";
4878
+ if (state.measurement !== null) {
4879
+ targetSliceWhiteSpace = resolveGlyphSliceWhiteSpace(state.measurement.snapshot);
4880
+ }
4881
+ const shouldRenderGlyphLayer2 = shouldRenderGlyphLayer(state.stage, visibleGlyphPlan, state.measurement);
4882
+ const shouldHideFlowText = shouldRenderGlyphLayer2;
4500
4883
  const shouldRenderMeasurementLayer = domMeasurementRequestKey !== null;
4501
4884
  const flowText = resolveFlowText(committedMeasurement, state.measurement, text);
4502
- useLayoutEffect(() => {
4885
+ useLayoutEffect2(() => {
4503
4886
  const config = readTorphDebugConfig();
4504
4887
  if (!shouldRunTorphInstrumentation(config)) {
4505
4888
  return;
@@ -4507,11 +4890,264 @@ function ActiveTorph({
4507
4890
  logTorphDebug(debugInstanceId, "effect:trace-meta", {
4508
4891
  traceSchemaVersion: TORPH_TRACE_SCHEMA_VERSION,
4509
4892
  includesIdlePostFrame: true,
4893
+ includesIdlePostFrameLifecycle: true,
4510
4894
  includesViewportAnchors: true,
4511
- includesRootOriginRefine: true
4895
+ includesRootOriginRefine: true,
4896
+ includesFullGlyphLayouts: true,
4897
+ includesIdleVisibleGlyphLayer: true,
4898
+ includesPreciseGlyphGeometry: true,
4899
+ includesAnimateTailFrames: true,
4900
+ includesLiveNodeStyles: true
4512
4901
  });
4513
4902
  }, [debugInstanceId]);
4514
- useLayoutEffect(() => {
4903
+ useLayoutEffect2(() => {
4904
+ const config = readTorphDebugConfig();
4905
+ if (!shouldRunTorphInstrumentation(config)) {
4906
+ debugFinalizeSignatureRef.current = null;
4907
+ return;
4908
+ }
4909
+ if (state.stage !== "animate" || state.measurement === null || plan === null) {
4910
+ debugFinalizeSignatureRef.current = null;
4911
+ }
4912
+ }, [plan, state.measurement, state.stage]);
4913
+ const logAnimateFinalizeSnapshot = (measurement, activePlan, reason, barrier, signal, event) => {
4914
+ const config = readTorphDebugConfig();
4915
+ if (!shouldRunTorphInstrumentation(config)) {
4916
+ return;
4917
+ }
4918
+ const root = ref.current;
4919
+ const overlayNode = overlayRef.current;
4920
+ const flowNode = flowTextRef.current;
4921
+ if (root === null || overlayNode === null || flowNode === null) {
4922
+ return;
4923
+ }
4924
+ const overlayLiveSnapshot = measureOverlayBoxSnapshot(root, overlayNode, "live");
4925
+ const overlayExitSnapshot = measureOverlayBoxSnapshot(root, overlayNode, "exit");
4926
+ const flowSnapshot = measureLiveFlowSnapshot(root, flowNode);
4927
+ if (overlayLiveSnapshot === null || flowSnapshot === null) {
4928
+ return;
4929
+ }
4930
+ const overlayLiveDrift = measureSnapshotDrift(measurement.snapshot, overlayLiveSnapshot);
4931
+ const flowDrift = measureSnapshotDrift(measurement.snapshot, flowSnapshot);
4932
+ const rootRect = root.getBoundingClientRect();
4933
+ const flowRect = flowNode.getBoundingClientRect();
4934
+ const overlayRect = overlayNode.getBoundingClientRect();
4935
+ const signature = JSON.stringify({
4936
+ text,
4937
+ renderText: measurement.snapshot.renderText,
4938
+ reason,
4939
+ signal,
4940
+ barrier: summarizeMorphFinalizeBarrier(barrier),
4941
+ overlayLiveDrift: summarizeSnapshotDrift(overlayLiveDrift),
4942
+ flowDrift: summarizeSnapshotDrift(flowDrift),
4943
+ overlayWidth: roundDebugValue(overlayRect.width),
4944
+ rootWidth: roundDebugValue(rootRect.width)
4945
+ });
4946
+ if (debugFinalizeSignatureRef.current === signature) {
4947
+ return;
4948
+ }
4949
+ debugFinalizeSignatureRef.current = signature;
4950
+ let morphRole = null;
4951
+ let morphKey = null;
4952
+ let morphGlyph = null;
4953
+ const target = event?.target;
4954
+ if (target instanceof HTMLElement) {
4955
+ morphRole = target.dataset.morphRole ?? null;
4956
+ morphKey = target.dataset.morphKey ?? null;
4957
+ morphGlyph = target.dataset.morphGlyph ?? null;
4958
+ }
4959
+ logTorphDebug(debugInstanceId, "effect:animate-finalize-snapshot", {
4960
+ text,
4961
+ reason,
4962
+ propertyName: event?.propertyName ?? null,
4963
+ finalizeSignal: signal,
4964
+ morphRole,
4965
+ morphKey,
4966
+ morphGlyph,
4967
+ barrier: summarizeMorphFinalizeBarrier(barrier),
4968
+ target: {
4969
+ layoutInlineSize: roundDebugValue(measurement.layoutInlineSize),
4970
+ reservedInlineSize: roundDebugValue(measurement.reservedInlineSize),
4971
+ flowInlineSize: roundDebugValue(measurement.flowInlineSize),
4972
+ rootOrigin: {
4973
+ left: roundDebugValue(measurement.rootOrigin.left),
4974
+ top: roundDebugValue(measurement.rootOrigin.top)
4975
+ },
4976
+ snapshot: summarizeDebugSnapshot(measurement.snapshot)
4977
+ },
4978
+ plan: {
4979
+ frameWidth: roundDebugValue(activePlan.frameWidth),
4980
+ frameHeight: roundDebugValue(activePlan.frameHeight),
4981
+ layoutInlineSizeFrom: roundDebugValue(activePlan.layoutInlineSizeFrom),
4982
+ layoutInlineSizeTo: roundDebugValue(activePlan.layoutInlineSizeTo),
4983
+ sourceRenderText: activePlan.sourceRenderText,
4984
+ targetRenderText: activePlan.targetRenderText
4985
+ },
4986
+ overlayLive: summarizeDebugSnapshot(overlayLiveSnapshot),
4987
+ overlayLiveGlyphs: summarizeDebugGlyphs(overlayLiveSnapshot),
4988
+ overlayLiveGlyphsPrecise: summarizePreciseGlyphs(overlayLiveSnapshot, rootRect),
4989
+ overlayLiveDrift: summarizeSnapshotDrift(overlayLiveDrift),
4990
+ overlayExit: summarizeDebugSnapshot(overlayExitSnapshot),
4991
+ overlayExitGlyphs: summarizeDebugGlyphs(overlayExitSnapshot),
4992
+ flow: summarizeDebugSnapshot(flowSnapshot),
4993
+ flowGlyphs: summarizeDebugGlyphs(flowSnapshot),
4994
+ flowGlyphsPrecise: summarizePreciseGlyphs(flowSnapshot, rootRect),
4995
+ flowDrift: summarizeSnapshotDrift(flowDrift),
4996
+ rootOriginDrift: summarizeDebugRootOriginDrift(measurement, rootRect),
4997
+ overlayLiveViewportAnchors: summarizeDebugViewportAnchors(overlayLiveSnapshot, rootRect),
4998
+ overlayExitViewportAnchors: summarizeDebugViewportAnchors(overlayExitSnapshot, rootRect),
4999
+ flowViewportAnchors: summarizeDebugViewportAnchors(flowSnapshot, rootRect),
5000
+ rootBox: {
5001
+ width: roundDebugValue(rootRect.width),
5002
+ height: roundDebugValue(rootRect.height)
5003
+ },
5004
+ overlayBox: {
5005
+ width: roundDebugValue(overlayRect.width),
5006
+ height: roundDebugValue(overlayRect.height)
5007
+ },
5008
+ flowBox: {
5009
+ width: roundDebugValue(flowRect.width),
5010
+ height: roundDebugValue(flowRect.height)
5011
+ },
5012
+ rootBoxPrecise: summarizePreciseRect(rootRect),
5013
+ overlayBoxPrecise: summarizePreciseRect(overlayRect),
5014
+ flowBoxPrecise: summarizePreciseRect(flowRect),
5015
+ rootRuntimeStyles: summarizeRootRuntimeStyles(root),
5016
+ overlayLiveNodeStyles: summarizeLiveNodeStyles(overlayNode),
5017
+ animateTailFrames: debugAnimateTailFramesRef.current
5018
+ });
5019
+ debugAnimateTailFramesRef.current = [];
5020
+ };
5021
+ useLayoutEffect2(() => {
5022
+ if (state.stage !== "animate" || state.measurement === null || plan === null) {
5023
+ return;
5024
+ }
5025
+ const root = ref.current;
5026
+ if (root === null) {
5027
+ return;
5028
+ }
5029
+ const measurement = state.measurement;
5030
+ const hasMoveTransitions = plan.liveItems.some((item) => item.kind === "move");
5031
+ let barrier = createMorphFinalizeBarrier(hasMoveTransitions);
5032
+ let finalizeScheduled = false;
5033
+ let finalizeCommitted = false;
5034
+ let finalizeFrame = null;
5035
+ const armedAt = performance.now();
5036
+ const finalizeNow = (reason, signal, barrierState, event) => {
5037
+ if (finalizeCommitted) {
5038
+ return;
5039
+ }
5040
+ finalizeCommitted = true;
5041
+ const target = event?.target;
5042
+ let morphRole = null;
5043
+ let morphKey = null;
5044
+ let morphGlyph = null;
5045
+ if (target instanceof HTMLElement) {
5046
+ morphRole = target.dataset.morphRole ?? null;
5047
+ morphKey = target.dataset.morphKey ?? null;
5048
+ morphGlyph = target.dataset.morphGlyph ?? null;
5049
+ }
5050
+ logAnimateFinalizeSnapshot(measurement, plan, reason, barrierState, signal, event);
5051
+ finalizeMorphTransition2(measurement, reason);
5052
+ const config = readTorphDebugConfig();
5053
+ if (!shouldRunTorphInstrumentation(config)) {
5054
+ return;
5055
+ }
5056
+ logTorphDebug(debugInstanceId, "effect:finalize-authority", {
5057
+ text,
5058
+ reason,
5059
+ propertyName: event?.propertyName ?? null,
5060
+ morphRole,
5061
+ morphKey,
5062
+ morphGlyph,
5063
+ elapsedMs: roundDebugValue(performance.now() - armedAt),
5064
+ hasMoveTransitions,
5065
+ finalizeSignal: signal,
5066
+ barrier: summarizeMorphFinalizeBarrier(barrierState),
5067
+ measurement: summarizeDebugMeasurement(measurement)
5068
+ });
5069
+ };
5070
+ const scheduleFinalize = (reason, signal, barrierState, event) => {
5071
+ if (finalizeCommitted || finalizeScheduled) {
5072
+ return;
5073
+ }
5074
+ finalizeScheduled = true;
5075
+ if (timelineRef.current.finalizeTimer !== null) {
5076
+ window.clearTimeout(timelineRef.current.finalizeTimer);
5077
+ timelineRef.current.finalizeTimer = null;
5078
+ }
5079
+ logTorphDebug(debugInstanceId, "effect:finalize-raf-schedule", {
5080
+ text,
5081
+ reason,
5082
+ propertyName: event?.propertyName ?? null,
5083
+ finalizeSignal: signal,
5084
+ elapsedMs: roundDebugValue(performance.now() - armedAt),
5085
+ barrier: summarizeMorphFinalizeBarrier(barrierState)
5086
+ });
5087
+ finalizeFrame = requestAnimationFrame(() => {
5088
+ finalizeFrame = null;
5089
+ finalizeNow(reason, signal, barrierState, event);
5090
+ });
5091
+ };
5092
+ const onTransitionEnd = (event) => {
5093
+ const signal = resolveMorphFinalizeSignal(event, hasMoveTransitions);
5094
+ if (signal === null) {
5095
+ return;
5096
+ }
5097
+ barrier = recordMorphFinalizeSignal(barrier, signal);
5098
+ const target = event.target;
5099
+ let morphRole = null;
5100
+ let morphKey = null;
5101
+ let morphGlyph = null;
5102
+ if (target instanceof HTMLElement) {
5103
+ morphRole = target.dataset.morphRole ?? null;
5104
+ morphKey = target.dataset.morphKey ?? null;
5105
+ morphGlyph = target.dataset.morphGlyph ?? null;
5106
+ }
5107
+ logTorphDebug(debugInstanceId, "effect:finalize-barrier-progress", {
5108
+ text,
5109
+ propertyName: event.propertyName,
5110
+ signal,
5111
+ morphRole,
5112
+ morphKey,
5113
+ morphGlyph,
5114
+ elapsedMs: roundDebugValue(performance.now() - armedAt),
5115
+ barrier: summarizeMorphFinalizeBarrier(barrier)
5116
+ });
5117
+ if (!isMorphFinalizeBarrierSatisfied(barrier)) {
5118
+ return;
5119
+ }
5120
+ scheduleFinalize("transitionend", signal, barrier, event);
5121
+ };
5122
+ root.addEventListener("transitionend", onTransitionEnd);
5123
+ if (timelineRef.current.finalizeTimer !== null) {
5124
+ window.clearTimeout(timelineRef.current.finalizeTimer);
5125
+ }
5126
+ timelineRef.current.finalizeTimer = window.setTimeout(() => {
5127
+ timelineRef.current.finalizeTimer = null;
5128
+ scheduleFinalize("watchdog-timeout", null, barrier);
5129
+ }, MORPH.durationMs + 32);
5130
+ return () => {
5131
+ root.removeEventListener("transitionend", onTransitionEnd);
5132
+ if (finalizeFrame !== null) {
5133
+ cancelAnimationFrame(finalizeFrame);
5134
+ finalizeFrame = null;
5135
+ }
5136
+ if (timelineRef.current.finalizeTimer !== null) {
5137
+ window.clearTimeout(timelineRef.current.finalizeTimer);
5138
+ timelineRef.current.finalizeTimer = null;
5139
+ }
5140
+ };
5141
+ }, [
5142
+ debugInstanceId,
5143
+ finalizeMorphTransition2,
5144
+ plan,
5145
+ state.measurement,
5146
+ state.stage,
5147
+ text,
5148
+ timelineRef
5149
+ ]);
5150
+ useLayoutEffect2(() => {
4515
5151
  const config = readTorphDebugConfig();
4516
5152
  if (!shouldRunTorphInstrumentation(config)) {
4517
5153
  debugPendingIdlePostFramesRef.current = false;
@@ -4522,25 +5158,34 @@ function ActiveTorph({
4522
5158
  if (previousStage !== state.stage) {
4523
5159
  if (state.stage === "idle") {
4524
5160
  debugPendingIdlePostFramesRef.current = true;
5161
+ debugIdlePostFrameTokenRef.current += 1;
5162
+ } else {
5163
+ debugPendingIdlePostFramesRef.current = false;
4525
5164
  }
4526
5165
  logTorphDebug(debugInstanceId, "effect:stage-transition", {
5166
+ traceSchemaVersion: TORPH_TRACE_SCHEMA_VERSION,
5167
+ renderOrdinal: debugRenderOrdinal,
4527
5168
  text,
4528
5169
  fromStage: previousStage,
4529
5170
  toStage: state.stage,
4530
5171
  committed: summarizeDebugMeasurement(committedMeasurement),
4531
5172
  stateMeasurement: summarizeDebugMeasurement(state.measurement),
4532
- flowText
5173
+ flowText,
5174
+ domMeasurementRequestKey,
5175
+ domMeasurementKey,
5176
+ measurementBackend
4533
5177
  });
4534
5178
  debugPreviousStageRef.current = state.stage;
4535
5179
  }
4536
5180
  }, [committedMeasurement, debugInstanceId, flowText, state, text]);
4537
- useLayoutEffect(() => {
5181
+ useLayoutEffect2(() => {
4538
5182
  const config = readTorphDebugConfig();
4539
5183
  if (!shouldRunTorphInstrumentation(config)) {
4540
5184
  if (debugFrameHandleRef.current !== null) {
4541
5185
  cancelAnimationFrame(debugFrameHandleRef.current);
4542
5186
  debugFrameHandleRef.current = null;
4543
5187
  }
5188
+ debugAnimateTailFramesRef.current = [];
4544
5189
  return;
4545
5190
  }
4546
5191
  if (state.stage === "idle" || state.measurement === null) {
@@ -4549,6 +5194,7 @@ function ActiveTorph({
4549
5194
  debugFrameHandleRef.current = null;
4550
5195
  }
4551
5196
  debugFrameOrdinalRef.current = 0;
5197
+ debugAnimateTailFramesRef.current = [];
4552
5198
  return;
4553
5199
  }
4554
5200
  debugFrameOrdinalRef.current = 0;
@@ -4593,6 +5239,19 @@ function ActiveTorph({
4593
5239
  if (flowSnapshot !== null) {
4594
5240
  flowDrift = summarizeSnapshotDrift(measureSnapshotDrift(measurement.snapshot, flowSnapshot));
4595
5241
  }
5242
+ debugAnimateTailFramesRef.current.push({
5243
+ frame: debugFrameOrdinalRef.current,
5244
+ stateStage: state.stage,
5245
+ rootBoxPrecise: summarizePreciseRect(rootRect),
5246
+ overlayBoxPrecise: summarizePreciseRect(overlayRect),
5247
+ flowBoxPrecise: summarizePreciseRect(flowRect),
5248
+ overlayLiveGlyphsPrecise: summarizePreciseGlyphs(overlayLiveSnapshot, rootRect),
5249
+ flowGlyphsPrecise: summarizePreciseGlyphs(flowSnapshot, rootRect),
5250
+ overlayLiveNodeStyles: summarizeLiveNodeStyles(overlayNode)
5251
+ });
5252
+ if (debugAnimateTailFramesRef.current.length > 8) {
5253
+ debugAnimateTailFramesRef.current.shift();
5254
+ }
4596
5255
  let planSummary = null;
4597
5256
  if (plan !== null) {
4598
5257
  planSummary = {
@@ -4624,13 +5283,17 @@ function ActiveTorph({
4624
5283
  flowBox: summarizeDebugRect(flowRect),
4625
5284
  rootOriginDrift: summarizeDebugRootOriginDrift(measurement, rootRect),
4626
5285
  overlayLive: summarizeDebugSnapshot(overlayLiveSnapshot),
5286
+ overlayLiveGlyphs: summarizeDebugGlyphs(overlayLiveSnapshot),
4627
5287
  overlayLiveViewportAnchors: summarizeDebugViewportAnchors(overlayLiveSnapshot, rootRect),
4628
5288
  overlayLiveDrift,
4629
5289
  overlayExit: summarizeDebugSnapshot(overlayExitSnapshot),
5290
+ overlayExitGlyphs: summarizeDebugGlyphs(overlayExitSnapshot),
4630
5291
  overlayExitViewportAnchors: summarizeDebugViewportAnchors(overlayExitSnapshot, rootRect),
4631
5292
  flow: summarizeDebugSnapshot(flowSnapshot),
5293
+ flowGlyphs: summarizeDebugGlyphs(flowSnapshot),
4632
5294
  flowViewportAnchors: summarizeDebugViewportAnchors(flowSnapshot, rootRect),
4633
- flowDrift
5295
+ flowDrift,
5296
+ rootRuntimeStyles: summarizeRootRuntimeStyles(root)
4634
5297
  });
4635
5298
  debugFrameOrdinalRef.current += 1;
4636
5299
  debugFrameHandleRef.current = requestAnimationFrame(captureFrame);
@@ -4644,11 +5307,22 @@ function ActiveTorph({
4644
5307
  }
4645
5308
  };
4646
5309
  }, [committedMeasurement, debugInstanceId, flowText, plan, ref, state, text]);
4647
- useLayoutEffect(() => {
5310
+ useLayoutEffect2(() => {
4648
5311
  const config = readTorphDebugConfig();
4649
5312
  if (!shouldRunTorphInstrumentation(config)) {
4650
5313
  debugPendingIdlePostFramesRef.current = false;
4651
5314
  if (debugIdlePostFrameHandleRef.current !== null) {
5315
+ logTorphDebug(debugInstanceId, "effect:idle-post-frame-cleanup", {
5316
+ traceSchemaVersion: TORPH_TRACE_SCHEMA_VERSION,
5317
+ reason: "instrumentation-disabled",
5318
+ renderOrdinal: debugRenderOrdinal,
5319
+ hookRenderOrdinal,
5320
+ token: debugIdlePostFrameTokenRef.current,
5321
+ handle: debugIdlePostFrameHandleRef.current,
5322
+ domMeasurementRequestKey,
5323
+ domMeasurementKey,
5324
+ measurementBackend
5325
+ });
4652
5326
  cancelAnimationFrame(debugIdlePostFrameHandleRef.current);
4653
5327
  debugIdlePostFrameHandleRef.current = null;
4654
5328
  }
@@ -4660,28 +5334,93 @@ function ActiveTorph({
4660
5334
  if (state.stage !== "idle" || state.measurement === null) {
4661
5335
  return;
4662
5336
  }
4663
- debugPendingIdlePostFramesRef.current = false;
4664
5337
  debugIdlePostFrameOrdinalRef.current = 0;
5338
+ const token = debugIdlePostFrameTokenRef.current;
5339
+ const scheduledRenderOrdinal = debugRenderOrdinal;
5340
+ const scheduledHookRenderOrdinal = hookRenderOrdinal;
4665
5341
  const measurement = state.measurement;
4666
5342
  let remainingFrames = 3;
4667
5343
  let cancelled = false;
5344
+ logTorphDebug(debugInstanceId, "effect:idle-post-frame-arm", {
5345
+ traceSchemaVersion: TORPH_TRACE_SCHEMA_VERSION,
5346
+ renderOrdinal: scheduledRenderOrdinal,
5347
+ hookRenderOrdinal: scheduledHookRenderOrdinal,
5348
+ token,
5349
+ text,
5350
+ stateStage: state.stage,
5351
+ flowText,
5352
+ committed: summarizeDebugMeasurement(committedMeasurement),
5353
+ stateMeasurement: summarizeDebugMeasurement(measurement),
5354
+ domMeasurementRequestKey,
5355
+ domMeasurementKey,
5356
+ measurementBackend
5357
+ });
4668
5358
  const captureIdlePostFrame = () => {
4669
5359
  if (cancelled) {
4670
5360
  return;
4671
5361
  }
5362
+ if (debugPendingIdlePostFramesRef.current) {
5363
+ debugPendingIdlePostFramesRef.current = false;
5364
+ }
5365
+ logTorphDebug(debugInstanceId, "effect:idle-post-frame-fire", {
5366
+ traceSchemaVersion: TORPH_TRACE_SCHEMA_VERSION,
5367
+ renderOrdinal: debugRenderOrdinalRef.current,
5368
+ hookRenderOrdinal,
5369
+ scheduledRenderOrdinal,
5370
+ scheduledHookRenderOrdinal,
5371
+ token,
5372
+ frame: debugIdlePostFrameOrdinalRef.current,
5373
+ remainingFrames,
5374
+ text,
5375
+ stateStage: state.stage,
5376
+ flowText
5377
+ });
4672
5378
  const root = ref.current;
4673
5379
  const flowNode = flowTextRef.current;
4674
- if (root === null || flowNode === null) {
5380
+ const overlayNode = overlayRef.current;
5381
+ if (root === null || flowNode === null || overlayNode === null) {
5382
+ logTorphDebug(debugInstanceId, "effect:idle-post-frame-skip", {
5383
+ traceSchemaVersion: TORPH_TRACE_SCHEMA_VERSION,
5384
+ renderOrdinal: debugRenderOrdinalRef.current,
5385
+ hookRenderOrdinal,
5386
+ scheduledRenderOrdinal,
5387
+ scheduledHookRenderOrdinal,
5388
+ token,
5389
+ text,
5390
+ stateStage: state.stage,
5391
+ frame: debugIdlePostFrameOrdinalRef.current,
5392
+ hasRoot: root !== null,
5393
+ hasFlowNode: flowNode !== null,
5394
+ hasOverlayNode: overlayNode !== null,
5395
+ flowText,
5396
+ committed: summarizeDebugMeasurement(committedMeasurement),
5397
+ stateMeasurement: summarizeDebugMeasurement(measurement)
5398
+ });
4675
5399
  return;
4676
5400
  }
4677
5401
  const rootRect = root.getBoundingClientRect();
4678
5402
  const flowRect = flowNode.getBoundingClientRect();
5403
+ const overlayRect = overlayNode.getBoundingClientRect();
4679
5404
  const flowSnapshot = measureLiveFlowSnapshot(root, flowNode);
5405
+ let visibleSnapshot = null;
5406
+ if (overlayNode !== null) {
5407
+ visibleSnapshot = measureOverlayBoxSnapshot(root, overlayNode, "live");
5408
+ }
4680
5409
  let flowDrift = null;
4681
5410
  if (flowSnapshot !== null) {
4682
5411
  flowDrift = summarizeSnapshotDrift(measureSnapshotDrift(measurement.snapshot, flowSnapshot));
4683
5412
  }
5413
+ let visibleDrift = null;
5414
+ if (visibleSnapshot !== null) {
5415
+ visibleDrift = summarizeSnapshotDrift(measureSnapshotDrift(measurement.snapshot, visibleSnapshot));
5416
+ }
4684
5417
  logTorphDebug(debugInstanceId, "effect:idle-post-frame", {
5418
+ traceSchemaVersion: TORPH_TRACE_SCHEMA_VERSION,
5419
+ renderOrdinal: debugRenderOrdinalRef.current,
5420
+ hookRenderOrdinal,
5421
+ scheduledRenderOrdinal,
5422
+ scheduledHookRenderOrdinal,
5423
+ token,
4685
5424
  text,
4686
5425
  frame: debugIdlePostFrameOrdinalRef.current,
4687
5426
  stateStage: state.stage,
@@ -4690,11 +5429,24 @@ function ActiveTorph({
4690
5429
  committed: summarizeDebugMeasurement(committedMeasurement),
4691
5430
  stateMeasurement: summarizeDebugMeasurement(measurement),
4692
5431
  rootBox: summarizeDebugRect(rootRect),
5432
+ rootBoxPrecise: summarizePreciseRect(rootRect),
5433
+ overlayBox: summarizeDebugRect(overlayRect),
5434
+ overlayBoxPrecise: summarizePreciseRect(overlayRect),
4693
5435
  flowBox: summarizeDebugRect(flowRect),
5436
+ flowBoxPrecise: summarizePreciseRect(flowRect),
5437
+ rootRuntimeStyles: summarizeRootRuntimeStyles(root),
4694
5438
  rootOriginDrift: summarizeDebugRootOriginDrift(measurement, rootRect),
4695
5439
  flow: summarizeDebugSnapshot(flowSnapshot),
5440
+ flowGlyphs: summarizeDebugGlyphs(flowSnapshot),
5441
+ flowGlyphsPrecise: summarizePreciseGlyphs(flowSnapshot, rootRect),
4696
5442
  flowViewportAnchors: summarizeDebugViewportAnchors(flowSnapshot, rootRect),
4697
- flowDrift
5443
+ flowDrift,
5444
+ visible: summarizeDebugSnapshot(visibleSnapshot),
5445
+ visibleGlyphs: summarizeDebugGlyphs(visibleSnapshot),
5446
+ visibleGlyphsPrecise: summarizePreciseGlyphs(visibleSnapshot, rootRect),
5447
+ visibleViewportAnchors: summarizeDebugViewportAnchors(visibleSnapshot, rootRect),
5448
+ visibleDrift,
5449
+ visibleLiveNodeStyles: summarizeLiveNodeStyles(overlayNode)
4698
5450
  });
4699
5451
  debugIdlePostFrameOrdinalRef.current += 1;
4700
5452
  remainingFrames -= 1;
@@ -4703,121 +5455,77 @@ function ActiveTorph({
4703
5455
  return;
4704
5456
  }
4705
5457
  debugIdlePostFrameHandleRef.current = requestAnimationFrame(captureIdlePostFrame);
5458
+ logTorphDebug(debugInstanceId, "effect:idle-post-frame-reschedule", {
5459
+ traceSchemaVersion: TORPH_TRACE_SCHEMA_VERSION,
5460
+ renderOrdinal: debugRenderOrdinalRef.current,
5461
+ hookRenderOrdinal,
5462
+ scheduledRenderOrdinal,
5463
+ scheduledHookRenderOrdinal,
5464
+ token,
5465
+ handle: debugIdlePostFrameHandleRef.current,
5466
+ nextFrame: debugIdlePostFrameOrdinalRef.current,
5467
+ remainingFrames,
5468
+ text,
5469
+ stateStage: state.stage,
5470
+ flowText
5471
+ });
4706
5472
  };
4707
5473
  debugIdlePostFrameHandleRef.current = requestAnimationFrame(captureIdlePostFrame);
5474
+ logTorphDebug(debugInstanceId, "effect:idle-post-frame-schedule", {
5475
+ traceSchemaVersion: TORPH_TRACE_SCHEMA_VERSION,
5476
+ renderOrdinal: scheduledRenderOrdinal,
5477
+ hookRenderOrdinal: scheduledHookRenderOrdinal,
5478
+ token,
5479
+ handle: debugIdlePostFrameHandleRef.current,
5480
+ remainingFrames,
5481
+ text,
5482
+ stateStage: state.stage,
5483
+ flowText,
5484
+ domMeasurementRequestKey,
5485
+ domMeasurementKey,
5486
+ measurementBackend
5487
+ });
4708
5488
  return () => {
4709
5489
  cancelled = true;
4710
5490
  if (debugIdlePostFrameHandleRef.current !== null) {
5491
+ logTorphDebug(debugInstanceId, "effect:idle-post-frame-cleanup", {
5492
+ traceSchemaVersion: TORPH_TRACE_SCHEMA_VERSION,
5493
+ reason: "effect-rerun-or-unmount",
5494
+ renderOrdinal: debugRenderOrdinalRef.current,
5495
+ hookRenderOrdinal,
5496
+ scheduledRenderOrdinal,
5497
+ scheduledHookRenderOrdinal,
5498
+ token,
5499
+ handle: debugIdlePostFrameHandleRef.current,
5500
+ completedFrames: debugIdlePostFrameOrdinalRef.current,
5501
+ remainingFrames,
5502
+ text,
5503
+ stateStage: state.stage,
5504
+ flowText,
5505
+ domMeasurementRequestKey,
5506
+ domMeasurementKey,
5507
+ measurementBackend
5508
+ });
4711
5509
  cancelAnimationFrame(debugIdlePostFrameHandleRef.current);
4712
5510
  debugIdlePostFrameHandleRef.current = null;
4713
5511
  }
4714
5512
  };
4715
5513
  }, [committedMeasurement, debugInstanceId, flowText, ref, state, text]);
4716
- useLayoutEffect(() => {
5514
+ useLayoutEffect2(() => {
4717
5515
  return () => {
4718
5516
  if (debugIdlePostFrameHandleRef.current !== null) {
5517
+ logTorphDebug(debugInstanceId, "effect:idle-post-frame-cleanup", {
5518
+ traceSchemaVersion: TORPH_TRACE_SCHEMA_VERSION,
5519
+ reason: "component-unmount",
5520
+ renderOrdinal: debugRenderOrdinalRef.current,
5521
+ token: debugIdlePostFrameTokenRef.current,
5522
+ handle: debugIdlePostFrameHandleRef.current
5523
+ });
4719
5524
  cancelAnimationFrame(debugIdlePostFrameHandleRef.current);
4720
5525
  debugIdlePostFrameHandleRef.current = null;
4721
5526
  }
4722
5527
  };
4723
5528
  }, []);
4724
- useLayoutEffect(() => {
4725
- const config = readTorphDebugConfig();
4726
- if (!shouldRunTorphInstrumentation(config)) {
4727
- debugFinalizeSignatureRef.current = null;
4728
- return;
4729
- }
4730
- if (state.stage !== "animate" || state.measurement === null || plan === null) {
4731
- debugFinalizeSignatureRef.current = null;
4732
- return;
4733
- }
4734
- const root = ref.current;
4735
- const overlayNode = overlayRef.current;
4736
- const flowNode = flowTextRef.current;
4737
- if (root === null || overlayNode === null || flowNode === null) {
4738
- debugFinalizeSignatureRef.current = null;
4739
- return;
4740
- }
4741
- const measurement = state.measurement;
4742
- let handled = false;
4743
- const capture = (event) => {
4744
- if (handled) {
4745
- return;
4746
- }
4747
- if (event.propertyName !== "transform") {
4748
- return;
4749
- }
4750
- const target = event.target;
4751
- if (!(target instanceof HTMLElement)) {
4752
- return;
4753
- }
4754
- if (target.dataset.morphRole !== "live") {
4755
- return;
4756
- }
4757
- handled = true;
4758
- const overlayLiveSnapshot = measureOverlayBoxSnapshot(root, overlayNode, "live");
4759
- const overlayExitSnapshot = measureOverlayBoxSnapshot(root, overlayNode, "exit");
4760
- const flowSnapshot = measureLiveFlowSnapshot(root, flowNode);
4761
- if (overlayLiveSnapshot === null || flowSnapshot === null) {
4762
- return;
4763
- }
4764
- const overlayLiveDrift = measureSnapshotDrift(measurement.snapshot, overlayLiveSnapshot);
4765
- const flowDrift = measureSnapshotDrift(measurement.snapshot, flowSnapshot);
4766
- const rootRect = root.getBoundingClientRect();
4767
- const flowRect = flowNode.getBoundingClientRect();
4768
- const overlayRect = overlayNode.getBoundingClientRect();
4769
- const signature = JSON.stringify({
4770
- text,
4771
- renderText: measurement.snapshot.renderText,
4772
- overlayLiveDrift: summarizeSnapshotDrift(overlayLiveDrift),
4773
- flowDrift: summarizeSnapshotDrift(flowDrift),
4774
- overlayWidth: roundDebugValue(overlayRect.width),
4775
- rootWidth: roundDebugValue(rootRect.width)
4776
- });
4777
- if (debugFinalizeSignatureRef.current === signature) {
4778
- return;
4779
- }
4780
- debugFinalizeSignatureRef.current = signature;
4781
- logTorphDebug(debugInstanceId, "effect:animate-finalize-snapshot", {
4782
- text,
4783
- target: {
4784
- layoutInlineSize: roundDebugValue(measurement.layoutInlineSize),
4785
- reservedInlineSize: roundDebugValue(measurement.reservedInlineSize),
4786
- flowInlineSize: roundDebugValue(measurement.flowInlineSize),
4787
- rootOrigin: {
4788
- left: roundDebugValue(measurement.rootOrigin.left),
4789
- top: roundDebugValue(measurement.rootOrigin.top)
4790
- },
4791
- snapshot: summarizeDebugSnapshot(measurement.snapshot)
4792
- },
4793
- overlayLive: summarizeDebugSnapshot(overlayLiveSnapshot),
4794
- overlayLiveDrift: summarizeSnapshotDrift(overlayLiveDrift),
4795
- overlayExit: summarizeDebugSnapshot(overlayExitSnapshot),
4796
- flow: summarizeDebugSnapshot(flowSnapshot),
4797
- flowDrift: summarizeSnapshotDrift(flowDrift),
4798
- rootOriginDrift: summarizeDebugRootOriginDrift(measurement, rootRect),
4799
- overlayLiveViewportAnchors: summarizeDebugViewportAnchors(overlayLiveSnapshot, rootRect),
4800
- overlayExitViewportAnchors: summarizeDebugViewportAnchors(overlayExitSnapshot, rootRect),
4801
- flowViewportAnchors: summarizeDebugViewportAnchors(flowSnapshot, rootRect),
4802
- rootBox: {
4803
- width: roundDebugValue(rootRect.width),
4804
- height: roundDebugValue(rootRect.height)
4805
- },
4806
- overlayBox: {
4807
- width: roundDebugValue(overlayRect.width),
4808
- height: roundDebugValue(overlayRect.height)
4809
- },
4810
- flowBox: {
4811
- width: roundDebugValue(flowRect.width),
4812
- height: roundDebugValue(flowRect.height)
4813
- }
4814
- });
4815
- };
4816
- overlayNode.addEventListener("transitionend", capture);
4817
- return () => {
4818
- overlayNode.removeEventListener("transitionend", capture);
4819
- };
4820
- }, [debugInstanceId, plan, ref, state, text]);
4821
5529
  let measurementLayer = null;
4822
5530
  if (shouldRenderMeasurementLayer) {
4823
5531
  measurementLayer = /* @__PURE__ */ jsxDEV(MeasurementLayer, {
@@ -4828,11 +5536,13 @@ function ActiveTorph({
4828
5536
  }, undefined, false, undefined, this);
4829
5537
  }
4830
5538
  let overlay = null;
4831
- if (shouldRenderOverlay) {
5539
+ if (shouldRenderGlyphLayer2 && visibleGlyphPlan !== null) {
4832
5540
  overlay = /* @__PURE__ */ jsxDEV(MorphOverlay, {
4833
5541
  overlayRef,
4834
5542
  stage: state.stage,
4835
- plan
5543
+ plan: visibleGlyphPlan,
5544
+ sourceSliceWhiteSpace,
5545
+ targetSliceWhiteSpace
4836
5546
  }, undefined, false, undefined, this);
4837
5547
  }
4838
5548
  return /* @__PURE__ */ jsxDEV("div", {
@@ -4846,7 +5556,7 @@ function ActiveTorph({
4846
5556
  }, undefined, false, undefined, this),
4847
5557
  /* @__PURE__ */ jsxDEV("span", {
4848
5558
  "aria-hidden": "true",
4849
- style: getFallbackTextStyle(shouldRenderOverlay),
5559
+ style: getFallbackTextStyle(shouldHideFlowText),
4850
5560
  children: /* @__PURE__ */ jsxDEV("span", {
4851
5561
  ref: flowTextRef,
4852
5562
  children: flowText
@@ -4867,23 +5577,5 @@ function Torph({
4867
5577
  }, undefined, false, undefined, this);
4868
5578
  }
4869
5579
  export {
4870
- supportsIntrinsicWidthLock,
4871
- shouldHealIdleMeasurementFromFlow,
4872
- resolveMorphFrameBounds,
4873
- resolveFlowText,
4874
- resolveContentWidthLockInlineSize,
4875
- refineMeasurementWithLiveGeometry,
4876
- pairMorphCharacters,
4877
- needsMeasurementLayer,
4878
- measureMorphSnapshotFromLayer,
4879
- getRootStyle,
4880
- getRootDisplay,
4881
- getMeasurementLayerStyle,
4882
- getLiveTransition,
4883
- getLiveTransform,
4884
- getExitTransition,
4885
- getExitTransform,
4886
- buildMorphVisualBridge,
4887
- buildMorphPlan,
4888
5580
  Torph
4889
5581
  };