@myrmidon/gve-snapshot-rendition 1.0.2 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -71,12 +71,10 @@ const DEFAULT_SETTINGS = {
71
71
  `,
72
72
  },
73
73
  charAnimationId: undefined, // No character animation by default
74
- spreadTime: 1500,
75
74
  backwardFadeOutTime: 300,
76
75
  prologDuration: 800,
77
76
  // Hints - empty by default, to be filled by consumer code
78
77
  hints: {},
79
- hintMargin: 0,
80
78
  hintDesignWidth: 300,
81
79
  hintDesignHeight: 100,
82
80
  showHintHandles: false,
@@ -290,54 +288,6 @@ class AnimationEngine {
290
288
  this._logger.error("Animation execution error:", error);
291
289
  }
292
290
  }
293
- /**
294
- * Animate multiple elements with spreading transitions.
295
- * All elements are animated in parallel with GSAP.
296
- *
297
- * @param shifts - Map of element IDs to their shift amounts {x, y}
298
- * @param duration - Duration in milliseconds
299
- * @param rootElement - The root SVG element containing all elements
300
- * @returns Promise that resolves when all animations complete
301
- */
302
- async animateSpreading(shifts, duration, rootElement) {
303
- if (shifts.size === 0) {
304
- this._logger.debug("AnimationEngine", "No elements to spread");
305
- return;
306
- }
307
- this._logger.debug("AnimationEngine", `Animating spreading for ${shifts.size} elements`);
308
- this._logger.time("animateSpreading");
309
- const timeline = this._gsap.timeline();
310
- for (const [elementId, shift] of shifts.entries()) {
311
- const element = rootElement.querySelector(`#${elementId}`);
312
- if (!element) {
313
- this._logger.warn(`Element ${elementId} not found for spreading animation`);
314
- continue;
315
- }
316
- // Get current transform
317
- const currentTransform = element.getAttribute("transform") || "";
318
- const translateMatch = currentTransform.match(/translate\(([-\d.]+),\s*([-\d.]+)\)/);
319
- let currentX = 0;
320
- let currentY = 0;
321
- if (translateMatch) {
322
- currentX = parseFloat(translateMatch[1]);
323
- currentY = parseFloat(translateMatch[2]);
324
- }
325
- const newX = currentX + shift.x;
326
- const newY = currentY + shift.y;
327
- // Add to timeline (animations run in parallel with position 0)
328
- timeline.to(element, {
329
- attr: { transform: `translate(${newX}, ${newY})` },
330
- duration: duration / 1000, // Convert ms to seconds for GSAP
331
- ease: "power2.inOut",
332
- }, 0 // Start time: 0 means all animations start together
333
- );
334
- }
335
- // Wait for timeline to complete
336
- await new Promise((resolve) => {
337
- timeline.eventCallback("onComplete", resolve);
338
- });
339
- this._logger.timeEnd("animateSpreading");
340
- }
341
291
  /**
342
292
  * Fade in multiple elements (used for hilites).
343
293
  *
@@ -654,271 +604,6 @@ class BoundsCache {
654
604
  }
655
605
  }
656
606
 
657
- /**
658
- * Engine for calculating spreading (making room for new elements).
659
- * Implements the "stone in pond" ripple effect to preserve layout.
660
- */
661
- class SpreadingEngine {
662
- constructor(boundsCache, logger) {
663
- /**
664
- * Map of element ID to array of spreading metadata (one per version).
665
- * Each element can be shifted multiple times by different versions.
666
- */
667
- this._spreadingHistory = new Map();
668
- this._boundsCache = boundsCache;
669
- this._logger = logger;
670
- }
671
- /**
672
- * Track spreading metadata for elements affected by a version.
673
- * Called after spreading is applied to record what happened.
674
- *
675
- * @param versionIndex - Version index that caused the spreading
676
- * @param horizontalShifts - Map of element IDs to horizontal shifts
677
- * @param verticalShifts - Map of element IDs to vertical shifts
678
- */
679
- trackSpreading(versionIndex, horizontalShifts, verticalShifts) {
680
- // Collect all affected element IDs
681
- const affectedIds = new Set([
682
- ...horizontalShifts.keys(),
683
- ...verticalShifts.keys(),
684
- ]);
685
- for (const elementId of affectedIds) {
686
- const hShift = horizontalShifts.get(elementId) || 0;
687
- const vShift = verticalShifts.get(elementId) || 0;
688
- const metadata = {
689
- versionIndex,
690
- horizontalShift: hShift,
691
- verticalShift: vShift,
692
- };
693
- // Get or create history array for this element
694
- let history = this._spreadingHistory.get(elementId);
695
- if (!history) {
696
- history = [];
697
- this._spreadingHistory.set(elementId, history);
698
- }
699
- // Add to history
700
- history.push(metadata);
701
- this._logger.debug("Spreading", `Tracked spreading for ${elementId} at v${versionIndex}: h=${hShift}, v=${vShift}`);
702
- }
703
- }
704
- /**
705
- * Get spreading metadata for elements affected by a specific version.
706
- * Used when reversing spreading during backward navigation.
707
- *
708
- * @param versionIndex - Version index to get spreading for
709
- * @returns Map of element ID to spreading metadata
710
- */
711
- getSpreadingForVersion(versionIndex) {
712
- const result = new Map();
713
- for (const [elementId, history] of this._spreadingHistory.entries()) {
714
- // Find spreading caused by this version
715
- const metadata = history.find((m) => m.versionIndex === versionIndex);
716
- if (metadata) {
717
- result.set(elementId, metadata);
718
- }
719
- }
720
- return result;
721
- }
722
- /**
723
- * Remove spreading history for a specific version.
724
- * Called after spreading is reversed during backward navigation.
725
- *
726
- * @param versionIndex - Version index to remove history for
727
- */
728
- clearSpreadingForVersion(versionIndex) {
729
- for (const [elementId, history] of this._spreadingHistory.entries()) {
730
- // Remove entries for this version
731
- const filtered = history.filter((m) => m.versionIndex !== versionIndex);
732
- if (filtered.length === 0) {
733
- // No more history for this element
734
- this._spreadingHistory.delete(elementId);
735
- }
736
- else {
737
- this._spreadingHistory.set(elementId, filtered);
738
- }
739
- }
740
- this._logger.debug("Spreading", `Cleared spreading history for v${versionIndex}`);
741
- }
742
- /**
743
- * Calculate spreading required to make room for a new element.
744
- *
745
- * @param newElementBounds - Bounds of the element to be added
746
- * @param excludeIds - Element IDs to exclude from shifting (e.g., the new element itself)
747
- * @returns Spreading result with shift amounts for each affected element
748
- */
749
- calculateSpreading(newElementBounds, excludeIds = []) {
750
- this._logger.time("calculateSpreading");
751
- this._logger.debug("Spreading", "Calculating spreading for new element", newElementBounds);
752
- const horizontalShifts = new Map();
753
- const verticalShifts = new Map();
754
- // Get all existing elements
755
- const allIds = this._boundsCache
756
- .getAllIds()
757
- .filter((id) => !excludeIds.includes(id));
758
- // Calculate horizontal spreading
759
- const hShifts = this.calculateHorizontalSpreading(newElementBounds, allIds);
760
- hShifts.forEach((shift, id) => horizontalShifts.set(id, shift));
761
- // Calculate vertical spreading
762
- const vShifts = this.calculateVerticalSpreading(newElementBounds, allIds);
763
- vShifts.forEach((shift, id) => verticalShifts.set(id, shift));
764
- const hasShifts = horizontalShifts.size > 0 || verticalShifts.size > 0;
765
- this._logger.debug("Spreading", "Spreading calculation complete", {
766
- horizontalShifts: horizontalShifts.size,
767
- verticalShifts: verticalShifts.size,
768
- hasShifts,
769
- });
770
- this._logger.timeEnd("calculateSpreading");
771
- return { horizontalShifts, verticalShifts, hasShifts };
772
- }
773
- /**
774
- * Calculate horizontal spreading.
775
- * Elements overlapping on the left are shifted left, those on the right are shifted right.
776
- */
777
- calculateHorizontalSpreading(newBounds, elementIds) {
778
- const shifts = new Map();
779
- let maxLeftOverlap = 0;
780
- let maxRightOverlap = 0;
781
- // Find all overlaps
782
- const leftElements = [];
783
- const rightElements = [];
784
- for (const id of elementIds) {
785
- const bounds = this._boundsCache.get(id);
786
- if (!bounds)
787
- continue;
788
- // Check for horizontal overlap
789
- const horizontalOverlap = this.calculateHorizontalOverlap(newBounds, bounds);
790
- if (horizontalOverlap > 0) {
791
- // Determine which side this element is on
792
- if (bounds.right > newBounds.x && bounds.x < newBounds.x) {
793
- // Element overlaps from the left
794
- leftElements.push(id);
795
- maxLeftOverlap = Math.max(maxLeftOverlap, horizontalOverlap);
796
- }
797
- else if (bounds.x < newBounds.right &&
798
- bounds.right > newBounds.right) {
799
- // Element overlaps from the right
800
- rightElements.push(id);
801
- maxRightOverlap = Math.max(maxRightOverlap, horizontalOverlap);
802
- }
803
- }
804
- }
805
- // Apply shifts
806
- if (maxLeftOverlap > 0) {
807
- for (const id of leftElements) {
808
- shifts.set(id, -maxLeftOverlap); // Shift left (negative)
809
- this._logger.debug("Spreading", `Element ${id} shifts left by ${maxLeftOverlap}px`);
810
- }
811
- }
812
- if (maxRightOverlap > 0) {
813
- for (const id of rightElements) {
814
- shifts.set(id, maxRightOverlap); // Shift right (positive)
815
- this._logger.debug("Spreading", `Element ${id} shifts right by ${maxRightOverlap}px`);
816
- }
817
- }
818
- return shifts;
819
- }
820
- /**
821
- * Calculate vertical spreading.
822
- * Elements overlapping from above are shifted up, those from below are shifted down.
823
- */
824
- calculateVerticalSpreading(newBounds, elementIds) {
825
- const shifts = new Map();
826
- let maxTopOverlap = 0;
827
- let maxBottomOverlap = 0;
828
- // Find all overlaps
829
- const topElements = [];
830
- const bottomElements = [];
831
- for (const id of elementIds) {
832
- const bounds = this._boundsCache.get(id);
833
- if (!bounds)
834
- continue;
835
- // Check for vertical overlap
836
- const verticalOverlap = this.calculateVerticalOverlap(newBounds, bounds);
837
- if (verticalOverlap > 0) {
838
- // Determine which side this element is on
839
- if (bounds.bottom > newBounds.y && bounds.y < newBounds.y) {
840
- // Element overlaps from above
841
- topElements.push(id);
842
- maxTopOverlap = Math.max(maxTopOverlap, verticalOverlap);
843
- }
844
- else if (bounds.y < newBounds.bottom &&
845
- bounds.bottom > newBounds.bottom) {
846
- // Element overlaps from below
847
- bottomElements.push(id);
848
- maxBottomOverlap = Math.max(maxBottomOverlap, verticalOverlap);
849
- }
850
- }
851
- }
852
- // Apply shifts
853
- if (maxTopOverlap > 0) {
854
- for (const id of topElements) {
855
- shifts.set(id, -maxTopOverlap); // Shift up (negative)
856
- this._logger.debug("Spreading", `Element ${id} shifts up by ${maxTopOverlap}px`);
857
- }
858
- }
859
- if (maxBottomOverlap > 0) {
860
- for (const id of bottomElements) {
861
- shifts.set(id, maxBottomOverlap); // Shift down (positive)
862
- this._logger.debug("Spreading", `Element ${id} shifts down by ${maxBottomOverlap}px`);
863
- }
864
- }
865
- return shifts;
866
- }
867
- /**
868
- * Calculate the horizontal overlap between two bounding rectangles.
869
- * Returns 0 if there is no overlap.
870
- */
871
- calculateHorizontalOverlap(rect1, rect2) {
872
- // Check if rectangles overlap horizontally
873
- const overlapLeft = Math.max(rect1.x, rect2.x);
874
- const overlapRight = Math.min(rect1.right, rect2.right);
875
- if (overlapLeft < overlapRight) {
876
- return overlapRight - overlapLeft;
877
- }
878
- return 0;
879
- }
880
- /**
881
- * Calculate the vertical overlap between two bounding rectangles.
882
- * Returns 0 if there is no overlap.
883
- */
884
- calculateVerticalOverlap(rect1, rect2) {
885
- // Check if rectangles overlap vertically
886
- const overlapTop = Math.max(rect1.y, rect2.y);
887
- const overlapBottom = Math.min(rect1.bottom, rect2.bottom);
888
- if (overlapTop < overlapBottom) {
889
- return overlapBottom - overlapTop;
890
- }
891
- return 0;
892
- }
893
- /**
894
- * Check if two bounding rectangles overlap.
895
- */
896
- checkOverlap(rect1, rect2) {
897
- return !((rect1.right <= rect2.x || // rect1 is to the left of rect2
898
- rect1.x >= rect2.right || // rect1 is to the right of rect2
899
- rect1.bottom <= rect2.y || // rect1 is above rect2
900
- rect1.y >= rect2.bottom) // rect1 is below rect2
901
- );
902
- }
903
- /**
904
- * Find all elements that overlap with given bounds.
905
- */
906
- findOverlappingElements(bounds, excludeIds = []) {
907
- const overlapping = [];
908
- for (const id of this._boundsCache.getAllIds()) {
909
- if (excludeIds.includes(id))
910
- continue;
911
- const elementBounds = this._boundsCache.get(id);
912
- if (!elementBounds)
913
- continue;
914
- if (this.checkOverlap(bounds, elementBounds)) {
915
- overlapping.push(id);
916
- }
917
- }
918
- return overlapping;
919
- }
920
- }
921
-
922
607
  /**
923
608
  * Utility functions for working with character nodes.
924
609
  */
@@ -1560,273 +1245,16 @@ class TextLayout {
1560
1245
  }
1561
1246
  }
1562
1247
 
1563
- /**
1564
- * Feature resolver converts features into rendition configuration.
1565
- * This handles all the r_* rendition features and applies them to create
1566
- * a complete configuration for rendering.
1567
- */
1568
- class FeatureResolver {
1569
- constructor(logger) {
1570
- this._logger = logger;
1571
- }
1572
- /**
1573
- * Resolve rendition features into a complete configuration.
1574
- * Features override the base settings.
1575
- *
1576
- * @param baseSettings - The base settings from component
1577
- * @param features - Array of features to apply
1578
- * @param nodeFeatureContext - Optional context for extracting node features for placeholder resolution
1579
- * @returns Resolved rendition configuration
1580
- */
1581
- resolve(baseSettings, features, nodeFeatureContext) {
1582
- const config = {
1583
- fontSize: baseSettings.fontSize,
1584
- fontFamily: baseSettings.fontFamily,
1585
- foreColor: baseSettings.foreColor,
1586
- backColor: baseSettings.backColor,
1587
- italic: baseSettings.italic,
1588
- bold: baseSettings.bold,
1589
- underline: baseSettings.underline,
1590
- overline: baseSettings.overline,
1591
- strike: baseSettings.strike,
1592
- textOffsetX: 0,
1593
- textOffsetY: 0,
1594
- };
1595
- if (!features || features.length === 0) {
1596
- return config;
1597
- }
1598
- // Initialize hintVars map - will contain ALL feature values for placeholder
1599
- // replacement
1600
- config.hintVars = new Map();
1601
- // Initialize hintOverrides map for r_h-* features
1602
- config.hintOverrides = new Map();
1603
- // Extract ALL features from reference nodes for placeholder resolution.
1604
- // This includes features like "note", "group", etc. that don't have r_ prefix.
1605
- // Features are now stored per-node keyed by version tag.
1606
- if (nodeFeatureContext) {
1607
- const { refNodes, outputTag } = nodeFeatureContext;
1608
- for (const node of refNodes) {
1609
- const nodeFeaturesArray = node.features?.[outputTag];
1610
- if (!nodeFeaturesArray)
1611
- continue;
1612
- for (const nodeFeature of nodeFeaturesArray) {
1613
- if (nodeFeature.name.startsWith("$"))
1614
- continue;
1615
- if (!config.hintVars.has(nodeFeature.name)) {
1616
- config.hintVars.set(nodeFeature.name, nodeFeature.value);
1617
- this._logger.debug("FeatureResolver", `Added node feature to hintVars from node ${node.id} @ ${outputTag}: ${nodeFeature.name}=${nodeFeature.value}`);
1618
- }
1619
- }
1620
- }
1621
- }
1622
- // Process each rendition feature from operation
1623
- for (const feature of features) {
1624
- if (!feature.name.startsWith("r_"))
1625
- continue;
1626
- const value = feature.value;
1627
- // Add ALL r_* features to hintVars for placeholder replacement
1628
- // This allows {{r_fore-color}}, {{r_font-size}}, etc. in hint SVG templates
1629
- // These override any node features with the same name
1630
- config.hintVars.set(feature.name, value);
1631
- switch (feature.name) {
1632
- case "r_font-size":
1633
- config.fontSize = parseFloat(value);
1634
- break;
1635
- case "r_font-family":
1636
- config.fontFamily = value;
1637
- break;
1638
- case "r_fore-color":
1639
- config.foreColor = value;
1640
- break;
1641
- case "r_back-color":
1642
- config.backColor = value;
1643
- break;
1644
- case "r_italic":
1645
- config.italic = value === "true" || value === "1";
1646
- break;
1647
- case "r_bold":
1648
- config.bold = value === "true" || value === "1";
1649
- break;
1650
- case "r_underline":
1651
- config.underline = parseFloat(value);
1652
- break;
1653
- case "r_overline":
1654
- config.overline = parseFloat(value);
1655
- break;
1656
- case "r_strike":
1657
- config.strike = parseFloat(value);
1658
- break;
1659
- case "r_text-line-style":
1660
- config.textLineStyle = value;
1661
- break;
1662
- case "r_text-line-color":
1663
- config.textLineColor = value;
1664
- break;
1665
- case "r_rotate":
1666
- config.rotate = parseFloat(value);
1667
- break;
1668
- // r_t-* features for added text
1669
- case "r_t-position":
1670
- config.textPosition = value;
1671
- break;
1672
- case "r_t-offset-x":
1673
- // Store the value as-is (number or string). It will be parsed later
1674
- // in renderAdditionalText when RBR bounds are available.
1675
- config.textOffsetX = typeof value === "number" ? value : value;
1676
- break;
1677
- case "r_t-offset-y":
1678
- // Store the value as-is (number or string). It will be parsed later
1679
- // in renderAdditionalText when RBR bounds are available.
1680
- config.textOffsetY = typeof value === "number" ? value : value;
1681
- break;
1682
- case "r_t-solid":
1683
- config.textSolid = value === "1" || value === "true";
1684
- break;
1685
- case "r_t-displaced-span":
1686
- config.textDisplacedSpan = parseDisplacedSpan(value);
1687
- if (!config.textDisplacedSpan) {
1688
- this._logger.warn(`Invalid r_t-displaced-span value: ${value}`);
1689
- }
1690
- break;
1691
- case "r_t-value":
1692
- config.textValue = value;
1693
- break;
1694
- case "r_hints":
1695
- config.hints = value.trim().split(/\s+/);
1696
- break;
1697
- case "r_hint-vars":
1698
- // Parse custom variables and merge with existing hintVars
1699
- const customVars = parseHintVars(value);
1700
- customVars.forEach((val, key) => {
1701
- config.hintVars.set(key, val);
1702
- });
1703
- break;
1704
- // r_h-* features for hint property overrides
1705
- default:
1706
- if (feature.name.startsWith("r_h-")) {
1707
- this.parseHintOverride(feature.name, value, config);
1708
- }
1709
- break;
1710
- }
1711
- }
1712
- return config;
1713
- }
1714
- /**
1715
- * Extract character offsets from init features.
1716
- * This is used for base text layout adjustments.
1717
- *
1718
- * @param features - Features from the first annotate operation (init features)
1719
- * @returns Map of node IDs to their offsets (values may be numbers or "Ntw"/"Nth" strings)
1720
- */
1721
- extractCharOffsets(features) {
1722
- if (!features)
1723
- return new Map();
1724
- const charOffsetsValue = getFeatureValue(features, "r_char-offsets");
1725
- if (!charOffsetsValue)
1726
- return new Map();
1727
- return parseCharOffsets(charOffsetsValue);
1728
- }
1729
- /**
1730
- * Check if features contain a specific rendition feature.
1731
- */
1732
- hasRenditionFeature(features, featureName) {
1733
- if (!features)
1734
- return false;
1735
- return features.some((f) => f.name === featureName);
1736
- }
1737
- /**
1738
- * Get the value of a rendition feature.
1739
- */
1740
- getRenditionFeatureValue(features, featureName) {
1741
- return getFeatureValue(features, featureName);
1742
- }
1743
- /**
1744
- * Parse r_h-* hint override features.
1745
- * Format: "value" applies to all hints, or "@hintId1 hintId2:value" applies to specific hints.
1746
- *
1747
- * @param featureName - The feature name (e.g., "r_h-position")
1748
- * @param value - The feature value
1749
- * @param config - The rendition config to update
1750
- */
1751
- parseHintOverride(featureName, value, config) {
1752
- // Extract property name from feature name (r_h-position -> position)
1753
- const propertyName = featureName.substring(4); // Remove "r_h-"
1754
- // Parse value to check if it targets specific hints
1755
- let targetHints = null; // null = all hints
1756
- let actualValue = value;
1757
- if (value.startsWith("@")) {
1758
- // Format: "@hintId1 hintId2:value"
1759
- const colonIndex = value.indexOf(":");
1760
- if (colonIndex > 0) {
1761
- const hintIdsStr = value.substring(1, colonIndex); // Remove "@" and get up to ":"
1762
- targetHints = hintIdsStr.trim().split(/\s+/);
1763
- actualValue = value.substring(colonIndex + 1);
1764
- }
1765
- else {
1766
- this._logger.warn(`Invalid r_h-* format (missing colon): ${featureName}=${value}`);
1767
- return;
1768
- }
1769
- }
1770
- // If no hints defined yet in config, we'll still store the override
1771
- // It will be applied when hints are processed
1772
- const hints = targetHints || (config.hints ? config.hints : ["*"]);
1773
- for (const hintId of hints) {
1774
- // Get or create override object for this hint
1775
- let override = config.hintOverrides.get(hintId);
1776
- if (!override) {
1777
- override = {};
1778
- config.hintOverrides.set(hintId, override);
1779
- }
1780
- // Set the property value
1781
- switch (propertyName) {
1782
- case "position":
1783
- override.position = actualValue;
1784
- break;
1785
- case "offset-x":
1786
- override.offsetX = actualValue;
1787
- break;
1788
- case "offset-y":
1789
- override.offsetY = actualValue;
1790
- break;
1791
- case "scale-x":
1792
- override.scaleX = actualValue;
1793
- break;
1794
- case "scale-y":
1795
- override.scaleY = actualValue;
1796
- break;
1797
- case "rotation":
1798
- override.rotation = actualValue;
1799
- break;
1800
- case "solid":
1801
- override.solid = actualValue === "1" || actualValue === "true";
1802
- break;
1803
- case "displaced-span":
1804
- override.displacedSpan = parseDisplacedSpan(actualValue);
1805
- if (!override.displacedSpan) {
1806
- this._logger.warn(`Invalid r_h-displaced-span value: ${actualValue}`);
1807
- }
1808
- break;
1809
- default:
1810
- this._logger.warn(`Unknown hint override property: ${propertyName}`);
1811
- break;
1812
- }
1813
- this._logger.debug("FeatureResolver", `Added hint override for ${hintId}: ${propertyName}=${actualValue}`);
1814
- }
1815
- }
1816
- }
1817
-
1818
1248
  /**
1819
1249
  * Text renderer handles rendering of base text and additional text.
1820
1250
  */
1821
1251
  class TextRenderer {
1822
- constructor(settings, logger, animationEngine, boundsCache, spreadingEngine) {
1252
+ constructor(settings, logger, animationEngine, boundsCache) {
1823
1253
  this._settings = settings;
1824
1254
  this._logger = logger;
1825
1255
  this._animationEngine = animationEngine;
1826
1256
  this._boundsCache = boundsCache;
1827
- this._spreadingEngine = spreadingEngine;
1828
1257
  this._textLayout = new TextLayout(settings, logger);
1829
- this._featureResolver = new FeatureResolver(logger);
1830
1258
  }
1831
1259
  /**
1832
1260
  * Render base text (v0).
@@ -1995,56 +1423,43 @@ class TextRenderer {
1995
1423
  // Parse offsets using RBR bounds (offsets can be like "0.5th" = half of RBR height)
1996
1424
  const offsetX = parseOffset(config.textOffsetX || 0, rbr.height, rbr.width);
1997
1425
  const offsetY = parseOffset(config.textOffsetY || 0, rbr.height, rbr.width);
1998
- const baseX = targetPos.x + offsetX;
1426
+ // Pre-calculate text width to correctly align the EBR with the RBR target point.
1427
+ // calculateTargetPosition returns the RBR alignment point, but the text renderer
1428
+ // lays characters left-to-right from baseX, so we must shift left so the intended
1429
+ // edge or center of the EBR lands on that point.
1430
+ // n/s/c/o: horizontal centers of EBR and RBR must coincide → shift left by half width
1431
+ // w/nw/sw: EBR right edge at RBR target X → shift left by full width
1432
+ const textWidth = this.calculateTotalTextWidth(nodes);
1433
+ let alignedX = targetPos.x;
1434
+ switch (position) {
1435
+ case "n":
1436
+ case "s":
1437
+ case "c":
1438
+ case "o":
1439
+ alignedX -= textWidth / 2;
1440
+ break;
1441
+ case "w":
1442
+ case "nw":
1443
+ case "sw":
1444
+ alignedX -= textWidth;
1445
+ break;
1446
+ }
1447
+ const baseX = alignedX + offsetX;
1999
1448
  const baseY = targetPos.y + offsetY;
2000
1449
  this._logger.debug("TextRenderer", `Text position calculated`, {
2001
1450
  position,
2002
1451
  targetPos,
1452
+ textWidth,
1453
+ alignedX,
2003
1454
  offsets: { x: offsetX, y: offsetY },
2004
1455
  base: { x: baseX, y: baseY },
2005
1456
  });
2006
1457
  // 3. Calculate positions for each character
2007
1458
  const positions = this.calculateAdditionalTextPositions(nodes, baseX, baseY);
2008
- // 4. Calculate bounding rectangle for the text (needed for spreading and prolog)
1459
+ // 4. Calculate bounding rectangle for the text (needed for prolog)
2009
1460
  const textBounds = this.calculateTextBounds(nodes, positions);
2010
- // 5. Apply spreading if r_t-solid=1
2011
- // Check config.textSolid to determine if spreading should be applied
2012
- if (config.textSolid) {
2013
- if (textBounds) {
2014
- this._logger.debug("TextRenderer", `Applying spreading for solid additional text`, textBounds);
2015
- const spreading = this._spreadingEngine.calculateSpreading(textBounds, [
2016
- `text-group-${versionTag}`,
2017
- ]);
2018
- if (spreading.hasShifts) {
2019
- // Combine horizontal and vertical shifts
2020
- const combinedShifts = new Map();
2021
- const allIds = new Set([
2022
- ...spreading.horizontalShifts.keys(),
2023
- ...spreading.verticalShifts.keys(),
2024
- ]);
2025
- for (const id of allIds) {
2026
- combinedShifts.set(id, {
2027
- x: spreading.horizontalShifts.get(id) || 0,
2028
- y: spreading.verticalShifts.get(id) || 0,
2029
- });
2030
- }
2031
- // Animate spreading
2032
- await this._animationEngine.animateSpreading(combinedShifts, this._settings.spreadTime, rootSvg);
2033
- // Update cached bounds for shifted elements
2034
- for (const id of allIds) {
2035
- const element = rootSvg.querySelector(`#${id}`);
2036
- if (element) {
2037
- this._boundsCache.updateFromElement(element);
2038
- }
2039
- }
2040
- }
2041
- }
2042
- }
2043
- else {
2044
- this._logger.debug("TextRenderer", `Skipping spreading - additional text is not solid (r_t-solid not set)`);
2045
- }
2046
- // 6. Check if prolog panning is needed (element visibility check)
2047
- // This must happen AFTER positioning and spreading, but BEFORE rendering characters
1461
+ // 5. Check if prolog panning is needed (element visibility check)
1462
+ // This must happen AFTER positioning, but BEFORE rendering characters
2048
1463
  if (panZoomInstance &&
2049
1464
  viewportWidth &&
2050
1465
  viewportHeight &&
@@ -2056,13 +1471,13 @@ class TextRenderer {
2056
1471
  await this._animationEngine.animateProlog(panZoomInstance, textBounds, viewportWidth, viewportHeight, this._settings.prologDuration);
2057
1472
  }
2058
1473
  }
2059
- // 7. Get animation function if specified
1474
+ // 6. Get animation function if specified
2060
1475
  const animationFn = this._settings.charAnimationId
2061
1476
  ? this._animationEngine
2062
1477
  .getFactory()
2063
1478
  .resolveAnimation(`#${this._settings.charAnimationId}`, this._settings.animations, "char")
2064
1479
  : undefined;
2065
- // 8. Render each character with features
1480
+ // 7. Render each character with features
2066
1481
  for (let i = 0; i < nodes.length; i++) {
2067
1482
  const node = nodes[i];
2068
1483
  const pos = positions[i];
@@ -2190,6 +1605,33 @@ class TextRenderer {
2190
1605
  return { x: centerX, y: centerY };
2191
1606
  }
2192
1607
  }
1608
+ /**
1609
+ * Calculate total rendered width of additional text nodes.
1610
+ * Mirrors the spacing logic in calculateAdditionalTextPositions so the result
1611
+ * is the exact horizontal span from the first character's left edge to the last
1612
+ * character's right edge.
1613
+ */
1614
+ calculateTotalTextWidth(nodes) {
1615
+ let totalWidth = 0;
1616
+ let count = 0;
1617
+ for (const node of nodes) {
1618
+ if (isLineBreak(node))
1619
+ continue;
1620
+ if (isSpace(node)) {
1621
+ totalWidth += this._settings.fontSize * 0.33 + this._settings.charSpacing;
1622
+ count++;
1623
+ continue;
1624
+ }
1625
+ const charWidth = getTextWidth(node.data, this._settings.fontFamily, this._settings.fontSize, this._settings.bold, this._settings.italic, this._measurementRoot);
1626
+ totalWidth += charWidth + this._settings.charSpacing;
1627
+ count++;
1628
+ }
1629
+ // charSpacing is added after every character; remove the trailing one
1630
+ if (count > 0) {
1631
+ totalWidth -= this._settings.charSpacing;
1632
+ }
1633
+ return Math.max(0, totalWidth);
1634
+ }
2193
1635
  /**
2194
1636
  * Calculate bounding rectangle for a set of positioned characters.
2195
1637
  */
@@ -2239,12 +1681,11 @@ class TextRenderer {
2239
1681
  * Hints are visual counterparts of editing operations.
2240
1682
  */
2241
1683
  class HintRenderer {
2242
- constructor(settings, logger, animationEngine, boundsCache, spreadingEngine) {
1684
+ constructor(settings, logger, animationEngine, boundsCache) {
2243
1685
  this._settings = settings;
2244
1686
  this._logger = logger;
2245
1687
  this._animationEngine = animationEngine;
2246
1688
  this._boundsCache = boundsCache;
2247
- this._spreadingEngine = spreadingEngine;
2248
1689
  }
2249
1690
  /**
2250
1691
  * Render a hint for an operation.
@@ -2269,8 +1710,8 @@ class HintRenderer {
2269
1710
  this._logger.info(`Rendering hint: ${hintId} for operation ${operationId}`);
2270
1711
  try {
2271
1712
  // 1. Apply r_h-* overrides to hint properties
2272
- // Overrides can target this specific hint or all hints ("*")
2273
- const effectiveHint = this.applyHintOverrides(hint, hintId, hintOverrides, allBaseNodes);
1713
+ // Overrides can target this specific hint (by key or ordinal) or all hints ("*")
1714
+ const effectiveHint = this.applyHintOverrides(hint, hintId, hintOrdinal, hintOverrides, allBaseNodes);
2274
1715
  // 2. Resolve placeholders in SVG
2275
1716
  const resolvedSvg = resolvePlaceholders(effectiveHint.svg, variables);
2276
1717
  this._logger.debug("HintRenderer", `Placeholders resolved`, {
@@ -2302,6 +1743,12 @@ class HintRenderer {
2302
1743
  }
2303
1744
  // 5. Check if this is a placeholder hint
2304
1745
  const hasPlaceholder = hintGroup.querySelector("#placeholder") !== null;
1746
+ // Default-mode placeholder: a placeholder element WITHOUT class="fit".
1747
+ // Per documentation, in default mode the text keeps its natural size and the
1748
+ // container (if any) scales to fit it — meaning no RBR-based scaling is applied.
1749
+ // Fit-mode placeholders still scale the hint to the RBR normally.
1750
+ const isDefaultPlaceholder = hasPlaceholder &&
1751
+ !hintGroup.querySelector("#placeholder")?.classList.contains("fit");
2305
1752
  // 6. Render hint for each RBR (unless it's a placeholder hint and already rendered once)
2306
1753
  let placeholderRendered = false;
2307
1754
  for (let i = 0; i < rbrs.length; i++) {
@@ -2322,6 +1769,13 @@ class HintRenderer {
2322
1769
  rootSvg.appendChild(currentHintGroup);
2323
1770
  // 8. Get the scale factors needed to fit RBR
2324
1771
  const scalingInfo = this.calculateScalingInfo(currentHintGroup, effectiveHint, rbr);
1772
+ // For default-mode placeholder hints the text drives the size, not the RBR.
1773
+ // Override any RBR-derived scale so the hint keeps its natural rendered size.
1774
+ // The EBR (measured from the natural-size hint) is then used to center it.
1775
+ if (isDefaultPlaceholder) {
1776
+ scalingInfo.scaleX = 1;
1777
+ scalingInfo.scaleY = 1;
1778
+ }
2325
1779
  // 9. Apply scale and rotation transforms to prepare the hint
2326
1780
  await this.applyHintSizeTransforms(currentHintGroup, effectiveHint, scalingInfo);
2327
1781
  // 10. Get EBR (Element Bounding Rectangle) AFTER transformations
@@ -2329,11 +1783,7 @@ class HintRenderer {
2329
1783
  const ebrBounds = this.getEBRBounds(currentHintGroup);
2330
1784
  // 11. Apply positioning transform - align EBR with RBR based on position
2331
1785
  await this.applyHintPositionTransform(currentHintGroup, effectiveHint, rbr, ebrBounds, scalingInfo);
2332
- // 10. Handle spreading if solid
2333
- if (effectiveHint.solid) {
2334
- await this.handleSpreading(currentHintGroup, rootSvg, versionTag);
2335
- }
2336
- // 11. Check if prolog panning is needed (element visibility check)
1786
+ // 10. Check if prolog panning is needed (element visibility check)
2337
1787
  // This must happen AFTER positioning and spreading, but BEFORE making visible
2338
1788
  if (panZoomInstance && viewportWidth && viewportHeight && this._settings.prologDuration > 0) {
2339
1789
  // IMPORTANT: Use getTransformedBBox() instead of getSafeBBox() here!
@@ -2473,12 +1923,6 @@ class HintRenderer {
2473
1923
  scaledSize: { width, height },
2474
1924
  });
2475
1925
  }
2476
- // Apply hint margin
2477
- if (this._settings.hintMargin > 0) {
2478
- width += this._settings.hintMargin * 2;
2479
- height += this._settings.hintMargin * 2;
2480
- this._logger.debug("HintRenderer", `Applied hint margin: ${this._settings.hintMargin}px`);
2481
- }
2482
1926
  // Calculate scale factors based on current hint size
2483
1927
  const currentBBox = getSafeBBox(hintGroup);
2484
1928
  const scaleX = currentBBox.width > 0 ? width / currentBBox.width : 1;
@@ -2674,74 +2118,32 @@ class HintRenderer {
2674
2118
  return { x: centerX, y: centerY };
2675
2119
  }
2676
2120
  }
2677
- /**
2678
- * Handle spreading for solid hints.
2679
- * @param versionTag - Version tag for tracking spreading history
2680
- */
2681
- async handleSpreading(hintGroup, rootSvg, versionTag) {
2682
- const hintBounds = getSafeBBox(hintGroup);
2683
- const hintId = hintGroup.getAttribute("id");
2684
- const spreading = this._spreadingEngine.calculateSpreading(hintBounds, [
2685
- hintId,
2686
- ]);
2687
- if (spreading.hasShifts) {
2688
- this._logger.debug("HintRenderer", `Spreading required for hint ${hintId}`, {
2689
- horizontalShifts: spreading.horizontalShifts.size,
2690
- verticalShifts: spreading.verticalShifts.size,
2691
- });
2692
- // Combine horizontal and vertical shifts
2693
- const combinedShifts = new Map();
2694
- const allIds = new Set([
2695
- ...spreading.horizontalShifts.keys(),
2696
- ...spreading.verticalShifts.keys(),
2697
- ]);
2698
- for (const id of allIds) {
2699
- combinedShifts.set(id, {
2700
- x: spreading.horizontalShifts.get(id) || 0,
2701
- y: spreading.verticalShifts.get(id) || 0,
2702
- });
2703
- }
2704
- // Animate spreading
2705
- await this._animationEngine.animateSpreading(combinedShifts, this._settings.spreadTime, rootSvg);
2706
- // Update cached bounds for shifted elements
2707
- for (const id of allIds) {
2708
- const element = rootSvg.querySelector(`#${id}`);
2709
- if (element) {
2710
- this._boundsCache.updateFromElement(element);
2711
- }
2712
- }
2713
- // Track spreading metadata for backward navigation
2714
- // Extract version index from versionTag (e.g., "v2" → 2)
2715
- const versionMatch = versionTag.match(/v(\d+)/);
2716
- if (versionMatch) {
2717
- const versionIndex = parseInt(versionMatch[1]);
2718
- this._spreadingEngine.trackSpreading(versionIndex, spreading.horizontalShifts, spreading.verticalShifts);
2719
- }
2720
- }
2721
- }
2722
2121
  /**
2723
2122
  * Apply r_h-* overrides to hint properties.
2724
- * Overrides can target specific hints or all hints ("*").
2123
+ * Priority (lowest to highest): wildcard ("*") < key-based < ordinal-based.
2725
2124
  *
2726
2125
  * @param hint - Original hint design
2727
- * @param hintId - ID of the hint being rendered
2728
- * @param hintOverrides - Map of overrides from r_h-* features
2126
+ * @param hintId - ID (key) of the hint being rendered
2127
+ * @param hintOrdinal - 1-based ordinal position of this hint within the operation
2128
+ * @param hintOverrides - Map of overrides from r_h-* features (keyed by ID string or ordinal number)
2729
2129
  * @param allBaseNodes - All base nodes for displaced span resolution
2730
2130
  * @returns Modified hint with overrides applied
2731
2131
  */
2732
- applyHintOverrides(hint, hintId, hintOverrides, allBaseNodes) {
2132
+ applyHintOverrides(hint, hintId, hintOrdinal, hintOverrides, allBaseNodes) {
2733
2133
  if (!hintOverrides || hintOverrides.size === 0) {
2734
2134
  return hint;
2735
2135
  }
2736
2136
  // Clone hint to avoid modifying original
2737
2137
  const effectiveHint = { ...hint };
2738
- // Check for overrides targeting this specific hint or all hints
2739
- const specificOverride = hintOverrides.get(hintId);
2138
+ // Look up overrides by wildcard, key, and ordinal; merge in priority order
2740
2139
  const wildcardOverride = hintOverrides.get("*");
2741
- // Apply wildcard overrides first, then specific overrides (so specific wins)
2140
+ const specificOverride = hintOverrides.get(hintId);
2141
+ const ordinalOverride = hintOverrides.get(hintOrdinal);
2142
+ // Ordinal (most specific) wins over key-specific, which wins over wildcard
2742
2143
  const override = {
2743
2144
  ...wildcardOverride,
2744
2145
  ...specificOverride,
2146
+ ...ordinalOverride,
2745
2147
  };
2746
2148
  // Apply each override property
2747
2149
  if (override.position !== undefined) {
@@ -2768,10 +2170,6 @@ class HintRenderer {
2768
2170
  effectiveHint.rotation = parseFloat(override.rotation);
2769
2171
  this._logger.debug("HintRenderer", `Override rotation for ${hintId}: ${override.rotation}`);
2770
2172
  }
2771
- if (override.solid !== undefined) {
2772
- effectiveHint.solid = override.solid;
2773
- this._logger.debug("HintRenderer", `Override solid for ${hintId}: ${override.solid}`);
2774
- }
2775
2173
  if (override.displacedSpan !== undefined) {
2776
2174
  // Convert to displacedRefSpan format (e.g., "64x3")
2777
2175
  const { nodeId, count } = override.displacedSpan;
@@ -2788,6 +2186,262 @@ class HintRenderer {
2788
2186
  }
2789
2187
  }
2790
2188
 
2189
+ /**
2190
+ * Feature resolver converts features into rendition configuration.
2191
+ * This handles all the r_* rendition features and applies them to create
2192
+ * a complete configuration for rendering.
2193
+ */
2194
+ class FeatureResolver {
2195
+ constructor(logger) {
2196
+ this._logger = logger;
2197
+ }
2198
+ /**
2199
+ * Resolve rendition features into a complete configuration.
2200
+ * Features override the base settings.
2201
+ *
2202
+ * @param baseSettings - The base settings from component
2203
+ * @param features - Array of features to apply
2204
+ * @param nodeFeatureContext - Optional context for extracting node features for placeholder resolution
2205
+ * @returns Resolved rendition configuration
2206
+ */
2207
+ resolve(baseSettings, features, nodeFeatureContext) {
2208
+ const config = {
2209
+ fontSize: baseSettings.fontSize,
2210
+ fontFamily: baseSettings.fontFamily,
2211
+ foreColor: baseSettings.foreColor,
2212
+ backColor: baseSettings.backColor,
2213
+ italic: baseSettings.italic,
2214
+ bold: baseSettings.bold,
2215
+ underline: baseSettings.underline,
2216
+ overline: baseSettings.overline,
2217
+ strike: baseSettings.strike,
2218
+ textOffsetX: 0,
2219
+ textOffsetY: 0,
2220
+ };
2221
+ if (!features || features.length === 0) {
2222
+ return config;
2223
+ }
2224
+ // Initialize hintVars map - will contain ALL feature values for placeholder
2225
+ // replacement
2226
+ config.hintVars = new Map();
2227
+ // Initialize hintOverrides map for r_h-* features
2228
+ config.hintOverrides = new Map();
2229
+ // Extract ALL features from reference nodes for placeholder resolution.
2230
+ // This includes features like "note", "group", etc. that don't have r_ prefix.
2231
+ // Features are now stored per-node keyed by version tag.
2232
+ if (nodeFeatureContext) {
2233
+ const { refNodes, outputTag } = nodeFeatureContext;
2234
+ for (const node of refNodes) {
2235
+ const nodeFeaturesArray = node.features?.[outputTag];
2236
+ if (!nodeFeaturesArray)
2237
+ continue;
2238
+ for (const nodeFeature of nodeFeaturesArray) {
2239
+ if (nodeFeature.name.startsWith("$"))
2240
+ continue;
2241
+ if (!config.hintVars.has(nodeFeature.name)) {
2242
+ config.hintVars.set(nodeFeature.name, nodeFeature.value);
2243
+ this._logger.debug("FeatureResolver", `Added node feature to hintVars from node ${node.id} @ ${outputTag}: ${nodeFeature.name}=${nodeFeature.value}`);
2244
+ }
2245
+ }
2246
+ }
2247
+ }
2248
+ // Process each rendition feature from operation
2249
+ for (const feature of features) {
2250
+ if (!feature.name.startsWith("r_"))
2251
+ continue;
2252
+ const value = feature.value;
2253
+ // Add ALL r_* features to hintVars for placeholder replacement
2254
+ // This allows {{r_fore-color}}, {{r_font-size}}, etc. in hint SVG templates
2255
+ // These override any node features with the same name
2256
+ config.hintVars.set(feature.name, value);
2257
+ switch (feature.name) {
2258
+ case "r_font-size":
2259
+ config.fontSize = parseFloat(value);
2260
+ break;
2261
+ case "r_font-family":
2262
+ config.fontFamily = value;
2263
+ break;
2264
+ case "r_fore-color":
2265
+ config.foreColor = value;
2266
+ break;
2267
+ case "r_back-color":
2268
+ config.backColor = value;
2269
+ break;
2270
+ case "r_italic":
2271
+ config.italic = value === "true" || value === "1";
2272
+ break;
2273
+ case "r_bold":
2274
+ config.bold = value === "true" || value === "1";
2275
+ break;
2276
+ case "r_underline":
2277
+ config.underline = parseFloat(value);
2278
+ break;
2279
+ case "r_overline":
2280
+ config.overline = parseFloat(value);
2281
+ break;
2282
+ case "r_strike":
2283
+ config.strike = parseFloat(value);
2284
+ break;
2285
+ case "r_text-line-style":
2286
+ config.textLineStyle = value;
2287
+ break;
2288
+ case "r_text-line-color":
2289
+ config.textLineColor = value;
2290
+ break;
2291
+ case "r_rotate":
2292
+ config.rotate = parseFloat(value);
2293
+ break;
2294
+ // r_t-* features for added text
2295
+ case "r_t-position":
2296
+ config.textPosition = value;
2297
+ break;
2298
+ case "r_t-offset-x":
2299
+ // Store the value as-is (number or string). It will be parsed later
2300
+ // in renderAdditionalText when RBR bounds are available.
2301
+ config.textOffsetX = typeof value === "number" ? value : value;
2302
+ break;
2303
+ case "r_t-offset-y":
2304
+ // Store the value as-is (number or string). It will be parsed later
2305
+ // in renderAdditionalText when RBR bounds are available.
2306
+ config.textOffsetY = typeof value === "number" ? value : value;
2307
+ break;
2308
+ case "r_t-displaced-span":
2309
+ config.textDisplacedSpan = parseDisplacedSpan(value);
2310
+ if (!config.textDisplacedSpan) {
2311
+ this._logger.warn(`Invalid r_t-displaced-span value: ${value}`);
2312
+ }
2313
+ break;
2314
+ case "r_t-value":
2315
+ config.textValue = value;
2316
+ break;
2317
+ case "r_hints":
2318
+ config.hints = value.trim().split(/\s+/);
2319
+ break;
2320
+ case "r_hint-vars":
2321
+ // Parse custom variables and merge with existing hintVars
2322
+ const customVars = parseHintVars(value);
2323
+ customVars.forEach((val, key) => {
2324
+ config.hintVars.set(key, val);
2325
+ });
2326
+ break;
2327
+ // r_h-* features for hint property overrides
2328
+ default:
2329
+ if (feature.name.startsWith("r_h-")) {
2330
+ this.parseHintOverride(feature.name, value, config);
2331
+ }
2332
+ break;
2333
+ }
2334
+ }
2335
+ return config;
2336
+ }
2337
+ /**
2338
+ * Extract character offsets from init features.
2339
+ * This is used for base text layout adjustments.
2340
+ *
2341
+ * @param features - Features from the first annotate operation (init features)
2342
+ * @returns Map of node IDs to their offsets (values may be numbers or "Ntw"/"Nth" strings)
2343
+ */
2344
+ extractCharOffsets(features) {
2345
+ if (!features)
2346
+ return new Map();
2347
+ const charOffsetsValue = getFeatureValue(features, "r_char-offsets");
2348
+ if (!charOffsetsValue)
2349
+ return new Map();
2350
+ return parseCharOffsets(charOffsetsValue);
2351
+ }
2352
+ /**
2353
+ * Check if features contain a specific rendition feature.
2354
+ */
2355
+ hasRenditionFeature(features, featureName) {
2356
+ if (!features)
2357
+ return false;
2358
+ return features.some((f) => f.name === featureName);
2359
+ }
2360
+ /**
2361
+ * Get the value of a rendition feature.
2362
+ */
2363
+ getRenditionFeatureValue(features, featureName) {
2364
+ return getFeatureValue(features, featureName);
2365
+ }
2366
+ /**
2367
+ * Parse r_h-* hint override features.
2368
+ * Format: "value" applies to all hints, or "@target1 target2:value" applies to specific hints.
2369
+ * Targets can be hint ID strings or 1-based ordinal integers (e.g., "@1 beta:e" targets
2370
+ * the first hint by position and all hints with key "beta"). Numbers and keys can be mixed.
2371
+ *
2372
+ * @param featureName - The feature name (e.g., "r_h-position")
2373
+ * @param value - The feature value
2374
+ * @param config - The rendition config to update
2375
+ */
2376
+ parseHintOverride(featureName, value, config) {
2377
+ // Extract property name from feature name (r_h-position -> position)
2378
+ const propertyName = featureName.substring(4); // Remove "r_h-"
2379
+ // Parse value to check if it targets specific hints
2380
+ // null = all hints; otherwise an array of string keys and/or numeric ordinals
2381
+ let targetHints = null;
2382
+ let actualValue = value;
2383
+ if (value.startsWith("@")) {
2384
+ // Format: "@target1 target2:value" where targets are hint IDs or 1-based ordinals
2385
+ const colonIndex = value.indexOf(":");
2386
+ if (colonIndex > 0) {
2387
+ const hintIdsStr = value.substring(1, colonIndex); // Remove "@" and get up to ":"
2388
+ targetHints = hintIdsStr.trim().split(/\s+/).map((token) => {
2389
+ const n = parseInt(token, 10);
2390
+ // Use numeric key for pure integer tokens (ordinal-based targeting)
2391
+ return !isNaN(n) && String(n) === token ? n : token;
2392
+ });
2393
+ actualValue = value.substring(colonIndex + 1);
2394
+ }
2395
+ else {
2396
+ this._logger.warn(`Invalid r_h-* format (missing colon): ${featureName}=${value}`);
2397
+ return;
2398
+ }
2399
+ }
2400
+ // If no hints defined yet in config, we'll still store the override.
2401
+ // It will be applied when hints are processed.
2402
+ const hints = targetHints ?? (config.hints ?? ["*"]);
2403
+ for (const hintKey of hints) {
2404
+ // Get or create override object for this hint key / ordinal
2405
+ let override = config.hintOverrides.get(hintKey);
2406
+ if (!override) {
2407
+ override = {};
2408
+ config.hintOverrides.set(hintKey, override);
2409
+ }
2410
+ // Set the property value
2411
+ switch (propertyName) {
2412
+ case "position":
2413
+ override.position = actualValue;
2414
+ break;
2415
+ case "offset-x":
2416
+ override.offsetX = actualValue;
2417
+ break;
2418
+ case "offset-y":
2419
+ override.offsetY = actualValue;
2420
+ break;
2421
+ case "scale-x":
2422
+ override.scaleX = actualValue;
2423
+ break;
2424
+ case "scale-y":
2425
+ override.scaleY = actualValue;
2426
+ break;
2427
+ case "rotation":
2428
+ override.rotation = actualValue;
2429
+ break;
2430
+ case "displaced-span":
2431
+ override.displacedSpan = parseDisplacedSpan(actualValue);
2432
+ if (!override.displacedSpan) {
2433
+ this._logger.warn(`Invalid r_h-displaced-span value: ${actualValue}`);
2434
+ }
2435
+ break;
2436
+ default:
2437
+ this._logger.warn(`Unknown hint override property: ${propertyName}`);
2438
+ break;
2439
+ }
2440
+ this._logger.debug("FeatureResolver", `Added hint override for ${hintKey}: ${propertyName}=${actualValue}`);
2441
+ }
2442
+ }
2443
+ }
2444
+
2791
2445
  function getDefaultExportFromCjs (x) {
2792
2446
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
2793
2447
  }
@@ -26291,7 +25945,7 @@ class GveSnapshotRendition extends HTMLElement {
26291
25945
  * of the web component is loaded.
26292
25946
  */
26293
25947
  static get version() {
26294
- return "1.0.2";
25948
+ return "2.0.1";
26295
25949
  }
26296
25950
  constructor() {
26297
25951
  super();
@@ -26308,9 +25962,7 @@ class GveSnapshotRendition extends HTMLElement {
26308
25962
  this._autoForwardEnabled = this._settings.autoForwardOnGroup;
26309
25963
  // Create logger
26310
25964
  this._logger = new Logger("GVE-Rendition", this._settings.debug);
26311
- // Create bounds cache and spreading engine
26312
25965
  this._boundsCache = new BoundsCache(this._logger);
26313
- this._spreadingEngine = new SpreadingEngine(this._boundsCache, this._logger);
26314
25966
  // Create feature resolver
26315
25967
  this._featureResolver = new FeatureResolver(this._logger);
26316
25968
  // Create shadow DOM
@@ -26422,8 +26074,8 @@ class GveSnapshotRendition extends HTMLElement {
26422
26074
  const gsap = window.gsap;
26423
26075
  if (gsap) {
26424
26076
  this._animationEngine = new AnimationEngine(gsap, this._logger);
26425
- this._textRenderer = new TextRenderer(this._settings, this._logger, this._animationEngine, this._boundsCache, this._spreadingEngine);
26426
- this._hintRenderer = new HintRenderer(this._settings, this._logger, this._animationEngine, this._boundsCache, this._spreadingEngine);
26077
+ this._textRenderer = new TextRenderer(this._settings, this._logger, this._animationEngine, this._boundsCache);
26078
+ this._hintRenderer = new HintRenderer(this._settings, this._logger, this._animationEngine, this._boundsCache);
26427
26079
  this._logger.info("GSAP initialized");
26428
26080
  this.renderContent();
26429
26081
  }
@@ -27176,39 +26828,7 @@ class GveSnapshotRendition extends HTMLElement {
27176
26828
  return;
27177
26829
  }
27178
26830
  this._logger.info(`Removing ${elements.length} elements from v${versionIndex}`);
27179
- // Step 1: Reverse spreading animation if spreadTime > 0
27180
- // This must happen BEFORE removing elements and BEFORE fadeout
27181
- if (this._settings.spreadTime > 0 &&
27182
- this._spreadingEngine &&
27183
- this._animationEngine) {
27184
- const spreadingMetadata = this._spreadingEngine.getSpreadingForVersion(versionIndex);
27185
- if (spreadingMetadata.size > 0) {
27186
- this._logger.debug("Navigation", `Reversing spreading for ${spreadingMetadata.size} elements from v${versionIndex}`);
27187
- // Build combined shifts (negative of original shifts to reverse)
27188
- const reverseShifts = new Map();
27189
- for (const [elementId, metadata] of spreadingMetadata.entries()) {
27190
- reverseShifts.set(elementId, {
27191
- x: -metadata.horizontalShift, // Negative to reverse
27192
- y: -metadata.verticalShift, // Negative to reverse
27193
- });
27194
- }
27195
- // Animate spreading reversal
27196
- await this._animationEngine.animateSpreading(reverseShifts, this._settings.spreadTime, container // Use container for animation context
27197
- );
27198
- // Update cached bounds for shifted elements
27199
- if (this._boundsCache) {
27200
- for (const elementId of reverseShifts.keys()) {
27201
- const element = container.querySelector(`#${elementId}`);
27202
- if (element) {
27203
- this._boundsCache.updateFromElement(element);
27204
- }
27205
- }
27206
- }
27207
- // Clear spreading history for this version
27208
- this._spreadingEngine.clearSpreadingForVersion(versionIndex);
27209
- }
27210
- }
27211
- // Step 2: If backwardFadeOutTime > 0, animate fadeout
26831
+ // Step 1: If backwardFadeOutTime > 0, animate fadeout
27212
26832
  if (this._settings.backwardFadeOutTime > 0) {
27213
26833
  const fadeOutPromises = [];
27214
26834
  elements.forEach((el) => {
@@ -27220,7 +26840,7 @@ class GveSnapshotRendition extends HTMLElement {
27220
26840
  });
27221
26841
  await Promise.all(fadeOutPromises);
27222
26842
  }
27223
- // Step 3: Remove elements from DOM
26843
+ // Step 2: Remove elements from DOM
27224
26844
  elements.forEach((el) => el.remove());
27225
26845
  }
27226
26846
  /**
@@ -41212,7 +40832,7 @@ function requireD () {
41212
40832
  + 'pragma private protected public pure ref return scope shared static struct '
41213
40833
  + 'super switch synchronized template this throw try typedef typeid typeof union '
41214
40834
  + 'unittest version void volatile while with __FILE__ __LINE__ __gshared|10 '
41215
- + '__thread __traits __DATE__ __EOF__ __TIME__ __TIMESTAMP__ __VENDOR__ 1.0.2',
40835
+ + '__thread __traits __DATE__ __EOF__ __TIME__ __TIMESTAMP__ __VENDOR__ 2.0.1',
41216
40836
  built_in:
41217
40837
  'bool cdouble cent cfloat char creal dchar delegate double dstring float function '
41218
40838
  + 'idouble ifloat ireal long real short string ubyte ucent uint ulong ushort wchar '
@@ -91176,10 +90796,6 @@ class GveHintDesigner extends HTMLElement {
91176
90796
  // Rotation
91177
90797
  this._rotationInput = this.createInput("number", "0");
91178
90798
  form.appendChild(this.createFormRow("Rotation:", this._rotationInput));
91179
- // Solid
91180
- this._solidCheckbox = document.createElement("input");
91181
- this._solidCheckbox.type = "checkbox";
91182
- form.appendChild(this.createFormRow("Solid:", this._solidCheckbox));
91183
90799
  // Displaced Ref Span
91184
90800
  this._displacedRefSpanInput = this.createInput("text", "");
91185
90801
  form.appendChild(this.createFormRow("Displaced Ref Span:", this._displacedRefSpanInput));
@@ -91665,8 +91281,6 @@ class GveHintDesigner extends HTMLElement {
91665
91281
  this._scaleYInput.value = hint.scaleY?.toString() || "1";
91666
91282
  if (this._rotationInput)
91667
91283
  this._rotationInput.value = hint.rotation?.toString() || "0";
91668
- if (this._solidCheckbox)
91669
- this._solidCheckbox.checked = hint.solid || false;
91670
91284
  if (this._displacedRefSpanInput)
91671
91285
  this._displacedRefSpanInput.value = hint.displacedRefSpan || "";
91672
91286
  if (this._svgTextarea) {
@@ -91732,8 +91346,6 @@ class GveHintDesigner extends HTMLElement {
91732
91346
  this._scaleYInput.value = "1";
91733
91347
  if (this._rotationInput)
91734
91348
  this._rotationInput.value = "0";
91735
- if (this._solidCheckbox)
91736
- this._solidCheckbox.checked = false;
91737
91349
  if (this._displacedRefSpanInput)
91738
91350
  this._displacedRefSpanInput.value = "";
91739
91351
  if (this._svgTextarea)
@@ -91806,7 +91418,6 @@ class GveHintDesigner extends HTMLElement {
91806
91418
  scaleX: 1,
91807
91419
  scaleY: 1,
91808
91420
  rotation: 0,
91809
- solid: false,
91810
91421
  animation: "",
91811
91422
  };
91812
91423
  this._data.hints[hintId] = newHint;
@@ -91851,7 +91462,6 @@ class GveHintDesigner extends HTMLElement {
91851
91462
  scaleX: parseFloat(this._scaleXInput?.value || "1"),
91852
91463
  scaleY: parseFloat(this._scaleYInput?.value || "1"),
91853
91464
  rotation: parseFloat(this._rotationInput?.value || "0"),
91854
- solid: this._solidCheckbox?.checked || false,
91855
91465
  displacedRefSpan: this._displacedRefSpanInput?.value || undefined,
91856
91466
  };
91857
91467
  // Handle animation based on "embedded JS" option