@myrmidon/gve-snapshot-rendition 1.0.2 → 2.0.2
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.cjs.min.js +7 -7
- package/dist/index.cjs.min.js.map +1 -1
- package/dist/index.js +427 -845
- package/dist/index.js.map +1 -1
- package/dist/src/animation/animation-engine.d.ts +0 -13
- package/dist/src/core/gve-snapshot-rendition.d.ts +0 -1
- package/dist/src/hint-designer/gve-hint-designer.d.ts +0 -1
- package/dist/src/rendering/feature-resolver.d.ts +4 -4
- package/dist/src/rendering/hint-renderer.d.ts +6 -12
- package/dist/src/rendering/text-renderer.d.ts +12 -11
- package/dist/src/settings/hint-models.d.ts +0 -5
- package/dist/src/settings/settings.d.ts +0 -4
- package/package.json +5 -5
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
|
|
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).
|
|
@@ -1957,7 +1385,7 @@ class TextRenderer {
|
|
|
1957
1385
|
}
|
|
1958
1386
|
// Apply r_t-value override if specified
|
|
1959
1387
|
// This overrides the operation's value (the text being added) for display only
|
|
1960
|
-
if (config.textValue) {
|
|
1388
|
+
if (config.textValue !== null && config.textValue !== undefined) {
|
|
1961
1389
|
this._logger.debug("TextRenderer", `Applying r_t-value override: "${config.textValue}" (original text had ${nodes.length} characters)`);
|
|
1962
1390
|
// Replace node data with characters from r_t-value
|
|
1963
1391
|
// If r_t-value has fewer characters than nodes, we only override the first N nodes
|
|
@@ -1989,137 +1417,186 @@ class TextRenderer {
|
|
|
1989
1417
|
// Use first RBR for positioning (additional text doesn't repeat per RBR)
|
|
1990
1418
|
const rbr = rbrs[0];
|
|
1991
1419
|
this._logger.debug("TextRenderer", `Using RBR for positioning`, rbr);
|
|
1992
|
-
// 2. Calculate
|
|
1993
|
-
const position = config.textPosition || "o";
|
|
1994
|
-
const targetPos = this.calculateTargetPosition(position, rbr);
|
|
1995
|
-
// Parse offsets using RBR bounds (offsets can be like "0.5th" = half of RBR height)
|
|
1420
|
+
// 2. Calculate position and offsets
|
|
1421
|
+
const position = config.textPosition || "o";
|
|
1996
1422
|
const offsetX = parseOffset(config.textOffsetX || 0, rbr.height, rbr.width);
|
|
1997
1423
|
const offsetY = parseOffset(config.textOffsetY || 0, rbr.height, rbr.width);
|
|
1998
|
-
|
|
1999
|
-
|
|
1424
|
+
// 3. Render characters at preliminary positions (baseline y=0) into a hidden
|
|
1425
|
+
// temporary group so we can measure the actual visual EBR via getBBox().
|
|
1426
|
+
// This mirrors the hint renderer's approach: render → measure → reposition.
|
|
1427
|
+
// Using config font metrics here ensures correct character widths.
|
|
1428
|
+
const initialPositions = this.calculateAdditionalTextPositions(nodes, 0, 0, config);
|
|
1429
|
+
const tempGroup = createSVGElement("g");
|
|
1430
|
+
tempGroup.setAttribute("visibility", "hidden");
|
|
1431
|
+
rootSvg.appendChild(tempGroup);
|
|
1432
|
+
const measuredElements = [];
|
|
1433
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
1434
|
+
const node = nodes[i];
|
|
1435
|
+
const pos = initialPositions[i];
|
|
1436
|
+
if (isLineBreak(node) || isSpace(node))
|
|
1437
|
+
continue;
|
|
1438
|
+
const textEl = createTextElement(node.data, {
|
|
1439
|
+
id: `n_${node.id}`,
|
|
1440
|
+
class: `node version-${versionTag}`,
|
|
1441
|
+
x: pos.x,
|
|
1442
|
+
y: pos.y,
|
|
1443
|
+
});
|
|
1444
|
+
applyTextStyle(textEl, {
|
|
1445
|
+
fontFamily: config.fontFamily,
|
|
1446
|
+
fontSize: config.fontSize,
|
|
1447
|
+
foreColor: config.foreColor,
|
|
1448
|
+
backColor: config.backColor,
|
|
1449
|
+
italic: config.italic,
|
|
1450
|
+
bold: config.bold,
|
|
1451
|
+
});
|
|
1452
|
+
tempGroup.appendChild(textEl);
|
|
1453
|
+
measuredElements.push({ el: textEl, node, initX: pos.x, initY: pos.y });
|
|
1454
|
+
}
|
|
1455
|
+
// 4. Measure actual visual EBR from the temp group
|
|
1456
|
+
const ebrBbox = getSafeBBox(tempGroup);
|
|
1457
|
+
const ebr = {
|
|
1458
|
+
x: ebrBbox.x,
|
|
1459
|
+
y: ebrBbox.y,
|
|
1460
|
+
width: ebrBbox.width,
|
|
1461
|
+
height: ebrBbox.height,
|
|
1462
|
+
right: ebrBbox.x + ebrBbox.width,
|
|
1463
|
+
bottom: ebrBbox.y + ebrBbox.height,
|
|
1464
|
+
};
|
|
1465
|
+
// 5. Compute translation: align EBR with RBR using the same logic as hints
|
|
1466
|
+
const rbrAlignPoint = this.calculateRBRAlignmentPoint(position, rbr);
|
|
1467
|
+
const ebrAlignPoint = this.calculateEBRAlignmentPoint(position, ebr);
|
|
1468
|
+
const dx = rbrAlignPoint.x - ebrAlignPoint.x + offsetX;
|
|
1469
|
+
const dy = rbrAlignPoint.y - ebrAlignPoint.y + offsetY;
|
|
1470
|
+
tempGroup.remove();
|
|
2000
1471
|
this._logger.debug("TextRenderer", `Text position calculated`, {
|
|
2001
1472
|
position,
|
|
2002
|
-
|
|
1473
|
+
rbr,
|
|
1474
|
+
ebr,
|
|
1475
|
+
rbrAlignPoint,
|
|
1476
|
+
ebrAlignPoint,
|
|
2003
1477
|
offsets: { x: offsetX, y: offsetY },
|
|
2004
|
-
|
|
1478
|
+
translation: { dx, dy },
|
|
2005
1479
|
});
|
|
2006
|
-
//
|
|
2007
|
-
const
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
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
|
|
1480
|
+
// 6. Compute final textBounds for prolog check
|
|
1481
|
+
const textBounds = {
|
|
1482
|
+
x: ebr.x + dx,
|
|
1483
|
+
y: ebr.y + dy,
|
|
1484
|
+
width: ebr.width,
|
|
1485
|
+
height: ebr.height,
|
|
1486
|
+
right: ebr.right + dx,
|
|
1487
|
+
bottom: ebr.bottom + dy,
|
|
1488
|
+
};
|
|
1489
|
+
// 7. Check if prolog panning is needed
|
|
2048
1490
|
if (panZoomInstance &&
|
|
2049
1491
|
viewportWidth &&
|
|
2050
1492
|
viewportHeight &&
|
|
2051
|
-
this._settings.prologDuration > 0
|
|
2052
|
-
textBounds) {
|
|
1493
|
+
this._settings.prologDuration > 0) {
|
|
2053
1494
|
const isVisible = this._animationEngine.isElementVisible(textBounds, panZoomInstance, viewportWidth, viewportHeight);
|
|
2054
1495
|
if (!isVisible) {
|
|
2055
1496
|
this._logger.debug("TextRenderer", `Additional text for ${versionTag} would be outside viewport, executing prolog`);
|
|
2056
1497
|
await this._animationEngine.animateProlog(panZoomInstance, textBounds, viewportWidth, viewportHeight, this._settings.prologDuration);
|
|
2057
1498
|
}
|
|
2058
1499
|
}
|
|
2059
|
-
//
|
|
1500
|
+
// 8. Get animation function if specified
|
|
2060
1501
|
const animationFn = this._settings.charAnimationId
|
|
2061
1502
|
? this._animationEngine
|
|
2062
1503
|
.getFactory()
|
|
2063
1504
|
.resolveAnimation(`#${this._settings.charAnimationId}`, this._settings.animations, "char")
|
|
2064
1505
|
: undefined;
|
|
2065
|
-
//
|
|
2066
|
-
for (
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
1506
|
+
// 9. Move each element to its final position and add to the SVG
|
|
1507
|
+
for (const { el, node, initX, initY } of measuredElements) {
|
|
1508
|
+
el.setAttribute("x", String(initX + dx));
|
|
1509
|
+
el.setAttribute("y", String(initY + dy));
|
|
1510
|
+
rootSvg.appendChild(el);
|
|
1511
|
+
const bbox = getSafeBBox(el);
|
|
1512
|
+
this._boundsCache.set(`n_${node.id}`, bbox);
|
|
1513
|
+
if (animationFn) {
|
|
1514
|
+
el.style.opacity = "0";
|
|
1515
|
+
await this._animationEngine.animate(el, animationFn, rootSvg);
|
|
2072
1516
|
}
|
|
2073
|
-
await this.renderCharacter(node, pos, rootSvg, versionTag, animationFn, config);
|
|
2074
1517
|
}
|
|
2075
1518
|
this._logger.timeEnd(`renderAdditionalText-${versionTag}`);
|
|
2076
1519
|
}
|
|
2077
1520
|
/**
|
|
2078
1521
|
* Calculate positions for additional text characters.
|
|
2079
1522
|
* Additional text flows left to right from the base position.
|
|
1523
|
+
* When config is provided its font metrics are used for character width
|
|
1524
|
+
* measurement, matching what applyTextStyle will actually render.
|
|
2080
1525
|
*/
|
|
2081
|
-
calculateAdditionalTextPositions(nodes, baseX, baseY) {
|
|
1526
|
+
calculateAdditionalTextPositions(nodes, baseX, baseY, config) {
|
|
2082
1527
|
const positions = [];
|
|
2083
1528
|
let currentX = baseX;
|
|
2084
1529
|
const currentY = baseY;
|
|
1530
|
+
const fontSize = config?.fontSize ?? this._settings.fontSize;
|
|
1531
|
+
const fontFamily = config?.fontFamily ?? this._settings.fontFamily;
|
|
1532
|
+
const bold = config?.bold ?? this._settings.bold;
|
|
1533
|
+
const italic = config?.italic ?? this._settings.italic;
|
|
2085
1534
|
for (let i = 0; i < nodes.length; i++) {
|
|
2086
1535
|
const node = nodes[i];
|
|
2087
1536
|
if (isLineBreak(node)) {
|
|
2088
|
-
|
|
2089
|
-
positions.push({
|
|
2090
|
-
x: currentX,
|
|
2091
|
-
y: currentY,
|
|
2092
|
-
nodeId: node.id,
|
|
2093
|
-
lineNumber: 0,
|
|
2094
|
-
});
|
|
1537
|
+
positions.push({ x: currentX, y: currentY, nodeId: node.id, lineNumber: 0 });
|
|
2095
1538
|
}
|
|
2096
1539
|
else if (isSpace(node)) {
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
positions.push({
|
|
2100
|
-
x: currentX,
|
|
2101
|
-
y: currentY,
|
|
2102
|
-
nodeId: node.id,
|
|
2103
|
-
lineNumber: 0,
|
|
2104
|
-
});
|
|
1540
|
+
const spaceWidth = fontSize * 0.33;
|
|
1541
|
+
positions.push({ x: currentX, y: currentY, nodeId: node.id, lineNumber: 0 });
|
|
2105
1542
|
currentX += spaceWidth + this._settings.charSpacing;
|
|
2106
1543
|
}
|
|
2107
1544
|
else {
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
x: currentX,
|
|
2111
|
-
y: currentY,
|
|
2112
|
-
nodeId: node.id,
|
|
2113
|
-
lineNumber: 0,
|
|
2114
|
-
});
|
|
2115
|
-
// Calculate character width for next position
|
|
2116
|
-
// Use measurement root for consistent font metrics with actual rendering
|
|
2117
|
-
const charWidth = getTextWidth(node.data, this._settings.fontFamily, this._settings.fontSize, this._settings.bold, this._settings.italic, this._measurementRoot);
|
|
1545
|
+
positions.push({ x: currentX, y: currentY, nodeId: node.id, lineNumber: 0 });
|
|
1546
|
+
const charWidth = getTextWidth(node.data, fontFamily, fontSize, bold, italic, this._measurementRoot);
|
|
2118
1547
|
currentX += charWidth + this._settings.charSpacing;
|
|
2119
1548
|
}
|
|
2120
1549
|
}
|
|
2121
1550
|
return positions;
|
|
2122
1551
|
}
|
|
1552
|
+
/**
|
|
1553
|
+
* Calculate the RBR anchor point for a given position type.
|
|
1554
|
+
* This is the point on the RBR where the corresponding EBR point will land.
|
|
1555
|
+
*/
|
|
1556
|
+
calculateRBRAlignmentPoint(position, rbr) {
|
|
1557
|
+
const centerX = rbr.x + rbr.width / 2;
|
|
1558
|
+
const centerY = rbr.y + rbr.height / 2;
|
|
1559
|
+
switch (position) {
|
|
1560
|
+
case "n": return { x: centerX, y: rbr.y };
|
|
1561
|
+
case "ne": return { x: rbr.right, y: rbr.y };
|
|
1562
|
+
case "e": return { x: rbr.right, y: centerY };
|
|
1563
|
+
case "se": return { x: rbr.right, y: rbr.bottom };
|
|
1564
|
+
case "s": return { x: centerX, y: rbr.bottom };
|
|
1565
|
+
case "sw": return { x: rbr.x, y: rbr.bottom };
|
|
1566
|
+
case "w": return { x: rbr.x, y: centerY };
|
|
1567
|
+
case "nw": return { x: rbr.x, y: rbr.y };
|
|
1568
|
+
case "inw": return { x: rbr.x, y: rbr.y };
|
|
1569
|
+
case "ine": return { x: rbr.right, y: rbr.y };
|
|
1570
|
+
case "isw": return { x: rbr.x, y: rbr.bottom };
|
|
1571
|
+
case "ise": return { x: rbr.right, y: rbr.bottom };
|
|
1572
|
+
case "o":
|
|
1573
|
+
default: return { x: centerX, y: centerY };
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Calculate the EBR anchor point for a given position type.
|
|
1578
|
+
* This is the point on the EBR that should coincide with the RBR anchor point.
|
|
1579
|
+
*/
|
|
1580
|
+
calculateEBRAlignmentPoint(position, ebr) {
|
|
1581
|
+
const centerX = ebr.x + ebr.width / 2;
|
|
1582
|
+
const centerY = ebr.y + ebr.height / 2;
|
|
1583
|
+
switch (position) {
|
|
1584
|
+
case "n": return { x: centerX, y: ebr.bottom };
|
|
1585
|
+
case "ne": return { x: ebr.x, y: ebr.bottom };
|
|
1586
|
+
case "e": return { x: ebr.x, y: centerY };
|
|
1587
|
+
case "se": return { x: ebr.x, y: ebr.y };
|
|
1588
|
+
case "s": return { x: centerX, y: ebr.y };
|
|
1589
|
+
case "sw": return { x: ebr.right, y: ebr.y };
|
|
1590
|
+
case "w": return { x: ebr.right, y: centerY };
|
|
1591
|
+
case "nw": return { x: ebr.right, y: ebr.bottom };
|
|
1592
|
+
case "inw": return { x: ebr.x, y: ebr.y };
|
|
1593
|
+
case "ine": return { x: ebr.right, y: ebr.y };
|
|
1594
|
+
case "isw": return { x: ebr.x, y: ebr.bottom };
|
|
1595
|
+
case "ise": return { x: ebr.right, y: ebr.bottom };
|
|
1596
|
+
case "o":
|
|
1597
|
+
default: return { x: centerX, y: centerY };
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
2123
1600
|
/**
|
|
2124
1601
|
* Calculate RBRs from reference nodes.
|
|
2125
1602
|
* Nodes on different lines create separate RBRs.
|
|
@@ -2162,69 +1639,6 @@ class TextRenderer {
|
|
|
2162
1639
|
}
|
|
2163
1640
|
return rbrs;
|
|
2164
1641
|
}
|
|
2165
|
-
/**
|
|
2166
|
-
* Calculate target position on the RBR based on position type.
|
|
2167
|
-
*/
|
|
2168
|
-
calculateTargetPosition(position, rbr) {
|
|
2169
|
-
const centerX = rbr.x + rbr.width / 2;
|
|
2170
|
-
const centerY = rbr.y + rbr.height / 2;
|
|
2171
|
-
switch (position) {
|
|
2172
|
-
case "n": // North (top center)
|
|
2173
|
-
return { x: centerX, y: rbr.y };
|
|
2174
|
-
case "ne": // Northeast (top right)
|
|
2175
|
-
return { x: rbr.right, y: rbr.y };
|
|
2176
|
-
case "e": // East (middle right)
|
|
2177
|
-
return { x: rbr.right, y: centerY };
|
|
2178
|
-
case "se": // Southeast (bottom right)
|
|
2179
|
-
return { x: rbr.right, y: rbr.bottom };
|
|
2180
|
-
case "s": // South (bottom center)
|
|
2181
|
-
return { x: centerX, y: rbr.bottom };
|
|
2182
|
-
case "sw": // Southwest (bottom left)
|
|
2183
|
-
return { x: rbr.x, y: rbr.bottom };
|
|
2184
|
-
case "w": // West (middle left)
|
|
2185
|
-
return { x: rbr.x, y: centerY };
|
|
2186
|
-
case "nw": // Northwest (top left)
|
|
2187
|
-
return { x: rbr.x, y: rbr.y };
|
|
2188
|
-
case "o": // Origin (center)
|
|
2189
|
-
default:
|
|
2190
|
-
return { x: centerX, y: centerY };
|
|
2191
|
-
}
|
|
2192
|
-
}
|
|
2193
|
-
/**
|
|
2194
|
-
* Calculate bounding rectangle for a set of positioned characters.
|
|
2195
|
-
*/
|
|
2196
|
-
calculateTextBounds(nodes, positions) {
|
|
2197
|
-
if (nodes.length === 0)
|
|
2198
|
-
return null;
|
|
2199
|
-
// We need to estimate bounds before rendering
|
|
2200
|
-
// Use approximate character dimensions based on settings
|
|
2201
|
-
const charWidth = this._settings.fontSize * 0.6; // Approximate
|
|
2202
|
-
const charHeight = this._settings.fontSize;
|
|
2203
|
-
let minX = Infinity;
|
|
2204
|
-
let minY = Infinity;
|
|
2205
|
-
let maxX = -Infinity;
|
|
2206
|
-
let maxY = -Infinity;
|
|
2207
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
2208
|
-
const pos = positions[i];
|
|
2209
|
-
const node = nodes[i];
|
|
2210
|
-
if (isLineBreak(node) || isSpace(node))
|
|
2211
|
-
continue;
|
|
2212
|
-
minX = Math.min(minX, pos.x);
|
|
2213
|
-
minY = Math.min(minY, pos.y - charHeight);
|
|
2214
|
-
maxX = Math.max(maxX, pos.x + charWidth);
|
|
2215
|
-
maxY = Math.max(maxY, pos.y);
|
|
2216
|
-
}
|
|
2217
|
-
if (!isFinite(minX))
|
|
2218
|
-
return null;
|
|
2219
|
-
return {
|
|
2220
|
-
x: minX,
|
|
2221
|
-
y: minY,
|
|
2222
|
-
width: maxX - minX,
|
|
2223
|
-
height: maxY - minY,
|
|
2224
|
-
right: maxX,
|
|
2225
|
-
bottom: maxY,
|
|
2226
|
-
};
|
|
2227
|
-
}
|
|
2228
1642
|
/**
|
|
2229
1643
|
* Update settings.
|
|
2230
1644
|
*/
|
|
@@ -2239,12 +1653,11 @@ class TextRenderer {
|
|
|
2239
1653
|
* Hints are visual counterparts of editing operations.
|
|
2240
1654
|
*/
|
|
2241
1655
|
class HintRenderer {
|
|
2242
|
-
constructor(settings, logger, animationEngine, boundsCache
|
|
1656
|
+
constructor(settings, logger, animationEngine, boundsCache) {
|
|
2243
1657
|
this._settings = settings;
|
|
2244
1658
|
this._logger = logger;
|
|
2245
1659
|
this._animationEngine = animationEngine;
|
|
2246
1660
|
this._boundsCache = boundsCache;
|
|
2247
|
-
this._spreadingEngine = spreadingEngine;
|
|
2248
1661
|
}
|
|
2249
1662
|
/**
|
|
2250
1663
|
* Render a hint for an operation.
|
|
@@ -2269,8 +1682,8 @@ class HintRenderer {
|
|
|
2269
1682
|
this._logger.info(`Rendering hint: ${hintId} for operation ${operationId}`);
|
|
2270
1683
|
try {
|
|
2271
1684
|
// 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);
|
|
1685
|
+
// Overrides can target this specific hint (by key or ordinal) or all hints ("*")
|
|
1686
|
+
const effectiveHint = this.applyHintOverrides(hint, hintId, hintOrdinal, hintOverrides, allBaseNodes);
|
|
2274
1687
|
// 2. Resolve placeholders in SVG
|
|
2275
1688
|
const resolvedSvg = resolvePlaceholders(effectiveHint.svg, variables);
|
|
2276
1689
|
this._logger.debug("HintRenderer", `Placeholders resolved`, {
|
|
@@ -2302,6 +1715,12 @@ class HintRenderer {
|
|
|
2302
1715
|
}
|
|
2303
1716
|
// 5. Check if this is a placeholder hint
|
|
2304
1717
|
const hasPlaceholder = hintGroup.querySelector("#placeholder") !== null;
|
|
1718
|
+
// Default-mode placeholder: a placeholder element WITHOUT class="fit".
|
|
1719
|
+
// Per documentation, in default mode the text keeps its natural size and the
|
|
1720
|
+
// container (if any) scales to fit it — meaning no RBR-based scaling is applied.
|
|
1721
|
+
// Fit-mode placeholders still scale the hint to the RBR normally.
|
|
1722
|
+
const isDefaultPlaceholder = hasPlaceholder &&
|
|
1723
|
+
!hintGroup.querySelector("#placeholder")?.classList.contains("fit");
|
|
2305
1724
|
// 6. Render hint for each RBR (unless it's a placeholder hint and already rendered once)
|
|
2306
1725
|
let placeholderRendered = false;
|
|
2307
1726
|
for (let i = 0; i < rbrs.length; i++) {
|
|
@@ -2322,6 +1741,13 @@ class HintRenderer {
|
|
|
2322
1741
|
rootSvg.appendChild(currentHintGroup);
|
|
2323
1742
|
// 8. Get the scale factors needed to fit RBR
|
|
2324
1743
|
const scalingInfo = this.calculateScalingInfo(currentHintGroup, effectiveHint, rbr);
|
|
1744
|
+
// For default-mode placeholder hints the text drives the size, not the RBR.
|
|
1745
|
+
// Override any RBR-derived scale so the hint keeps its natural rendered size.
|
|
1746
|
+
// The EBR (measured from the natural-size hint) is then used to center it.
|
|
1747
|
+
if (isDefaultPlaceholder) {
|
|
1748
|
+
scalingInfo.scaleX = 1;
|
|
1749
|
+
scalingInfo.scaleY = 1;
|
|
1750
|
+
}
|
|
2325
1751
|
// 9. Apply scale and rotation transforms to prepare the hint
|
|
2326
1752
|
await this.applyHintSizeTransforms(currentHintGroup, effectiveHint, scalingInfo);
|
|
2327
1753
|
// 10. Get EBR (Element Bounding Rectangle) AFTER transformations
|
|
@@ -2329,11 +1755,7 @@ class HintRenderer {
|
|
|
2329
1755
|
const ebrBounds = this.getEBRBounds(currentHintGroup);
|
|
2330
1756
|
// 11. Apply positioning transform - align EBR with RBR based on position
|
|
2331
1757
|
await this.applyHintPositionTransform(currentHintGroup, effectiveHint, rbr, ebrBounds, scalingInfo);
|
|
2332
|
-
// 10.
|
|
2333
|
-
if (effectiveHint.solid) {
|
|
2334
|
-
await this.handleSpreading(currentHintGroup, rootSvg, versionTag);
|
|
2335
|
-
}
|
|
2336
|
-
// 11. Check if prolog panning is needed (element visibility check)
|
|
1758
|
+
// 10. Check if prolog panning is needed (element visibility check)
|
|
2337
1759
|
// This must happen AFTER positioning and spreading, but BEFORE making visible
|
|
2338
1760
|
if (panZoomInstance && viewportWidth && viewportHeight && this._settings.prologDuration > 0) {
|
|
2339
1761
|
// IMPORTANT: Use getTransformedBBox() instead of getSafeBBox() here!
|
|
@@ -2473,12 +1895,6 @@ class HintRenderer {
|
|
|
2473
1895
|
scaledSize: { width, height },
|
|
2474
1896
|
});
|
|
2475
1897
|
}
|
|
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
1898
|
// Calculate scale factors based on current hint size
|
|
2483
1899
|
const currentBBox = getSafeBBox(hintGroup);
|
|
2484
1900
|
const scaleX = currentBBox.width > 0 ? width / currentBBox.width : 1;
|
|
@@ -2674,74 +2090,32 @@ class HintRenderer {
|
|
|
2674
2090
|
return { x: centerX, y: centerY };
|
|
2675
2091
|
}
|
|
2676
2092
|
}
|
|
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
2093
|
/**
|
|
2723
2094
|
* Apply r_h-* overrides to hint properties.
|
|
2724
|
-
*
|
|
2095
|
+
* Priority (lowest to highest): wildcard ("*") < key-based < ordinal-based.
|
|
2725
2096
|
*
|
|
2726
2097
|
* @param hint - Original hint design
|
|
2727
|
-
* @param hintId - ID of the hint being rendered
|
|
2728
|
-
* @param
|
|
2098
|
+
* @param hintId - ID (key) of the hint being rendered
|
|
2099
|
+
* @param hintOrdinal - 1-based ordinal position of this hint within the operation
|
|
2100
|
+
* @param hintOverrides - Map of overrides from r_h-* features (keyed by ID string or ordinal number)
|
|
2729
2101
|
* @param allBaseNodes - All base nodes for displaced span resolution
|
|
2730
2102
|
* @returns Modified hint with overrides applied
|
|
2731
2103
|
*/
|
|
2732
|
-
applyHintOverrides(hint, hintId, hintOverrides, allBaseNodes) {
|
|
2104
|
+
applyHintOverrides(hint, hintId, hintOrdinal, hintOverrides, allBaseNodes) {
|
|
2733
2105
|
if (!hintOverrides || hintOverrides.size === 0) {
|
|
2734
2106
|
return hint;
|
|
2735
2107
|
}
|
|
2736
2108
|
// Clone hint to avoid modifying original
|
|
2737
2109
|
const effectiveHint = { ...hint };
|
|
2738
|
-
//
|
|
2739
|
-
const specificOverride = hintOverrides.get(hintId);
|
|
2110
|
+
// Look up overrides by wildcard, key, and ordinal; merge in priority order
|
|
2740
2111
|
const wildcardOverride = hintOverrides.get("*");
|
|
2741
|
-
|
|
2112
|
+
const specificOverride = hintOverrides.get(hintId);
|
|
2113
|
+
const ordinalOverride = hintOverrides.get(hintOrdinal);
|
|
2114
|
+
// Ordinal (most specific) wins over key-specific, which wins over wildcard
|
|
2742
2115
|
const override = {
|
|
2743
2116
|
...wildcardOverride,
|
|
2744
2117
|
...specificOverride,
|
|
2118
|
+
...ordinalOverride,
|
|
2745
2119
|
};
|
|
2746
2120
|
// Apply each override property
|
|
2747
2121
|
if (override.position !== undefined) {
|
|
@@ -2768,10 +2142,6 @@ class HintRenderer {
|
|
|
2768
2142
|
effectiveHint.rotation = parseFloat(override.rotation);
|
|
2769
2143
|
this._logger.debug("HintRenderer", `Override rotation for ${hintId}: ${override.rotation}`);
|
|
2770
2144
|
}
|
|
2771
|
-
if (override.solid !== undefined) {
|
|
2772
|
-
effectiveHint.solid = override.solid;
|
|
2773
|
-
this._logger.debug("HintRenderer", `Override solid for ${hintId}: ${override.solid}`);
|
|
2774
|
-
}
|
|
2775
2145
|
if (override.displacedSpan !== undefined) {
|
|
2776
2146
|
// Convert to displacedRefSpan format (e.g., "64x3")
|
|
2777
2147
|
const { nodeId, count } = override.displacedSpan;
|
|
@@ -2788,6 +2158,262 @@ class HintRenderer {
|
|
|
2788
2158
|
}
|
|
2789
2159
|
}
|
|
2790
2160
|
|
|
2161
|
+
/**
|
|
2162
|
+
* Feature resolver converts features into rendition configuration.
|
|
2163
|
+
* This handles all the r_* rendition features and applies them to create
|
|
2164
|
+
* a complete configuration for rendering.
|
|
2165
|
+
*/
|
|
2166
|
+
class FeatureResolver {
|
|
2167
|
+
constructor(logger) {
|
|
2168
|
+
this._logger = logger;
|
|
2169
|
+
}
|
|
2170
|
+
/**
|
|
2171
|
+
* Resolve rendition features into a complete configuration.
|
|
2172
|
+
* Features override the base settings.
|
|
2173
|
+
*
|
|
2174
|
+
* @param baseSettings - The base settings from component
|
|
2175
|
+
* @param features - Array of features to apply
|
|
2176
|
+
* @param nodeFeatureContext - Optional context for extracting node features for placeholder resolution
|
|
2177
|
+
* @returns Resolved rendition configuration
|
|
2178
|
+
*/
|
|
2179
|
+
resolve(baseSettings, features, nodeFeatureContext) {
|
|
2180
|
+
const config = {
|
|
2181
|
+
fontSize: baseSettings.fontSize,
|
|
2182
|
+
fontFamily: baseSettings.fontFamily,
|
|
2183
|
+
foreColor: baseSettings.foreColor,
|
|
2184
|
+
backColor: baseSettings.backColor,
|
|
2185
|
+
italic: baseSettings.italic,
|
|
2186
|
+
bold: baseSettings.bold,
|
|
2187
|
+
underline: baseSettings.underline,
|
|
2188
|
+
overline: baseSettings.overline,
|
|
2189
|
+
strike: baseSettings.strike,
|
|
2190
|
+
textOffsetX: 0,
|
|
2191
|
+
textOffsetY: 0,
|
|
2192
|
+
};
|
|
2193
|
+
if (!features || features.length === 0) {
|
|
2194
|
+
return config;
|
|
2195
|
+
}
|
|
2196
|
+
// Initialize hintVars map - will contain ALL feature values for placeholder
|
|
2197
|
+
// replacement
|
|
2198
|
+
config.hintVars = new Map();
|
|
2199
|
+
// Initialize hintOverrides map for r_h-* features
|
|
2200
|
+
config.hintOverrides = new Map();
|
|
2201
|
+
// Extract ALL features from reference nodes for placeholder resolution.
|
|
2202
|
+
// This includes features like "note", "group", etc. that don't have r_ prefix.
|
|
2203
|
+
// Features are now stored per-node keyed by version tag.
|
|
2204
|
+
if (nodeFeatureContext) {
|
|
2205
|
+
const { refNodes, outputTag } = nodeFeatureContext;
|
|
2206
|
+
for (const node of refNodes) {
|
|
2207
|
+
const nodeFeaturesArray = node.features?.[outputTag];
|
|
2208
|
+
if (!nodeFeaturesArray)
|
|
2209
|
+
continue;
|
|
2210
|
+
for (const nodeFeature of nodeFeaturesArray) {
|
|
2211
|
+
if (nodeFeature.name.startsWith("$"))
|
|
2212
|
+
continue;
|
|
2213
|
+
if (!config.hintVars.has(nodeFeature.name)) {
|
|
2214
|
+
config.hintVars.set(nodeFeature.name, nodeFeature.value);
|
|
2215
|
+
this._logger.debug("FeatureResolver", `Added node feature to hintVars from node ${node.id} @ ${outputTag}: ${nodeFeature.name}=${nodeFeature.value}`);
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
// Process each rendition feature from operation
|
|
2221
|
+
for (const feature of features) {
|
|
2222
|
+
if (!feature.name.startsWith("r_"))
|
|
2223
|
+
continue;
|
|
2224
|
+
const value = feature.value;
|
|
2225
|
+
// Add ALL r_* features to hintVars for placeholder replacement
|
|
2226
|
+
// This allows {{r_fore-color}}, {{r_font-size}}, etc. in hint SVG templates
|
|
2227
|
+
// These override any node features with the same name
|
|
2228
|
+
config.hintVars.set(feature.name, value);
|
|
2229
|
+
switch (feature.name) {
|
|
2230
|
+
case "r_font-size":
|
|
2231
|
+
config.fontSize = parseFloat(value);
|
|
2232
|
+
break;
|
|
2233
|
+
case "r_font-family":
|
|
2234
|
+
config.fontFamily = value;
|
|
2235
|
+
break;
|
|
2236
|
+
case "r_fore-color":
|
|
2237
|
+
config.foreColor = value;
|
|
2238
|
+
break;
|
|
2239
|
+
case "r_back-color":
|
|
2240
|
+
config.backColor = value;
|
|
2241
|
+
break;
|
|
2242
|
+
case "r_italic":
|
|
2243
|
+
config.italic = value === "true" || value === "1";
|
|
2244
|
+
break;
|
|
2245
|
+
case "r_bold":
|
|
2246
|
+
config.bold = value === "true" || value === "1";
|
|
2247
|
+
break;
|
|
2248
|
+
case "r_underline":
|
|
2249
|
+
config.underline = parseFloat(value);
|
|
2250
|
+
break;
|
|
2251
|
+
case "r_overline":
|
|
2252
|
+
config.overline = parseFloat(value);
|
|
2253
|
+
break;
|
|
2254
|
+
case "r_strike":
|
|
2255
|
+
config.strike = parseFloat(value);
|
|
2256
|
+
break;
|
|
2257
|
+
case "r_text-line-style":
|
|
2258
|
+
config.textLineStyle = value;
|
|
2259
|
+
break;
|
|
2260
|
+
case "r_text-line-color":
|
|
2261
|
+
config.textLineColor = value;
|
|
2262
|
+
break;
|
|
2263
|
+
case "r_rotate":
|
|
2264
|
+
config.rotate = parseFloat(value);
|
|
2265
|
+
break;
|
|
2266
|
+
// r_t-* features for added text
|
|
2267
|
+
case "r_t-position":
|
|
2268
|
+
config.textPosition = value;
|
|
2269
|
+
break;
|
|
2270
|
+
case "r_t-offset-x":
|
|
2271
|
+
// Store the value as-is (number or string). It will be parsed later
|
|
2272
|
+
// in renderAdditionalText when RBR bounds are available.
|
|
2273
|
+
config.textOffsetX = typeof value === "number" ? value : value;
|
|
2274
|
+
break;
|
|
2275
|
+
case "r_t-offset-y":
|
|
2276
|
+
// Store the value as-is (number or string). It will be parsed later
|
|
2277
|
+
// in renderAdditionalText when RBR bounds are available.
|
|
2278
|
+
config.textOffsetY = typeof value === "number" ? value : value;
|
|
2279
|
+
break;
|
|
2280
|
+
case "r_t-displaced-span":
|
|
2281
|
+
config.textDisplacedSpan = parseDisplacedSpan(value);
|
|
2282
|
+
if (!config.textDisplacedSpan) {
|
|
2283
|
+
this._logger.warn(`Invalid r_t-displaced-span value: ${value}`);
|
|
2284
|
+
}
|
|
2285
|
+
break;
|
|
2286
|
+
case "r_t-value":
|
|
2287
|
+
config.textValue = value;
|
|
2288
|
+
break;
|
|
2289
|
+
case "r_hints":
|
|
2290
|
+
config.hints = value.trim().split(/\s+/);
|
|
2291
|
+
break;
|
|
2292
|
+
case "r_hint-vars":
|
|
2293
|
+
// Parse custom variables and merge with existing hintVars
|
|
2294
|
+
const customVars = parseHintVars(value);
|
|
2295
|
+
customVars.forEach((val, key) => {
|
|
2296
|
+
config.hintVars.set(key, val);
|
|
2297
|
+
});
|
|
2298
|
+
break;
|
|
2299
|
+
// r_h-* features for hint property overrides
|
|
2300
|
+
default:
|
|
2301
|
+
if (feature.name.startsWith("r_h-")) {
|
|
2302
|
+
this.parseHintOverride(feature.name, value, config);
|
|
2303
|
+
}
|
|
2304
|
+
break;
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
return config;
|
|
2308
|
+
}
|
|
2309
|
+
/**
|
|
2310
|
+
* Extract character offsets from init features.
|
|
2311
|
+
* This is used for base text layout adjustments.
|
|
2312
|
+
*
|
|
2313
|
+
* @param features - Features from the first annotate operation (init features)
|
|
2314
|
+
* @returns Map of node IDs to their offsets (values may be numbers or "Ntw"/"Nth" strings)
|
|
2315
|
+
*/
|
|
2316
|
+
extractCharOffsets(features) {
|
|
2317
|
+
if (!features)
|
|
2318
|
+
return new Map();
|
|
2319
|
+
const charOffsetsValue = getFeatureValue(features, "r_char-offsets");
|
|
2320
|
+
if (!charOffsetsValue)
|
|
2321
|
+
return new Map();
|
|
2322
|
+
return parseCharOffsets(charOffsetsValue);
|
|
2323
|
+
}
|
|
2324
|
+
/**
|
|
2325
|
+
* Check if features contain a specific rendition feature.
|
|
2326
|
+
*/
|
|
2327
|
+
hasRenditionFeature(features, featureName) {
|
|
2328
|
+
if (!features)
|
|
2329
|
+
return false;
|
|
2330
|
+
return features.some((f) => f.name === featureName);
|
|
2331
|
+
}
|
|
2332
|
+
/**
|
|
2333
|
+
* Get the value of a rendition feature.
|
|
2334
|
+
*/
|
|
2335
|
+
getRenditionFeatureValue(features, featureName) {
|
|
2336
|
+
return getFeatureValue(features, featureName);
|
|
2337
|
+
}
|
|
2338
|
+
/**
|
|
2339
|
+
* Parse r_h-* hint override features.
|
|
2340
|
+
* Format: "value" applies to all hints, or "@target1 target2:value" applies to specific hints.
|
|
2341
|
+
* Targets can be hint ID strings or 1-based ordinal integers (e.g., "@1 beta:e" targets
|
|
2342
|
+
* the first hint by position and all hints with key "beta"). Numbers and keys can be mixed.
|
|
2343
|
+
*
|
|
2344
|
+
* @param featureName - The feature name (e.g., "r_h-position")
|
|
2345
|
+
* @param value - The feature value
|
|
2346
|
+
* @param config - The rendition config to update
|
|
2347
|
+
*/
|
|
2348
|
+
parseHintOverride(featureName, value, config) {
|
|
2349
|
+
// Extract property name from feature name (r_h-position -> position)
|
|
2350
|
+
const propertyName = featureName.substring(4); // Remove "r_h-"
|
|
2351
|
+
// Parse value to check if it targets specific hints
|
|
2352
|
+
// null = all hints; otherwise an array of string keys and/or numeric ordinals
|
|
2353
|
+
let targetHints = null;
|
|
2354
|
+
let actualValue = value;
|
|
2355
|
+
if (value.startsWith("@")) {
|
|
2356
|
+
// Format: "@target1 target2:value" where targets are hint IDs or 1-based ordinals
|
|
2357
|
+
const colonIndex = value.indexOf(":");
|
|
2358
|
+
if (colonIndex > 0) {
|
|
2359
|
+
const hintIdsStr = value.substring(1, colonIndex); // Remove "@" and get up to ":"
|
|
2360
|
+
targetHints = hintIdsStr.trim().split(/\s+/).map((token) => {
|
|
2361
|
+
const n = parseInt(token, 10);
|
|
2362
|
+
// Use numeric key for pure integer tokens (ordinal-based targeting)
|
|
2363
|
+
return !isNaN(n) && String(n) === token ? n : token;
|
|
2364
|
+
});
|
|
2365
|
+
actualValue = value.substring(colonIndex + 1);
|
|
2366
|
+
}
|
|
2367
|
+
else {
|
|
2368
|
+
this._logger.warn(`Invalid r_h-* format (missing colon): ${featureName}=${value}`);
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
// If no hints defined yet in config, we'll still store the override.
|
|
2373
|
+
// It will be applied when hints are processed.
|
|
2374
|
+
const hints = targetHints ?? (config.hints ?? ["*"]);
|
|
2375
|
+
for (const hintKey of hints) {
|
|
2376
|
+
// Get or create override object for this hint key / ordinal
|
|
2377
|
+
let override = config.hintOverrides.get(hintKey);
|
|
2378
|
+
if (!override) {
|
|
2379
|
+
override = {};
|
|
2380
|
+
config.hintOverrides.set(hintKey, override);
|
|
2381
|
+
}
|
|
2382
|
+
// Set the property value
|
|
2383
|
+
switch (propertyName) {
|
|
2384
|
+
case "position":
|
|
2385
|
+
override.position = actualValue;
|
|
2386
|
+
break;
|
|
2387
|
+
case "offset-x":
|
|
2388
|
+
override.offsetX = actualValue;
|
|
2389
|
+
break;
|
|
2390
|
+
case "offset-y":
|
|
2391
|
+
override.offsetY = actualValue;
|
|
2392
|
+
break;
|
|
2393
|
+
case "scale-x":
|
|
2394
|
+
override.scaleX = actualValue;
|
|
2395
|
+
break;
|
|
2396
|
+
case "scale-y":
|
|
2397
|
+
override.scaleY = actualValue;
|
|
2398
|
+
break;
|
|
2399
|
+
case "rotation":
|
|
2400
|
+
override.rotation = actualValue;
|
|
2401
|
+
break;
|
|
2402
|
+
case "displaced-span":
|
|
2403
|
+
override.displacedSpan = parseDisplacedSpan(actualValue);
|
|
2404
|
+
if (!override.displacedSpan) {
|
|
2405
|
+
this._logger.warn(`Invalid r_h-displaced-span value: ${actualValue}`);
|
|
2406
|
+
}
|
|
2407
|
+
break;
|
|
2408
|
+
default:
|
|
2409
|
+
this._logger.warn(`Unknown hint override property: ${propertyName}`);
|
|
2410
|
+
break;
|
|
2411
|
+
}
|
|
2412
|
+
this._logger.debug("FeatureResolver", `Added hint override for ${hintKey}: ${propertyName}=${actualValue}`);
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2791
2417
|
function getDefaultExportFromCjs (x) {
|
|
2792
2418
|
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
2793
2419
|
}
|
|
@@ -26291,7 +25917,7 @@ class GveSnapshotRendition extends HTMLElement {
|
|
|
26291
25917
|
* of the web component is loaded.
|
|
26292
25918
|
*/
|
|
26293
25919
|
static get version() {
|
|
26294
|
-
return "
|
|
25920
|
+
return "2.0.2";
|
|
26295
25921
|
}
|
|
26296
25922
|
constructor() {
|
|
26297
25923
|
super();
|
|
@@ -26308,9 +25934,7 @@ class GveSnapshotRendition extends HTMLElement {
|
|
|
26308
25934
|
this._autoForwardEnabled = this._settings.autoForwardOnGroup;
|
|
26309
25935
|
// Create logger
|
|
26310
25936
|
this._logger = new Logger("GVE-Rendition", this._settings.debug);
|
|
26311
|
-
// Create bounds cache and spreading engine
|
|
26312
25937
|
this._boundsCache = new BoundsCache(this._logger);
|
|
26313
|
-
this._spreadingEngine = new SpreadingEngine(this._boundsCache, this._logger);
|
|
26314
25938
|
// Create feature resolver
|
|
26315
25939
|
this._featureResolver = new FeatureResolver(this._logger);
|
|
26316
25940
|
// Create shadow DOM
|
|
@@ -26422,8 +26046,8 @@ class GveSnapshotRendition extends HTMLElement {
|
|
|
26422
26046
|
const gsap = window.gsap;
|
|
26423
26047
|
if (gsap) {
|
|
26424
26048
|
this._animationEngine = new AnimationEngine(gsap, this._logger);
|
|
26425
|
-
this._textRenderer = new TextRenderer(this._settings, this._logger, this._animationEngine, this._boundsCache
|
|
26426
|
-
this._hintRenderer = new HintRenderer(this._settings, this._logger, this._animationEngine, this._boundsCache
|
|
26049
|
+
this._textRenderer = new TextRenderer(this._settings, this._logger, this._animationEngine, this._boundsCache);
|
|
26050
|
+
this._hintRenderer = new HintRenderer(this._settings, this._logger, this._animationEngine, this._boundsCache);
|
|
26427
26051
|
this._logger.info("GSAP initialized");
|
|
26428
26052
|
this.renderContent();
|
|
26429
26053
|
}
|
|
@@ -27176,39 +26800,7 @@ class GveSnapshotRendition extends HTMLElement {
|
|
|
27176
26800
|
return;
|
|
27177
26801
|
}
|
|
27178
26802
|
this._logger.info(`Removing ${elements.length} elements from v${versionIndex}`);
|
|
27179
|
-
// Step 1:
|
|
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
|
|
26803
|
+
// Step 1: If backwardFadeOutTime > 0, animate fadeout
|
|
27212
26804
|
if (this._settings.backwardFadeOutTime > 0) {
|
|
27213
26805
|
const fadeOutPromises = [];
|
|
27214
26806
|
elements.forEach((el) => {
|
|
@@ -27220,7 +26812,7 @@ class GveSnapshotRendition extends HTMLElement {
|
|
|
27220
26812
|
});
|
|
27221
26813
|
await Promise.all(fadeOutPromises);
|
|
27222
26814
|
}
|
|
27223
|
-
// Step
|
|
26815
|
+
// Step 2: Remove elements from DOM
|
|
27224
26816
|
elements.forEach((el) => el.remove());
|
|
27225
26817
|
}
|
|
27226
26818
|
/**
|
|
@@ -41212,7 +40804,7 @@ function requireD () {
|
|
|
41212
40804
|
+ 'pragma private protected public pure ref return scope shared static struct '
|
|
41213
40805
|
+ 'super switch synchronized template this throw try typedef typeid typeof union '
|
|
41214
40806
|
+ 'unittest version void volatile while with __FILE__ __LINE__ __gshared|10 '
|
|
41215
|
-
+ '__thread __traits __DATE__ __EOF__ __TIME__ __TIMESTAMP__ __VENDOR__
|
|
40807
|
+
+ '__thread __traits __DATE__ __EOF__ __TIME__ __TIMESTAMP__ __VENDOR__ 2.0.2',
|
|
41216
40808
|
built_in:
|
|
41217
40809
|
'bool cdouble cent cfloat char creal dchar delegate double dstring float function '
|
|
41218
40810
|
+ 'idouble ifloat ireal long real short string ubyte ucent uint ulong ushort wchar '
|
|
@@ -91176,10 +90768,6 @@ class GveHintDesigner extends HTMLElement {
|
|
|
91176
90768
|
// Rotation
|
|
91177
90769
|
this._rotationInput = this.createInput("number", "0");
|
|
91178
90770
|
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
90771
|
// Displaced Ref Span
|
|
91184
90772
|
this._displacedRefSpanInput = this.createInput("text", "");
|
|
91185
90773
|
form.appendChild(this.createFormRow("Displaced Ref Span:", this._displacedRefSpanInput));
|
|
@@ -91665,8 +91253,6 @@ class GveHintDesigner extends HTMLElement {
|
|
|
91665
91253
|
this._scaleYInput.value = hint.scaleY?.toString() || "1";
|
|
91666
91254
|
if (this._rotationInput)
|
|
91667
91255
|
this._rotationInput.value = hint.rotation?.toString() || "0";
|
|
91668
|
-
if (this._solidCheckbox)
|
|
91669
|
-
this._solidCheckbox.checked = hint.solid || false;
|
|
91670
91256
|
if (this._displacedRefSpanInput)
|
|
91671
91257
|
this._displacedRefSpanInput.value = hint.displacedRefSpan || "";
|
|
91672
91258
|
if (this._svgTextarea) {
|
|
@@ -91732,8 +91318,6 @@ class GveHintDesigner extends HTMLElement {
|
|
|
91732
91318
|
this._scaleYInput.value = "1";
|
|
91733
91319
|
if (this._rotationInput)
|
|
91734
91320
|
this._rotationInput.value = "0";
|
|
91735
|
-
if (this._solidCheckbox)
|
|
91736
|
-
this._solidCheckbox.checked = false;
|
|
91737
91321
|
if (this._displacedRefSpanInput)
|
|
91738
91322
|
this._displacedRefSpanInput.value = "";
|
|
91739
91323
|
if (this._svgTextarea)
|
|
@@ -91806,7 +91390,6 @@ class GveHintDesigner extends HTMLElement {
|
|
|
91806
91390
|
scaleX: 1,
|
|
91807
91391
|
scaleY: 1,
|
|
91808
91392
|
rotation: 0,
|
|
91809
|
-
solid: false,
|
|
91810
91393
|
animation: "",
|
|
91811
91394
|
};
|
|
91812
91395
|
this._data.hints[hintId] = newHint;
|
|
@@ -91851,7 +91434,6 @@ class GveHintDesigner extends HTMLElement {
|
|
|
91851
91434
|
scaleX: parseFloat(this._scaleXInput?.value || "1"),
|
|
91852
91435
|
scaleY: parseFloat(this._scaleYInput?.value || "1"),
|
|
91853
91436
|
rotation: parseFloat(this._rotationInput?.value || "0"),
|
|
91854
|
-
solid: this._solidCheckbox?.checked || false,
|
|
91855
91437
|
displacedRefSpan: this._displacedRefSpanInput?.value || undefined,
|
|
91856
91438
|
};
|
|
91857
91439
|
// Handle animation based on "embedded JS" option
|