@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.cjs.min.js +7 -7
- package/dist/index.cjs.min.js.map +1 -1
- package/dist/index.js +346 -736
- 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 +8 -4
- 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).
|
|
@@ -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
|
-
|
|
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
|
|
1459
|
+
// 4. Calculate bounding rectangle for the text (needed for prolog)
|
|
2009
1460
|
const textBounds = this.calculateTextBounds(nodes, positions);
|
|
2010
|
-
// 5.
|
|
2011
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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.
|
|
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
|
-
*
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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 "
|
|
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
|
|
26426
|
-
this._hintRenderer = new HintRenderer(this._settings, this._logger, this._animationEngine, this._boundsCache
|
|
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:
|
|
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
|
|
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__
|
|
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
|