@myrmidon/gve-snapshot-rendition 2.0.9 → 2.0.11
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/core/gve-snapshot-rendition.d.ts +13 -0
- package/dist/hint-designer/gve-hint-designer.d.ts +17 -0
- package/dist/index.cjs.min.js +4 -4
- package/dist/index.cjs.min.js.map +1 -1
- package/dist/index.js +728 -440
- package/dist/index.js.map +1 -1
- package/dist/rendering/hint-renderer.d.ts +4 -1
- package/dist/rendering/svg-utils.d.ts +18 -1
- package/dist/rendering/text-renderer.d.ts +4 -1
- package/dist/settings/settings.d.ts +4 -0
- package/dist/ui/toolbar.d.ts +14 -0
- package/dist/utils/text-utils.d.ts +5 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -12,6 +12,8 @@ const DEFAULT_SETTINGS = {
|
|
|
12
12
|
underline: undefined,
|
|
13
13
|
overline: undefined,
|
|
14
14
|
strike: undefined,
|
|
15
|
+
textLineStyle: undefined,
|
|
16
|
+
textLineColor: undefined,
|
|
15
17
|
// Spacing settings
|
|
16
18
|
spaceWidthFraction: 0.33,
|
|
17
19
|
charSpacing: 2,
|
|
@@ -695,9 +697,13 @@ function applyTextStyle(element, style) {
|
|
|
695
697
|
attrs["font-style"] = "italic";
|
|
696
698
|
if (style.bold)
|
|
697
699
|
attrs["font-weight"] = "bold";
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
//
|
|
700
|
+
setAttributes(element, attrs);
|
|
701
|
+
// Text decorations. Applied via inline CSS longhands (not the SVG
|
|
702
|
+
// text-decoration attribute) so that thickness, line style and color are
|
|
703
|
+
// honored by the text layout engine and follow the glyphs when the
|
|
704
|
+
// character is rotated. NOTE: per-character rotation is NOT handled here,
|
|
705
|
+
// because a plain rotate() would spin the glyph around the SVG origin;
|
|
706
|
+
// renderers apply rotate(deg, x, y) around each character's own anchor.
|
|
701
707
|
const decorations = [];
|
|
702
708
|
if (style.underline)
|
|
703
709
|
decorations.push("underline");
|
|
@@ -706,9 +712,20 @@ function applyTextStyle(element, style) {
|
|
|
706
712
|
if (style.strike)
|
|
707
713
|
decorations.push("line-through");
|
|
708
714
|
if (decorations.length > 0) {
|
|
709
|
-
|
|
715
|
+
element.style.textDecorationLine = decorations.join(" ");
|
|
716
|
+
// CSS has a single thickness for all decoration lines of an element:
|
|
717
|
+
// use the first non-zero one (they are rarely combined).
|
|
718
|
+
const thickness = style.underline || style.overline || style.strike;
|
|
719
|
+
if (thickness && thickness > 0) {
|
|
720
|
+
element.style.textDecorationThickness = `${thickness}px`;
|
|
721
|
+
}
|
|
722
|
+
if (style.textLineStyle) {
|
|
723
|
+
element.style.textDecorationStyle = style.textLineStyle;
|
|
724
|
+
}
|
|
725
|
+
if (style.textLineColor) {
|
|
726
|
+
element.style.textDecorationColor = style.textLineColor;
|
|
727
|
+
}
|
|
710
728
|
}
|
|
711
|
-
setAttributes(element, attrs);
|
|
712
729
|
}
|
|
713
730
|
/**
|
|
714
731
|
* Parse SVG from string and return the root element.
|
|
@@ -803,6 +820,32 @@ function getTransformedBBox(element) {
|
|
|
803
820
|
return new DOMRect(0, 0, 0, 0);
|
|
804
821
|
}
|
|
805
822
|
}
|
|
823
|
+
/**
|
|
824
|
+
* Compute the axis-aligned bounding box of a rectangle rotated around a
|
|
825
|
+
* point, using SVG rotation direction (positive = clockwise, y-axis down).
|
|
826
|
+
*
|
|
827
|
+
* Used to cache correct bounds for per-character rotated text: getBBox()
|
|
828
|
+
* returns LOCAL coordinates which ignore the rotate(deg, x, y) transform
|
|
829
|
+
* applied to the element, while getTransformedBBox() would also bake in
|
|
830
|
+
* ancestor transforms (e.g. the pan/zoom viewport), which must stay out
|
|
831
|
+
* of the bounds cache.
|
|
832
|
+
*/
|
|
833
|
+
function rotateRectAroundPoint(rect, degrees, cx, cy) {
|
|
834
|
+
const rad = (degrees * Math.PI) / 180;
|
|
835
|
+
const cos = Math.cos(rad);
|
|
836
|
+
const sin = Math.sin(rad);
|
|
837
|
+
const corners = [
|
|
838
|
+
[rect.x, rect.y],
|
|
839
|
+
[rect.x + rect.width, rect.y],
|
|
840
|
+
[rect.x, rect.y + rect.height],
|
|
841
|
+
[rect.x + rect.width, rect.y + rect.height],
|
|
842
|
+
];
|
|
843
|
+
const xs = corners.map(([x, y]) => cx + (x - cx) * cos - (y - cy) * sin);
|
|
844
|
+
const ys = corners.map(([x, y]) => cy + (x - cx) * sin + (y - cy) * cos);
|
|
845
|
+
const minX = Math.min(...xs);
|
|
846
|
+
const minY = Math.min(...ys);
|
|
847
|
+
return new DOMRect(minX, minY, Math.max(...xs) - minX, Math.max(...ys) - minY);
|
|
848
|
+
}
|
|
806
849
|
/**
|
|
807
850
|
* Get computed text width for a given text and style.
|
|
808
851
|
* Creates a temporary SVG element to measure.
|
|
@@ -921,16 +964,30 @@ function parseOffset(value, textHeight, textWidth) {
|
|
|
921
964
|
}
|
|
922
965
|
/**
|
|
923
966
|
* Replace placeholders in SVG content.
|
|
924
|
-
* Placeholders are in the format {{featureName}}
|
|
967
|
+
* Placeholders are in the format {{featureName}}, or a fallback chain
|
|
968
|
+
* {{name ?? other ?? ...}}: names are tried left to right and the first one
|
|
969
|
+
* having a variable wins. There is no limit to the number of fallbacks.
|
|
970
|
+
* For instance {{r_fore-color ?? r_color}} resolves to the value of
|
|
971
|
+
* r_fore-color when defined, otherwise to the value of r_color.
|
|
925
972
|
*
|
|
926
973
|
* @param svgContent - The SVG content with placeholders
|
|
927
974
|
* @param variables - Map of variable names to values
|
|
928
975
|
* @returns SVG content with placeholders replaced
|
|
929
976
|
*/
|
|
930
977
|
function resolvePlaceholders(svgContent, variables) {
|
|
931
|
-
// Match {{
|
|
932
|
-
|
|
933
|
-
|
|
978
|
+
// Match {{name}} or {{name ?? name ?? ...}} where each name can contain
|
|
979
|
+
// letters, numbers, hyphens, and underscores
|
|
980
|
+
return svgContent.replace(/\{\{\s*([\w\-]+(?:\s*\?\?\s*[\w\-]+)*)\s*\}\}/g, (match, chain) => {
|
|
981
|
+
const names = chain.split("??").map((n) => n.trim());
|
|
982
|
+
for (const name of names) {
|
|
983
|
+
const value = variables.get(name);
|
|
984
|
+
// An empty string is a defined value; only a missing variable
|
|
985
|
+
// falls through to the next name in the chain
|
|
986
|
+
if (value !== undefined) {
|
|
987
|
+
return value;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
return match; // Keep placeholder if no name in the chain resolves
|
|
934
991
|
});
|
|
935
992
|
}
|
|
936
993
|
|
|
@@ -1028,12 +1085,17 @@ function parseDisplacedSpan(value) {
|
|
|
1028
1085
|
*/
|
|
1029
1086
|
function parseHintVars(value) {
|
|
1030
1087
|
const result = new Map();
|
|
1031
|
-
// Match pattern name=value
|
|
1088
|
+
// Match pattern name=value, splitting at the first "=" only so that
|
|
1089
|
+
// values containing "=" (e.g. URLs with query strings) survive intact
|
|
1032
1090
|
const pairs = value.trim().split(/\s+/);
|
|
1033
1091
|
for (const pair of pairs) {
|
|
1034
|
-
const
|
|
1092
|
+
const sepIndex = pair.indexOf("=");
|
|
1093
|
+
if (sepIndex <= 0)
|
|
1094
|
+
continue;
|
|
1095
|
+
const name = pair.substring(0, sepIndex).trim();
|
|
1096
|
+
const val = pair.substring(sepIndex + 1).trim();
|
|
1035
1097
|
if (name && val) {
|
|
1036
|
-
result.set(name
|
|
1098
|
+
result.set(name, val);
|
|
1037
1099
|
}
|
|
1038
1100
|
}
|
|
1039
1101
|
return result;
|
|
@@ -1300,9 +1362,11 @@ class TextRenderer {
|
|
|
1300
1362
|
const node = nodes[i];
|
|
1301
1363
|
const pos = positions[i];
|
|
1302
1364
|
// Cache bounds for spaces even though they're not rendered
|
|
1303
|
-
// This is needed for RBR calculations that include spaces
|
|
1365
|
+
// This is needed for RBR calculations that include spaces.
|
|
1366
|
+
// Use the layout's space width (reference char width × fraction) so the
|
|
1367
|
+
// cached bounds match the horizontal advance actually applied.
|
|
1304
1368
|
if (isSpace(node)) {
|
|
1305
|
-
const spaceWidth = this.
|
|
1369
|
+
const spaceWidth = this._textLayout.getSpaceWidth();
|
|
1306
1370
|
const spaceY = pos.y - this._settings.fontSize; // Approximate top of character
|
|
1307
1371
|
// Create a DOMRect-like object for the space bounds
|
|
1308
1372
|
const spaceBounds = {
|
|
@@ -1352,6 +1416,11 @@ class TextRenderer {
|
|
|
1352
1416
|
backColor: config.backColor,
|
|
1353
1417
|
italic: config.italic,
|
|
1354
1418
|
bold: config.bold,
|
|
1419
|
+
underline: config.underline,
|
|
1420
|
+
overline: config.overline,
|
|
1421
|
+
strike: config.strike,
|
|
1422
|
+
textLineStyle: config.textLineStyle,
|
|
1423
|
+
textLineColor: config.textLineColor,
|
|
1355
1424
|
});
|
|
1356
1425
|
}
|
|
1357
1426
|
else {
|
|
@@ -1362,12 +1431,24 @@ class TextRenderer {
|
|
|
1362
1431
|
backColor: this._settings.backColor,
|
|
1363
1432
|
italic: this._settings.italic,
|
|
1364
1433
|
bold: this._settings.bold,
|
|
1434
|
+
underline: this._settings.underline,
|
|
1435
|
+
overline: this._settings.overline,
|
|
1436
|
+
strike: this._settings.strike,
|
|
1437
|
+
textLineStyle: this._settings.textLineStyle,
|
|
1438
|
+
textLineColor: this._settings.textLineColor,
|
|
1365
1439
|
});
|
|
1366
1440
|
}
|
|
1441
|
+
// Per-character rotation (r_rotate): rotate each glyph around its own
|
|
1442
|
+
// anchor point (the character's baseline position), not the SVG origin.
|
|
1443
|
+
const rotate = config?.rotate;
|
|
1444
|
+
if (rotate) {
|
|
1445
|
+
textEl.setAttribute("transform", `rotate(${rotate}, ${position.x}, ${position.y})`);
|
|
1446
|
+
}
|
|
1367
1447
|
rootSvg.appendChild(textEl);
|
|
1368
|
-
// Cache bounds
|
|
1448
|
+
// Cache bounds. getBBox() ignores the element's own transform, so for
|
|
1449
|
+
// rotated characters the cached rect is recomputed around the anchor.
|
|
1369
1450
|
const bbox = getSafeBBox(textEl);
|
|
1370
|
-
this._boundsCache.set(`n_${node.id}`, bbox);
|
|
1451
|
+
this._boundsCache.set(`n_${node.id}`, rotate ? rotateRectAroundPoint(bbox, rotate, position.x, position.y) : bbox);
|
|
1371
1452
|
// Animate if function provided
|
|
1372
1453
|
if (animationFn) {
|
|
1373
1454
|
// Set initial opacity to 0 for animation via CSS style (GSAP animates CSS)
|
|
@@ -1387,13 +1468,17 @@ class TextRenderer {
|
|
|
1387
1468
|
* @param panZoomInstance - The svg-pan-zoom instance (for prolog)
|
|
1388
1469
|
* @param viewportWidth - Width of the visible viewport (for prolog)
|
|
1389
1470
|
* @param viewportHeight - Height of the visible viewport (for prolog)
|
|
1471
|
+
* @param instant - When true (compressed time mode), skip the per-character
|
|
1472
|
+
* entrance animation and any prolog panning animation, applying final
|
|
1473
|
+
* state immediately instead.
|
|
1390
1474
|
* @returns Promise that resolves when rendering completes
|
|
1391
1475
|
*/
|
|
1392
|
-
async renderAdditionalText(nodes, rootSvg, config, versionTag, referenceNodes, allBaseNodes, panZoomInstance, viewportWidth, viewportHeight) {
|
|
1476
|
+
async renderAdditionalText(nodes, rootSvg, config, versionTag, referenceNodes, allBaseNodes, panZoomInstance, viewportWidth, viewportHeight, instant) {
|
|
1393
1477
|
this._logger.time(`renderAdditionalText-${versionTag}`);
|
|
1394
1478
|
this._logger.info(`Rendering ${nodes.length} additional text characters for ${versionTag}`);
|
|
1395
1479
|
if (nodes.length === 0) {
|
|
1396
1480
|
this._logger.warn("No nodes to render");
|
|
1481
|
+
this._logger.timeEnd(`renderAdditionalText-${versionTag}`);
|
|
1397
1482
|
return;
|
|
1398
1483
|
}
|
|
1399
1484
|
// Apply r_t-value override if specified
|
|
@@ -1410,8 +1495,8 @@ class TextRenderer {
|
|
|
1410
1495
|
}
|
|
1411
1496
|
// If r_t-value has fewer characters, we need to only render those nodes
|
|
1412
1497
|
if (overrideChars.length < nodes.length) {
|
|
1413
|
-
nodes = nodes.slice(0, overrideChars.length);
|
|
1414
1498
|
this._logger.debug("TextRenderer", `r_t-value has fewer characters (${overrideChars.length}) than nodes (${nodes.length}), only rendering first ${overrideChars.length} characters`);
|
|
1499
|
+
nodes = nodes.slice(0, overrideChars.length);
|
|
1415
1500
|
}
|
|
1416
1501
|
}
|
|
1417
1502
|
// 1. Calculate RBR (or use displaced span if specified)
|
|
@@ -1425,6 +1510,7 @@ class TextRenderer {
|
|
|
1425
1510
|
}
|
|
1426
1511
|
if (rbrs.length === 0) {
|
|
1427
1512
|
this._logger.warn("No RBRs calculated for additional text, skipping");
|
|
1513
|
+
this._logger.timeEnd(`renderAdditionalText-${versionTag}`);
|
|
1428
1514
|
return;
|
|
1429
1515
|
}
|
|
1430
1516
|
// Use first RBR for positioning (additional text doesn't repeat per RBR)
|
|
@@ -1465,7 +1551,17 @@ class TextRenderer {
|
|
|
1465
1551
|
backColor: config.backColor,
|
|
1466
1552
|
italic: config.italic,
|
|
1467
1553
|
bold: config.bold,
|
|
1554
|
+
underline: config.underline,
|
|
1555
|
+
overline: config.overline,
|
|
1556
|
+
strike: config.strike,
|
|
1557
|
+
textLineStyle: config.textLineStyle,
|
|
1558
|
+
textLineColor: config.textLineColor,
|
|
1468
1559
|
});
|
|
1560
|
+
// Per-character rotation (r_rotate) is a size transform: apply it
|
|
1561
|
+
// before measuring so the EBR accounts for the rotated glyphs.
|
|
1562
|
+
if (config.rotate) {
|
|
1563
|
+
textEl.setAttribute("transform", `rotate(${config.rotate}, ${pos.x}, ${pos.y})`);
|
|
1564
|
+
}
|
|
1469
1565
|
tempGroup.appendChild(textEl);
|
|
1470
1566
|
measuredElements.push({ el: textEl, node, initX: pos.x, initY: pos.y });
|
|
1471
1567
|
}
|
|
@@ -1511,22 +1607,34 @@ class TextRenderer {
|
|
|
1511
1607
|
const isVisible = this._animationEngine.isElementVisible(textBounds, panZoomInstance, viewportWidth, viewportHeight);
|
|
1512
1608
|
if (!isVisible) {
|
|
1513
1609
|
this._logger.debug("TextRenderer", `Additional text for ${versionTag} would be outside viewport, executing prolog`);
|
|
1514
|
-
await this._animationEngine.animateProlog(panZoomInstance, textBounds, viewportWidth, viewportHeight, this._settings.prologDuration);
|
|
1610
|
+
await this._animationEngine.animateProlog(panZoomInstance, textBounds, viewportWidth, viewportHeight, instant ? 0 : this._settings.prologDuration);
|
|
1515
1611
|
}
|
|
1516
1612
|
}
|
|
1517
|
-
// 8. Get animation function if specified
|
|
1518
|
-
|
|
1613
|
+
// 8. Get animation function if specified. In instant mode (compressed
|
|
1614
|
+
// time), skip resolving it entirely so characters appear immediately.
|
|
1615
|
+
const animationFn = !instant && this._settings.charAnimationId
|
|
1519
1616
|
? this._animationEngine
|
|
1520
1617
|
.getFactory()
|
|
1521
1618
|
.resolveAnimation(`#${this._settings.charAnimationId}`, this._settings.animations, "char")
|
|
1522
1619
|
: undefined;
|
|
1523
1620
|
// 9. Move each element to its final position and add to the SVG
|
|
1524
1621
|
for (const { el, node, initX, initY } of measuredElements) {
|
|
1525
|
-
|
|
1526
|
-
|
|
1622
|
+
const finalX = initX + dx;
|
|
1623
|
+
const finalY = initY + dy;
|
|
1624
|
+
el.setAttribute("x", String(finalX));
|
|
1625
|
+
el.setAttribute("y", String(finalY));
|
|
1626
|
+
// Re-anchor the per-character rotation to the moved position, or the
|
|
1627
|
+
// glyph would rotate around the pre-translation measuring anchor.
|
|
1628
|
+
if (config.rotate) {
|
|
1629
|
+
el.setAttribute("transform", `rotate(${config.rotate}, ${finalX}, ${finalY})`);
|
|
1630
|
+
}
|
|
1527
1631
|
rootSvg.appendChild(el);
|
|
1632
|
+
// getBBox() ignores the element's own transform, so for rotated
|
|
1633
|
+
// characters the cached rect is recomputed around the anchor.
|
|
1528
1634
|
const bbox = getSafeBBox(el);
|
|
1529
|
-
this._boundsCache.set(`n_${node.id}`,
|
|
1635
|
+
this._boundsCache.set(`n_${node.id}`, config.rotate
|
|
1636
|
+
? rotateRectAroundPoint(bbox, config.rotate, finalX, finalY)
|
|
1637
|
+
: bbox);
|
|
1530
1638
|
if (animationFn) {
|
|
1531
1639
|
el.style.opacity = "0";
|
|
1532
1640
|
await this._animationEngine.animate(el, animationFn, rootSvg);
|
|
@@ -1543,23 +1651,34 @@ class TextRenderer {
|
|
|
1543
1651
|
calculateAdditionalTextPositions(nodes, baseX, baseY, config) {
|
|
1544
1652
|
const positions = [];
|
|
1545
1653
|
let currentX = baseX;
|
|
1546
|
-
|
|
1654
|
+
let currentY = baseY;
|
|
1655
|
+
let lineNumber = 0;
|
|
1547
1656
|
const fontSize = config?.fontSize ?? this._settings.fontSize;
|
|
1548
1657
|
const fontFamily = config?.fontFamily ?? this._settings.fontFamily;
|
|
1549
1658
|
const bold = config?.bold ?? this._settings.bold;
|
|
1550
1659
|
const italic = config?.italic ?? this._settings.italic;
|
|
1660
|
+
// Space width follows the same rule as base text: a fraction
|
|
1661
|
+
// (spaceWidthFraction) of the reference character's width in the
|
|
1662
|
+
// effective font, not of the font size.
|
|
1663
|
+
const spaceWidth = getAverageCharWidth(fontFamily, fontSize, bold, italic, this._measurementRoot) *
|
|
1664
|
+
this._settings.spaceWidthFraction;
|
|
1551
1665
|
for (let i = 0; i < nodes.length; i++) {
|
|
1552
1666
|
const node = nodes[i];
|
|
1553
1667
|
if (isLineBreak(node)) {
|
|
1554
|
-
|
|
1668
|
+
// Line break: start a new line, mirroring base text layout. Uses the
|
|
1669
|
+
// effective font size so smaller/larger added text wraps accordingly.
|
|
1670
|
+
currentX = baseX;
|
|
1671
|
+
currentY +=
|
|
1672
|
+
fontSize * this._settings.lineHeight + this._settings.lineSpacing;
|
|
1673
|
+
lineNumber++;
|
|
1674
|
+
positions.push({ x: currentX, y: currentY, nodeId: node.id, lineNumber });
|
|
1555
1675
|
}
|
|
1556
1676
|
else if (isSpace(node)) {
|
|
1557
|
-
|
|
1558
|
-
positions.push({ x: currentX, y: currentY, nodeId: node.id, lineNumber: 0 });
|
|
1677
|
+
positions.push({ x: currentX, y: currentY, nodeId: node.id, lineNumber });
|
|
1559
1678
|
currentX += spaceWidth + this._settings.charSpacing;
|
|
1560
1679
|
}
|
|
1561
1680
|
else {
|
|
1562
|
-
positions.push({ x: currentX, y: currentY, nodeId: node.id, lineNumber
|
|
1681
|
+
positions.push({ x: currentX, y: currentY, nodeId: node.id, lineNumber });
|
|
1563
1682
|
const charWidth = getTextWidth(node.data, fontFamily, fontSize, bold, italic, this._measurementRoot);
|
|
1564
1683
|
currentX += charWidth + this._settings.charSpacing;
|
|
1565
1684
|
}
|
|
@@ -1625,8 +1744,15 @@ class TextRenderer {
|
|
|
1625
1744
|
if (nodes.length === 0)
|
|
1626
1745
|
return [];
|
|
1627
1746
|
const rbrs = [];
|
|
1628
|
-
const nodeElementIds = nodes.map((n) => `n_${n.id}`);
|
|
1629
1747
|
// Group nodes by line by checking their Y coordinates.
|
|
1748
|
+
// IMPORTANT: spaces and line breaks are skipped, exactly as in
|
|
1749
|
+
// HintRenderer.calculateRBRs. They have no SVG element, so the baseline
|
|
1750
|
+
// read below would fall back to their cached synthetic bounds top
|
|
1751
|
+
// (baseline - fontSize), which differs from neighboring characters'
|
|
1752
|
+
// baseline by ~fontSize — far beyond the 5px tolerance — falsely
|
|
1753
|
+
// splitting a single line into one group per word. Interior spaces
|
|
1754
|
+
// don't affect the union bounds anyway, since the flanking characters
|
|
1755
|
+
// cover their x-range.
|
|
1630
1756
|
// IMPORTANT: this compares the character's laid-out baseline Y (the `y`
|
|
1631
1757
|
// attribute TextLayout assigned when positioning it), NOT its cached
|
|
1632
1758
|
// getBBox().y. The tight ink bbox top varies per glyph shape (ascenders,
|
|
@@ -1636,7 +1762,11 @@ class TextRenderer {
|
|
|
1636
1762
|
const lineGroups = [];
|
|
1637
1763
|
let currentLineGroup = [];
|
|
1638
1764
|
let lastBaselineY = null;
|
|
1639
|
-
for (const
|
|
1765
|
+
for (const node of nodes) {
|
|
1766
|
+
if (isSpace(node) || isLineBreak(node)) {
|
|
1767
|
+
continue;
|
|
1768
|
+
}
|
|
1769
|
+
const elementId = `n_${node.id}`;
|
|
1640
1770
|
const bounds = this._boundsCache.get(elementId);
|
|
1641
1771
|
if (!bounds) {
|
|
1642
1772
|
this._logger.warn(`Bounds not found for node element ${elementId}`);
|
|
@@ -1659,6 +1789,17 @@ class TextRenderer {
|
|
|
1659
1789
|
if (currentLineGroup.length > 0) {
|
|
1660
1790
|
lineGroups.push(currentLineGroup);
|
|
1661
1791
|
}
|
|
1792
|
+
// Fallback: if every reference node is invisible (e.g. the operation's
|
|
1793
|
+
// reference span is a lone space), use the cached synthetic bounds of
|
|
1794
|
+
// all nodes as a single group so the added text still gets an RBR.
|
|
1795
|
+
if (lineGroups.length === 0) {
|
|
1796
|
+
const cachedIds = nodes
|
|
1797
|
+
.map((n) => `n_${n.id}`)
|
|
1798
|
+
.filter((id) => this._boundsCache.get(id) !== undefined);
|
|
1799
|
+
if (cachedIds.length > 0) {
|
|
1800
|
+
lineGroups.push(cachedIds);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1662
1803
|
// Calculate RBR for each line group
|
|
1663
1804
|
for (const group of lineGroups) {
|
|
1664
1805
|
const rbr = this._boundsCache.getUnionBounds(group);
|
|
@@ -1704,9 +1845,12 @@ class HintRenderer {
|
|
|
1704
1845
|
* @param panZoomInstance - The svg-pan-zoom instance (for prolog)
|
|
1705
1846
|
* @param viewportWidth - Width of the visible viewport (for prolog)
|
|
1706
1847
|
* @param viewportHeight - Height of the visible viewport (for prolog)
|
|
1848
|
+
* @param instant - When true (compressed time mode), skip the entrance
|
|
1849
|
+
* animation and any prolog panning animation, applying final state
|
|
1850
|
+
* immediately instead.
|
|
1707
1851
|
* @returns Promise that resolves when rendering completes
|
|
1708
1852
|
*/
|
|
1709
|
-
async renderHint(hintId, hint, referenceNodes, rootSvg, variables, operationId, hintOrdinal, allBaseNodes, versionTag, hintOverrides, panZoomInstance, viewportWidth, viewportHeight) {
|
|
1853
|
+
async renderHint(hintId, hint, referenceNodes, rootSvg, variables, operationId, hintOrdinal, allBaseNodes, versionTag, hintOverrides, panZoomInstance, viewportWidth, viewportHeight, instant) {
|
|
1710
1854
|
this._logger.time(`renderHint-${hintId}`);
|
|
1711
1855
|
this._logger.info(`Rendering hint: ${hintId} for operation ${operationId}`);
|
|
1712
1856
|
try {
|
|
@@ -1800,19 +1944,21 @@ class HintRenderer {
|
|
|
1800
1944
|
const isVisible = this._animationEngine.isElementVisible(finalBbox, panZoomInstance, viewportWidth, viewportHeight);
|
|
1801
1945
|
if (!isVisible) {
|
|
1802
1946
|
this._logger.debug("HintRenderer", `Element ${currentHintGroup.getAttribute("id")} would be outside viewport, executing prolog`);
|
|
1803
|
-
await this._animationEngine.animateProlog(panZoomInstance, finalBbox, viewportWidth, viewportHeight, this._settings.prologDuration);
|
|
1947
|
+
await this._animationEngine.animateProlog(panZoomInstance, finalBbox, viewportWidth, viewportHeight, instant ? 0 : this._settings.prologDuration);
|
|
1804
1948
|
}
|
|
1805
1949
|
}
|
|
1806
|
-
// 12. Make visible (set initial opacity to 0 if animation present)
|
|
1950
|
+
// 12. Make visible (set initial opacity to 0 if animation present).
|
|
1951
|
+
// In instant mode (compressed time), skip the entrance animation
|
|
1952
|
+
// entirely so the hint appears in its final state right away.
|
|
1807
1953
|
currentHintGroup.removeAttribute("visibility");
|
|
1808
|
-
if (effectiveHint.animation) {
|
|
1954
|
+
if (effectiveHint.animation && !instant) {
|
|
1809
1955
|
currentHintGroup.style.opacity = "0";
|
|
1810
1956
|
}
|
|
1811
1957
|
// 13. Cache bounds
|
|
1812
1958
|
const bbox = getSafeBBox(currentHintGroup);
|
|
1813
1959
|
this._boundsCache.set(currentHintGroup.getAttribute("id"), bbox);
|
|
1814
1960
|
// 14. Animate entrance
|
|
1815
|
-
if (effectiveHint.animation) {
|
|
1961
|
+
if (effectiveHint.animation && !instant) {
|
|
1816
1962
|
const animation = this._animationEngine
|
|
1817
1963
|
.getFactory()
|
|
1818
1964
|
.resolveAnimation(effectiveHint.animation, this._settings.animations, `hint-${hintId}`);
|
|
@@ -2241,6 +2387,8 @@ class FeatureResolver {
|
|
|
2241
2387
|
underline: baseSettings.underline,
|
|
2242
2388
|
overline: baseSettings.overline,
|
|
2243
2389
|
strike: baseSettings.strike,
|
|
2390
|
+
textLineStyle: baseSettings.textLineStyle,
|
|
2391
|
+
textLineColor: baseSettings.textLineColor,
|
|
2244
2392
|
textOffsetX: 0,
|
|
2245
2393
|
textOffsetY: 0,
|
|
2246
2394
|
};
|
|
@@ -2423,9 +2571,13 @@ class FeatureResolver {
|
|
|
2423
2571
|
return;
|
|
2424
2572
|
}
|
|
2425
2573
|
}
|
|
2426
|
-
//
|
|
2427
|
-
//
|
|
2428
|
-
|
|
2574
|
+
// Untargeted overrides (no "@" prefix) are always stored under the "*"
|
|
2575
|
+
// wildcard key. Storing them under the hint keys from config.hints would
|
|
2576
|
+
// (a) depend on whether r_hints was parsed before this feature, and
|
|
2577
|
+
// (b) give them key-level priority, letting them overwrite an explicit
|
|
2578
|
+
// "@key:" override for the same property — the documented precedence is
|
|
2579
|
+
// ordinal > key > wildcard (see applyHintOverrides in HintRenderer).
|
|
2580
|
+
const hints = targetHints ?? ["*"];
|
|
2429
2581
|
for (const hintKey of hints) {
|
|
2430
2582
|
// Get or create override object for this hint key / ordinal
|
|
2431
2583
|
let override = config.hintOverrides.get(hintKey);
|
|
@@ -4892,6 +5044,7 @@ class Toolbar {
|
|
|
4892
5044
|
this._disabledButtonIds = [];
|
|
4893
5045
|
this._slideshowReverse = false;
|
|
4894
5046
|
this._autoForwardEnabled = false;
|
|
5047
|
+
this._compressedTimeModeEnabled = false;
|
|
4895
5048
|
this.container = container;
|
|
4896
5049
|
this.render();
|
|
4897
5050
|
}
|
|
@@ -4937,6 +5090,9 @@ class Toolbar {
|
|
|
4937
5090
|
<button id="auto-forward-btn" class="toolbar-btn" title="Auto-Forward on Group: Continue through grouped operations automatically">
|
|
4938
5091
|
<i data-feather="fast-forward"></i>
|
|
4939
5092
|
</button>
|
|
5093
|
+
<button id="compressed-time-btn" class="toolbar-btn" title="Compressed Time: jump between versions instantly, without animations or delays">
|
|
5094
|
+
<i data-feather="zap"></i>
|
|
5095
|
+
</button>
|
|
4940
5096
|
</div>
|
|
4941
5097
|
|
|
4942
5098
|
<!-- Display Group -->
|
|
@@ -4990,6 +5146,7 @@ class Toolbar {
|
|
|
4990
5146
|
this.slideshowStopBtn = this.container.querySelector("#slideshow-stop-btn");
|
|
4991
5147
|
this.slideshowReverseBtn = this.container.querySelector("#slideshow-reverse-btn");
|
|
4992
5148
|
this.autoForwardBtn = this.container.querySelector("#auto-forward-btn");
|
|
5149
|
+
this.compressedTimeBtn = this.container.querySelector("#compressed-time-btn");
|
|
4993
5150
|
this.zoomInBtn = this.container.querySelector("#zoom-in-btn");
|
|
4994
5151
|
this.zoomOutBtn = this.container.querySelector("#zoom-out-btn");
|
|
4995
5152
|
this.fitBtn = this.container.querySelector("#fit-btn");
|
|
@@ -5043,6 +5200,11 @@ class Toolbar {
|
|
|
5043
5200
|
this.updateAutoForwardButton();
|
|
5044
5201
|
this.dispatchAutoForwardEvent(this._autoForwardEnabled);
|
|
5045
5202
|
});
|
|
5203
|
+
this.compressedTimeBtn.addEventListener("click", () => {
|
|
5204
|
+
this._compressedTimeModeEnabled = !this._compressedTimeModeEnabled;
|
|
5205
|
+
this.updateCompressedTimeButton();
|
|
5206
|
+
this.dispatchCompressedTimeEvent(this._compressedTimeModeEnabled);
|
|
5207
|
+
});
|
|
5046
5208
|
// Display buttons
|
|
5047
5209
|
this.zoomInBtn.addEventListener("click", () => this.dispatchDisplayEvent("zoom-in"));
|
|
5048
5210
|
this.zoomOutBtn.addEventListener("click", () => this.dispatchDisplayEvent("zoom-out"));
|
|
@@ -5263,6 +5425,17 @@ class Toolbar {
|
|
|
5263
5425
|
this.autoForwardBtn.classList.remove("active");
|
|
5264
5426
|
}
|
|
5265
5427
|
}
|
|
5428
|
+
/**
|
|
5429
|
+
* Update compressed time mode button visual state.
|
|
5430
|
+
*/
|
|
5431
|
+
updateCompressedTimeButton() {
|
|
5432
|
+
if (this._compressedTimeModeEnabled) {
|
|
5433
|
+
this.compressedTimeBtn.classList.add("active");
|
|
5434
|
+
}
|
|
5435
|
+
else {
|
|
5436
|
+
this.compressedTimeBtn.classList.remove("active");
|
|
5437
|
+
}
|
|
5438
|
+
}
|
|
5266
5439
|
/**
|
|
5267
5440
|
* Apply disabled state to buttons from settings.
|
|
5268
5441
|
*/
|
|
@@ -5277,6 +5450,7 @@ class Toolbar {
|
|
|
5277
5450
|
"slideshow-start-btn": this.slideshowStartBtn,
|
|
5278
5451
|
"slideshow-stop-btn": this.slideshowStopBtn,
|
|
5279
5452
|
"slideshow-reverse-btn": this.slideshowReverseBtn,
|
|
5453
|
+
"compressed-time-btn": this.compressedTimeBtn,
|
|
5280
5454
|
"zoom-in-btn": this.zoomInBtn,
|
|
5281
5455
|
"zoom-out-btn": this.zoomOutBtn,
|
|
5282
5456
|
"fit-btn": this.fitBtn,
|
|
@@ -5316,6 +5490,15 @@ class Toolbar {
|
|
|
5316
5490
|
bubbles: true,
|
|
5317
5491
|
}));
|
|
5318
5492
|
}
|
|
5493
|
+
/**
|
|
5494
|
+
* Dispatch compressed time event for parent component to handle.
|
|
5495
|
+
*/
|
|
5496
|
+
dispatchCompressedTimeEvent(enabled) {
|
|
5497
|
+
this.container.dispatchEvent(new CustomEvent("toolbar-compressed-time", {
|
|
5498
|
+
detail: { enabled },
|
|
5499
|
+
bubbles: true,
|
|
5500
|
+
}));
|
|
5501
|
+
}
|
|
5319
5502
|
/**
|
|
5320
5503
|
* Dispatch display event for parent component to handle.
|
|
5321
5504
|
*/
|
|
@@ -5399,6 +5582,13 @@ class Toolbar {
|
|
|
5399
5582
|
this._autoForwardEnabled = enabled;
|
|
5400
5583
|
this.updateAutoForwardButton();
|
|
5401
5584
|
}
|
|
5585
|
+
/**
|
|
5586
|
+
* Set compressed time mode enabled state and update button appearance.
|
|
5587
|
+
*/
|
|
5588
|
+
setCompressedTimeModeEnabled(enabled) {
|
|
5589
|
+
this._compressedTimeModeEnabled = enabled;
|
|
5590
|
+
this.updateCompressedTimeButton();
|
|
5591
|
+
}
|
|
5402
5592
|
}
|
|
5403
5593
|
|
|
5404
5594
|
/**
|
|
@@ -25588,375 +25778,375 @@ class GoldenLayout extends VirtualLayout {
|
|
|
25588
25778
|
* Source: golden-layout@2.6.0 dist/css/goldenlayout-base.css + adapted light theme.
|
|
25589
25779
|
* Update this file when upgrading golden-layout.
|
|
25590
25780
|
*/
|
|
25591
|
-
const GOLDEN_LAYOUT_CSS = `
|
|
25592
|
-
/* ── Golden Layout base ──────────────────────────────────────────────── */
|
|
25593
|
-
.lm_root {
|
|
25594
|
-
position: relative;
|
|
25595
|
-
}
|
|
25596
|
-
.lm_row > .lm_item {
|
|
25597
|
-
float: left;
|
|
25598
|
-
}
|
|
25599
|
-
.lm_content {
|
|
25600
|
-
overflow: hidden;
|
|
25601
|
-
position: relative;
|
|
25602
|
-
}
|
|
25603
|
-
.lm_dragging,
|
|
25604
|
-
.lm_dragging * {
|
|
25605
|
-
cursor: move !important;
|
|
25606
|
-
user-select: none;
|
|
25607
|
-
}
|
|
25608
|
-
.lm_maximised {
|
|
25609
|
-
position: absolute;
|
|
25610
|
-
top: 0;
|
|
25611
|
-
left: 0;
|
|
25612
|
-
z-index: 40;
|
|
25613
|
-
}
|
|
25614
|
-
.lm_maximise_placeholder {
|
|
25615
|
-
display: none;
|
|
25616
|
-
}
|
|
25617
|
-
.lm_splitter {
|
|
25618
|
-
position: relative;
|
|
25619
|
-
z-index: 2;
|
|
25620
|
-
touch-action: none;
|
|
25621
|
-
}
|
|
25622
|
-
.lm_splitter.lm_vertical .lm_drag_handle {
|
|
25623
|
-
width: 100%;
|
|
25624
|
-
position: absolute;
|
|
25625
|
-
cursor: ns-resize;
|
|
25626
|
-
touch-action: none;
|
|
25627
|
-
user-select: none;
|
|
25628
|
-
}
|
|
25629
|
-
.lm_splitter.lm_horizontal {
|
|
25630
|
-
float: left;
|
|
25631
|
-
height: 100%;
|
|
25632
|
-
}
|
|
25633
|
-
.lm_splitter.lm_horizontal .lm_drag_handle {
|
|
25634
|
-
height: 100%;
|
|
25635
|
-
position: absolute;
|
|
25636
|
-
cursor: ew-resize;
|
|
25637
|
-
touch-action: none;
|
|
25638
|
-
user-select: none;
|
|
25639
|
-
}
|
|
25640
|
-
.lm_header {
|
|
25641
|
-
overflow: visible;
|
|
25642
|
-
position: relative;
|
|
25643
|
-
z-index: 1;
|
|
25644
|
-
user-select: none;
|
|
25645
|
-
}
|
|
25646
|
-
.lm_header [class^=lm_] {
|
|
25647
|
-
box-sizing: content-box !important;
|
|
25648
|
-
}
|
|
25649
|
-
.lm_header .lm_controls {
|
|
25650
|
-
position: absolute;
|
|
25651
|
-
right: 3px;
|
|
25652
|
-
display: flex;
|
|
25653
|
-
}
|
|
25654
|
-
.lm_header .lm_controls > * {
|
|
25655
|
-
cursor: pointer;
|
|
25656
|
-
float: left;
|
|
25657
|
-
width: 18px;
|
|
25658
|
-
height: 18px;
|
|
25659
|
-
text-align: center;
|
|
25660
|
-
}
|
|
25661
|
-
.lm_header .lm_tabs {
|
|
25662
|
-
position: absolute;
|
|
25663
|
-
display: flex;
|
|
25664
|
-
}
|
|
25665
|
-
.lm_header .lm_tab {
|
|
25666
|
-
cursor: pointer;
|
|
25667
|
-
float: left;
|
|
25668
|
-
height: 14px;
|
|
25669
|
-
margin-top: 1px;
|
|
25670
|
-
padding: 0px 10px 5px;
|
|
25671
|
-
padding-right: 25px;
|
|
25672
|
-
position: relative;
|
|
25673
|
-
touch-action: none;
|
|
25674
|
-
}
|
|
25675
|
-
.lm_header .lm_tab i {
|
|
25676
|
-
width: 2px;
|
|
25677
|
-
height: 19px;
|
|
25678
|
-
position: absolute;
|
|
25679
|
-
}
|
|
25680
|
-
.lm_header .lm_tab i.lm_left {
|
|
25681
|
-
top: 0;
|
|
25682
|
-
left: -2px;
|
|
25683
|
-
}
|
|
25684
|
-
.lm_header .lm_tab i.lm_right {
|
|
25685
|
-
top: 0;
|
|
25686
|
-
right: -2px;
|
|
25687
|
-
}
|
|
25688
|
-
.lm_header .lm_tab .lm_title {
|
|
25689
|
-
display: inline-block;
|
|
25690
|
-
overflow: hidden;
|
|
25691
|
-
text-overflow: ellipsis;
|
|
25692
|
-
}
|
|
25693
|
-
.lm_header .lm_tab .lm_close_tab {
|
|
25694
|
-
width: 14px;
|
|
25695
|
-
height: 14px;
|
|
25696
|
-
position: absolute;
|
|
25697
|
-
top: 0;
|
|
25698
|
-
right: 0;
|
|
25699
|
-
text-align: center;
|
|
25700
|
-
}
|
|
25701
|
-
.lm_stack {
|
|
25702
|
-
position: relative;
|
|
25703
|
-
}
|
|
25704
|
-
.lm_stack > .lm_items {
|
|
25705
|
-
overflow: hidden;
|
|
25706
|
-
}
|
|
25707
|
-
.lm_stack.lm_left > .lm_items {
|
|
25708
|
-
position: absolute;
|
|
25709
|
-
left: 20px;
|
|
25710
|
-
top: 0;
|
|
25711
|
-
}
|
|
25712
|
-
.lm_stack.lm_right > .lm_items {
|
|
25713
|
-
position: absolute;
|
|
25714
|
-
right: 20px;
|
|
25715
|
-
top: 0;
|
|
25716
|
-
}
|
|
25717
|
-
.lm_stack.lm_right > .lm_header {
|
|
25718
|
-
position: absolute;
|
|
25719
|
-
right: 0;
|
|
25720
|
-
top: 0;
|
|
25721
|
-
}
|
|
25722
|
-
.lm_stack.lm_bottom > .lm_items {
|
|
25723
|
-
position: absolute;
|
|
25724
|
-
bottom: 20px;
|
|
25725
|
-
}
|
|
25726
|
-
.lm_stack.lm_bottom > .lm_header {
|
|
25727
|
-
position: absolute;
|
|
25728
|
-
bottom: 0;
|
|
25729
|
-
}
|
|
25730
|
-
.lm_left.lm_stack .lm_header,
|
|
25731
|
-
.lm_right.lm_stack .lm_header {
|
|
25732
|
-
height: 100%;
|
|
25733
|
-
}
|
|
25734
|
-
.lm_left.lm_dragProxy .lm_header,
|
|
25735
|
-
.lm_right.lm_dragProxy .lm_header,
|
|
25736
|
-
.lm_left.lm_dragProxy .lm_items,
|
|
25737
|
-
.lm_right.lm_dragProxy .lm_items {
|
|
25738
|
-
float: left;
|
|
25739
|
-
}
|
|
25740
|
-
.lm_left.lm_dragProxy .lm_header,
|
|
25741
|
-
.lm_right.lm_dragProxy .lm_header,
|
|
25742
|
-
.lm_left.lm_stack .lm_header,
|
|
25743
|
-
.lm_right.lm_stack .lm_header {
|
|
25744
|
-
width: 20px;
|
|
25745
|
-
vertical-align: top;
|
|
25746
|
-
}
|
|
25747
|
-
.lm_left.lm_dragProxy .lm_header .lm_tabs,
|
|
25748
|
-
.lm_right.lm_dragProxy .lm_header .lm_tabs,
|
|
25749
|
-
.lm_left.lm_stack .lm_header .lm_tabs,
|
|
25750
|
-
.lm_right.lm_stack .lm_header .lm_tabs {
|
|
25751
|
-
transform-origin: left top;
|
|
25752
|
-
top: 0;
|
|
25753
|
-
width: 1000px;
|
|
25754
|
-
}
|
|
25755
|
-
.lm_left.lm_dragProxy .lm_header .lm_controls,
|
|
25756
|
-
.lm_right.lm_dragProxy .lm_header .lm_controls,
|
|
25757
|
-
.lm_left.lm_stack .lm_header .lm_controls,
|
|
25758
|
-
.lm_right.lm_stack .lm_header .lm_controls {
|
|
25759
|
-
bottom: 0;
|
|
25760
|
-
flex-flow: column;
|
|
25761
|
-
}
|
|
25762
|
-
.lm_dragProxy.lm_left .lm_header .lm_tabs,
|
|
25763
|
-
.lm_stack.lm_left .lm_header .lm_tabs {
|
|
25764
|
-
transform: rotate(-90deg) scaleX(-1);
|
|
25765
|
-
left: 0;
|
|
25766
|
-
}
|
|
25767
|
-
.lm_dragProxy.lm_left .lm_header .lm_tabs .lm_tab,
|
|
25768
|
-
.lm_stack.lm_left .lm_header .lm_tabs .lm_tab {
|
|
25769
|
-
transform: scaleX(-1);
|
|
25770
|
-
margin-top: 1px;
|
|
25771
|
-
}
|
|
25772
|
-
.lm_dragProxy.lm_right .lm_content {
|
|
25773
|
-
float: left;
|
|
25774
|
-
}
|
|
25775
|
-
.lm_dragProxy.lm_right .lm_header .lm_tabs,
|
|
25776
|
-
.lm_stack.lm_right .lm_header .lm_tabs {
|
|
25777
|
-
transform: rotate(90deg) scaleX(1);
|
|
25778
|
-
left: 100%;
|
|
25779
|
-
margin-left: 0;
|
|
25780
|
-
}
|
|
25781
|
-
.lm_dragProxy.lm_right .lm_header .lm_controls,
|
|
25782
|
-
.lm_stack.lm_right .lm_header .lm_controls {
|
|
25783
|
-
left: 3px;
|
|
25784
|
-
}
|
|
25785
|
-
.lm_dragProxy.lm_bottom .lm_header,
|
|
25786
|
-
.lm_stack.lm_bottom .lm_header {
|
|
25787
|
-
width: 100%;
|
|
25788
|
-
}
|
|
25789
|
-
.lm_dragProxy.lm_bottom .lm_header .lm_tab,
|
|
25790
|
-
.lm_stack.lm_bottom .lm_header .lm_tab {
|
|
25791
|
-
margin-top: 0;
|
|
25792
|
-
border-top: none;
|
|
25793
|
-
}
|
|
25794
|
-
.lm_dragProxy.lm_bottom .lm_header .lm_controls,
|
|
25795
|
-
.lm_stack.lm_bottom .lm_header .lm_controls {
|
|
25796
|
-
top: 3px;
|
|
25797
|
-
}
|
|
25798
|
-
.lm_drop_tab_placeholder {
|
|
25799
|
-
float: left;
|
|
25800
|
-
width: 100px;
|
|
25801
|
-
visibility: hidden;
|
|
25802
|
-
}
|
|
25803
|
-
.lm_header .lm_controls .lm_tabdropdown:before {
|
|
25804
|
-
content: '';
|
|
25805
|
-
width: 0;
|
|
25806
|
-
height: 0;
|
|
25807
|
-
vertical-align: middle;
|
|
25808
|
-
display: inline-block;
|
|
25809
|
-
border-top: 5px dashed;
|
|
25810
|
-
border-right: 5px solid transparent;
|
|
25811
|
-
border-left: 5px solid transparent;
|
|
25812
|
-
color: white;
|
|
25813
|
-
}
|
|
25814
|
-
.lm_header .lm_tabdropdown_list {
|
|
25815
|
-
position: absolute;
|
|
25816
|
-
top: 20px;
|
|
25817
|
-
right: 0;
|
|
25818
|
-
z-index: 5;
|
|
25819
|
-
overflow: hidden;
|
|
25820
|
-
}
|
|
25821
|
-
.lm_header .lm_tabdropdown_list .lm_tab {
|
|
25822
|
-
clear: both;
|
|
25823
|
-
padding-right: 10px;
|
|
25824
|
-
margin: 0;
|
|
25825
|
-
}
|
|
25826
|
-
.lm_header .lm_tabdropdown_list .lm_tab .lm_title {
|
|
25827
|
-
width: 100px;
|
|
25828
|
-
}
|
|
25829
|
-
.lm_header .lm_tabdropdown_list .lm_close_tab {
|
|
25830
|
-
display: none !important;
|
|
25831
|
-
}
|
|
25832
|
-
.lm_dragProxy {
|
|
25833
|
-
position: absolute;
|
|
25834
|
-
top: 0;
|
|
25835
|
-
left: 0;
|
|
25836
|
-
z-index: 30;
|
|
25837
|
-
}
|
|
25838
|
-
.lm_dragProxy .lm_header {
|
|
25839
|
-
background: transparent;
|
|
25840
|
-
}
|
|
25841
|
-
.lm_dragProxy .lm_content {
|
|
25842
|
-
border-top: none;
|
|
25843
|
-
overflow: hidden;
|
|
25844
|
-
}
|
|
25845
|
-
.lm_dropTargetIndicator {
|
|
25846
|
-
display: none;
|
|
25847
|
-
position: absolute;
|
|
25848
|
-
z-index: 35;
|
|
25849
|
-
transition: all 200ms ease;
|
|
25850
|
-
}
|
|
25851
|
-
.lm_dropTargetIndicator .lm_inner {
|
|
25852
|
-
width: 100%;
|
|
25853
|
-
height: 100%;
|
|
25854
|
-
position: relative;
|
|
25855
|
-
top: 0;
|
|
25856
|
-
left: 0;
|
|
25857
|
-
}
|
|
25858
|
-
.lm_transition_indicator {
|
|
25859
|
-
display: none;
|
|
25860
|
-
width: 20px;
|
|
25861
|
-
height: 20px;
|
|
25862
|
-
position: absolute;
|
|
25863
|
-
top: 0;
|
|
25864
|
-
left: 0;
|
|
25865
|
-
z-index: 20;
|
|
25866
|
-
}
|
|
25867
|
-
.lm_popin {
|
|
25868
|
-
width: 20px;
|
|
25869
|
-
height: 20px;
|
|
25870
|
-
position: absolute;
|
|
25871
|
-
bottom: 0;
|
|
25872
|
-
right: 0;
|
|
25873
|
-
z-index: 9999;
|
|
25874
|
-
}
|
|
25875
|
-
.lm_popin > * {
|
|
25876
|
-
width: 100%;
|
|
25877
|
-
height: 100%;
|
|
25878
|
-
position: absolute;
|
|
25879
|
-
top: 0;
|
|
25880
|
-
left: 0;
|
|
25881
|
-
}
|
|
25882
|
-
.lm_popin > .lm_bg {
|
|
25883
|
-
z-index: 10;
|
|
25884
|
-
}
|
|
25885
|
-
.lm_popin > .lm_icon {
|
|
25886
|
-
z-index: 20;
|
|
25887
|
-
}
|
|
25888
|
-
|
|
25889
|
-
/* ── Adapted theme (using --gve- CSS variables) ──────────────────────── */
|
|
25890
|
-
.lm_goldenlayout {
|
|
25891
|
-
background: transparent;
|
|
25892
|
-
}
|
|
25893
|
-
.lm_content {
|
|
25894
|
-
background: var(--gve-bg-color, #fafafa);
|
|
25895
|
-
border: none;
|
|
25896
|
-
}
|
|
25897
|
-
.lm_dragProxy .lm_content {
|
|
25898
|
-
box-shadow: 2px 2px 4px rgba(0,0,0,0.2);
|
|
25899
|
-
}
|
|
25900
|
-
.lm_dropTargetIndicator {
|
|
25901
|
-
box-shadow: inset 0 0 30px rgba(0,0,0,0.3);
|
|
25902
|
-
outline: 1px dashed var(--gve-border-color, #ccc);
|
|
25903
|
-
}
|
|
25904
|
-
.lm_dropTargetIndicator .lm_inner {
|
|
25905
|
-
background: #000;
|
|
25906
|
-
opacity: 0.08;
|
|
25907
|
-
}
|
|
25908
|
-
.lm_splitter {
|
|
25909
|
-
background: var(--gve-border-color, #ccc);
|
|
25910
|
-
opacity: 0.4;
|
|
25911
|
-
transition: opacity 200ms ease;
|
|
25912
|
-
}
|
|
25913
|
-
.lm_splitter:hover,
|
|
25914
|
-
.lm_splitter.lm_dragging {
|
|
25915
|
-
background: var(--gve-border-color, #aaa);
|
|
25916
|
-
opacity: 1;
|
|
25917
|
-
}
|
|
25918
|
-
.lm_header {
|
|
25919
|
-
height: 26px;
|
|
25920
|
-
background: var(--gve-toolbar-bg, #f5f5f5);
|
|
25921
|
-
border-bottom: 1px solid var(--gve-border-color, #ddd);
|
|
25922
|
-
}
|
|
25923
|
-
.lm_header .lm_tab {
|
|
25924
|
-
font-family: Arial, sans-serif;
|
|
25925
|
-
font-size: 12px;
|
|
25926
|
-
font-weight: 600;
|
|
25927
|
-
color: var(--gve-text-secondary, #777);
|
|
25928
|
-
background: transparent;
|
|
25929
|
-
margin-right: 2px;
|
|
25930
|
-
padding: 5px 10px 4px;
|
|
25931
|
-
padding-right: 14px;
|
|
25932
|
-
border: none;
|
|
25933
|
-
}
|
|
25934
|
-
.lm_header .lm_tab .lm_title {
|
|
25935
|
-
padding-top: 1px;
|
|
25936
|
-
}
|
|
25937
|
-
.lm_header .lm_tab.lm_active {
|
|
25938
|
-
background: var(--gve-bg-color, #fff);
|
|
25939
|
-
color: var(--gve-text-color, #333);
|
|
25940
|
-
border-bottom: 2px solid var(--gve-primary-color, #007bff);
|
|
25941
|
-
padding-bottom: 3px;
|
|
25942
|
-
}
|
|
25943
|
-
.lm_header .lm_tab:hover {
|
|
25944
|
-
color: var(--gve-text-color, #333);
|
|
25945
|
-
}
|
|
25946
|
-
.lm_header .lm_controls .lm_tabdropdown:before {
|
|
25947
|
-
color: var(--gve-text-color, #333);
|
|
25948
|
-
}
|
|
25949
|
-
.lm_transition_indicator {
|
|
25950
|
-
background: #000;
|
|
25951
|
-
border: 1px dashed #555;
|
|
25952
|
-
}
|
|
25953
|
-
.lm_popin {
|
|
25954
|
-
cursor: pointer;
|
|
25955
|
-
}
|
|
25956
|
-
.lm_popin .lm_bg {
|
|
25957
|
-
background: #000;
|
|
25958
|
-
opacity: 0.7;
|
|
25959
|
-
}
|
|
25781
|
+
const GOLDEN_LAYOUT_CSS = `
|
|
25782
|
+
/* ── Golden Layout base ──────────────────────────────────────────────── */
|
|
25783
|
+
.lm_root {
|
|
25784
|
+
position: relative;
|
|
25785
|
+
}
|
|
25786
|
+
.lm_row > .lm_item {
|
|
25787
|
+
float: left;
|
|
25788
|
+
}
|
|
25789
|
+
.lm_content {
|
|
25790
|
+
overflow: hidden;
|
|
25791
|
+
position: relative;
|
|
25792
|
+
}
|
|
25793
|
+
.lm_dragging,
|
|
25794
|
+
.lm_dragging * {
|
|
25795
|
+
cursor: move !important;
|
|
25796
|
+
user-select: none;
|
|
25797
|
+
}
|
|
25798
|
+
.lm_maximised {
|
|
25799
|
+
position: absolute;
|
|
25800
|
+
top: 0;
|
|
25801
|
+
left: 0;
|
|
25802
|
+
z-index: 40;
|
|
25803
|
+
}
|
|
25804
|
+
.lm_maximise_placeholder {
|
|
25805
|
+
display: none;
|
|
25806
|
+
}
|
|
25807
|
+
.lm_splitter {
|
|
25808
|
+
position: relative;
|
|
25809
|
+
z-index: 2;
|
|
25810
|
+
touch-action: none;
|
|
25811
|
+
}
|
|
25812
|
+
.lm_splitter.lm_vertical .lm_drag_handle {
|
|
25813
|
+
width: 100%;
|
|
25814
|
+
position: absolute;
|
|
25815
|
+
cursor: ns-resize;
|
|
25816
|
+
touch-action: none;
|
|
25817
|
+
user-select: none;
|
|
25818
|
+
}
|
|
25819
|
+
.lm_splitter.lm_horizontal {
|
|
25820
|
+
float: left;
|
|
25821
|
+
height: 100%;
|
|
25822
|
+
}
|
|
25823
|
+
.lm_splitter.lm_horizontal .lm_drag_handle {
|
|
25824
|
+
height: 100%;
|
|
25825
|
+
position: absolute;
|
|
25826
|
+
cursor: ew-resize;
|
|
25827
|
+
touch-action: none;
|
|
25828
|
+
user-select: none;
|
|
25829
|
+
}
|
|
25830
|
+
.lm_header {
|
|
25831
|
+
overflow: visible;
|
|
25832
|
+
position: relative;
|
|
25833
|
+
z-index: 1;
|
|
25834
|
+
user-select: none;
|
|
25835
|
+
}
|
|
25836
|
+
.lm_header [class^=lm_] {
|
|
25837
|
+
box-sizing: content-box !important;
|
|
25838
|
+
}
|
|
25839
|
+
.lm_header .lm_controls {
|
|
25840
|
+
position: absolute;
|
|
25841
|
+
right: 3px;
|
|
25842
|
+
display: flex;
|
|
25843
|
+
}
|
|
25844
|
+
.lm_header .lm_controls > * {
|
|
25845
|
+
cursor: pointer;
|
|
25846
|
+
float: left;
|
|
25847
|
+
width: 18px;
|
|
25848
|
+
height: 18px;
|
|
25849
|
+
text-align: center;
|
|
25850
|
+
}
|
|
25851
|
+
.lm_header .lm_tabs {
|
|
25852
|
+
position: absolute;
|
|
25853
|
+
display: flex;
|
|
25854
|
+
}
|
|
25855
|
+
.lm_header .lm_tab {
|
|
25856
|
+
cursor: pointer;
|
|
25857
|
+
float: left;
|
|
25858
|
+
height: 14px;
|
|
25859
|
+
margin-top: 1px;
|
|
25860
|
+
padding: 0px 10px 5px;
|
|
25861
|
+
padding-right: 25px;
|
|
25862
|
+
position: relative;
|
|
25863
|
+
touch-action: none;
|
|
25864
|
+
}
|
|
25865
|
+
.lm_header .lm_tab i {
|
|
25866
|
+
width: 2px;
|
|
25867
|
+
height: 19px;
|
|
25868
|
+
position: absolute;
|
|
25869
|
+
}
|
|
25870
|
+
.lm_header .lm_tab i.lm_left {
|
|
25871
|
+
top: 0;
|
|
25872
|
+
left: -2px;
|
|
25873
|
+
}
|
|
25874
|
+
.lm_header .lm_tab i.lm_right {
|
|
25875
|
+
top: 0;
|
|
25876
|
+
right: -2px;
|
|
25877
|
+
}
|
|
25878
|
+
.lm_header .lm_tab .lm_title {
|
|
25879
|
+
display: inline-block;
|
|
25880
|
+
overflow: hidden;
|
|
25881
|
+
text-overflow: ellipsis;
|
|
25882
|
+
}
|
|
25883
|
+
.lm_header .lm_tab .lm_close_tab {
|
|
25884
|
+
width: 14px;
|
|
25885
|
+
height: 14px;
|
|
25886
|
+
position: absolute;
|
|
25887
|
+
top: 0;
|
|
25888
|
+
right: 0;
|
|
25889
|
+
text-align: center;
|
|
25890
|
+
}
|
|
25891
|
+
.lm_stack {
|
|
25892
|
+
position: relative;
|
|
25893
|
+
}
|
|
25894
|
+
.lm_stack > .lm_items {
|
|
25895
|
+
overflow: hidden;
|
|
25896
|
+
}
|
|
25897
|
+
.lm_stack.lm_left > .lm_items {
|
|
25898
|
+
position: absolute;
|
|
25899
|
+
left: 20px;
|
|
25900
|
+
top: 0;
|
|
25901
|
+
}
|
|
25902
|
+
.lm_stack.lm_right > .lm_items {
|
|
25903
|
+
position: absolute;
|
|
25904
|
+
right: 20px;
|
|
25905
|
+
top: 0;
|
|
25906
|
+
}
|
|
25907
|
+
.lm_stack.lm_right > .lm_header {
|
|
25908
|
+
position: absolute;
|
|
25909
|
+
right: 0;
|
|
25910
|
+
top: 0;
|
|
25911
|
+
}
|
|
25912
|
+
.lm_stack.lm_bottom > .lm_items {
|
|
25913
|
+
position: absolute;
|
|
25914
|
+
bottom: 20px;
|
|
25915
|
+
}
|
|
25916
|
+
.lm_stack.lm_bottom > .lm_header {
|
|
25917
|
+
position: absolute;
|
|
25918
|
+
bottom: 0;
|
|
25919
|
+
}
|
|
25920
|
+
.lm_left.lm_stack .lm_header,
|
|
25921
|
+
.lm_right.lm_stack .lm_header {
|
|
25922
|
+
height: 100%;
|
|
25923
|
+
}
|
|
25924
|
+
.lm_left.lm_dragProxy .lm_header,
|
|
25925
|
+
.lm_right.lm_dragProxy .lm_header,
|
|
25926
|
+
.lm_left.lm_dragProxy .lm_items,
|
|
25927
|
+
.lm_right.lm_dragProxy .lm_items {
|
|
25928
|
+
float: left;
|
|
25929
|
+
}
|
|
25930
|
+
.lm_left.lm_dragProxy .lm_header,
|
|
25931
|
+
.lm_right.lm_dragProxy .lm_header,
|
|
25932
|
+
.lm_left.lm_stack .lm_header,
|
|
25933
|
+
.lm_right.lm_stack .lm_header {
|
|
25934
|
+
width: 20px;
|
|
25935
|
+
vertical-align: top;
|
|
25936
|
+
}
|
|
25937
|
+
.lm_left.lm_dragProxy .lm_header .lm_tabs,
|
|
25938
|
+
.lm_right.lm_dragProxy .lm_header .lm_tabs,
|
|
25939
|
+
.lm_left.lm_stack .lm_header .lm_tabs,
|
|
25940
|
+
.lm_right.lm_stack .lm_header .lm_tabs {
|
|
25941
|
+
transform-origin: left top;
|
|
25942
|
+
top: 0;
|
|
25943
|
+
width: 1000px;
|
|
25944
|
+
}
|
|
25945
|
+
.lm_left.lm_dragProxy .lm_header .lm_controls,
|
|
25946
|
+
.lm_right.lm_dragProxy .lm_header .lm_controls,
|
|
25947
|
+
.lm_left.lm_stack .lm_header .lm_controls,
|
|
25948
|
+
.lm_right.lm_stack .lm_header .lm_controls {
|
|
25949
|
+
bottom: 0;
|
|
25950
|
+
flex-flow: column;
|
|
25951
|
+
}
|
|
25952
|
+
.lm_dragProxy.lm_left .lm_header .lm_tabs,
|
|
25953
|
+
.lm_stack.lm_left .lm_header .lm_tabs {
|
|
25954
|
+
transform: rotate(-90deg) scaleX(-1);
|
|
25955
|
+
left: 0;
|
|
25956
|
+
}
|
|
25957
|
+
.lm_dragProxy.lm_left .lm_header .lm_tabs .lm_tab,
|
|
25958
|
+
.lm_stack.lm_left .lm_header .lm_tabs .lm_tab {
|
|
25959
|
+
transform: scaleX(-1);
|
|
25960
|
+
margin-top: 1px;
|
|
25961
|
+
}
|
|
25962
|
+
.lm_dragProxy.lm_right .lm_content {
|
|
25963
|
+
float: left;
|
|
25964
|
+
}
|
|
25965
|
+
.lm_dragProxy.lm_right .lm_header .lm_tabs,
|
|
25966
|
+
.lm_stack.lm_right .lm_header .lm_tabs {
|
|
25967
|
+
transform: rotate(90deg) scaleX(1);
|
|
25968
|
+
left: 100%;
|
|
25969
|
+
margin-left: 0;
|
|
25970
|
+
}
|
|
25971
|
+
.lm_dragProxy.lm_right .lm_header .lm_controls,
|
|
25972
|
+
.lm_stack.lm_right .lm_header .lm_controls {
|
|
25973
|
+
left: 3px;
|
|
25974
|
+
}
|
|
25975
|
+
.lm_dragProxy.lm_bottom .lm_header,
|
|
25976
|
+
.lm_stack.lm_bottom .lm_header {
|
|
25977
|
+
width: 100%;
|
|
25978
|
+
}
|
|
25979
|
+
.lm_dragProxy.lm_bottom .lm_header .lm_tab,
|
|
25980
|
+
.lm_stack.lm_bottom .lm_header .lm_tab {
|
|
25981
|
+
margin-top: 0;
|
|
25982
|
+
border-top: none;
|
|
25983
|
+
}
|
|
25984
|
+
.lm_dragProxy.lm_bottom .lm_header .lm_controls,
|
|
25985
|
+
.lm_stack.lm_bottom .lm_header .lm_controls {
|
|
25986
|
+
top: 3px;
|
|
25987
|
+
}
|
|
25988
|
+
.lm_drop_tab_placeholder {
|
|
25989
|
+
float: left;
|
|
25990
|
+
width: 100px;
|
|
25991
|
+
visibility: hidden;
|
|
25992
|
+
}
|
|
25993
|
+
.lm_header .lm_controls .lm_tabdropdown:before {
|
|
25994
|
+
content: '';
|
|
25995
|
+
width: 0;
|
|
25996
|
+
height: 0;
|
|
25997
|
+
vertical-align: middle;
|
|
25998
|
+
display: inline-block;
|
|
25999
|
+
border-top: 5px dashed;
|
|
26000
|
+
border-right: 5px solid transparent;
|
|
26001
|
+
border-left: 5px solid transparent;
|
|
26002
|
+
color: white;
|
|
26003
|
+
}
|
|
26004
|
+
.lm_header .lm_tabdropdown_list {
|
|
26005
|
+
position: absolute;
|
|
26006
|
+
top: 20px;
|
|
26007
|
+
right: 0;
|
|
26008
|
+
z-index: 5;
|
|
26009
|
+
overflow: hidden;
|
|
26010
|
+
}
|
|
26011
|
+
.lm_header .lm_tabdropdown_list .lm_tab {
|
|
26012
|
+
clear: both;
|
|
26013
|
+
padding-right: 10px;
|
|
26014
|
+
margin: 0;
|
|
26015
|
+
}
|
|
26016
|
+
.lm_header .lm_tabdropdown_list .lm_tab .lm_title {
|
|
26017
|
+
width: 100px;
|
|
26018
|
+
}
|
|
26019
|
+
.lm_header .lm_tabdropdown_list .lm_close_tab {
|
|
26020
|
+
display: none !important;
|
|
26021
|
+
}
|
|
26022
|
+
.lm_dragProxy {
|
|
26023
|
+
position: absolute;
|
|
26024
|
+
top: 0;
|
|
26025
|
+
left: 0;
|
|
26026
|
+
z-index: 30;
|
|
26027
|
+
}
|
|
26028
|
+
.lm_dragProxy .lm_header {
|
|
26029
|
+
background: transparent;
|
|
26030
|
+
}
|
|
26031
|
+
.lm_dragProxy .lm_content {
|
|
26032
|
+
border-top: none;
|
|
26033
|
+
overflow: hidden;
|
|
26034
|
+
}
|
|
26035
|
+
.lm_dropTargetIndicator {
|
|
26036
|
+
display: none;
|
|
26037
|
+
position: absolute;
|
|
26038
|
+
z-index: 35;
|
|
26039
|
+
transition: all 200ms ease;
|
|
26040
|
+
}
|
|
26041
|
+
.lm_dropTargetIndicator .lm_inner {
|
|
26042
|
+
width: 100%;
|
|
26043
|
+
height: 100%;
|
|
26044
|
+
position: relative;
|
|
26045
|
+
top: 0;
|
|
26046
|
+
left: 0;
|
|
26047
|
+
}
|
|
26048
|
+
.lm_transition_indicator {
|
|
26049
|
+
display: none;
|
|
26050
|
+
width: 20px;
|
|
26051
|
+
height: 20px;
|
|
26052
|
+
position: absolute;
|
|
26053
|
+
top: 0;
|
|
26054
|
+
left: 0;
|
|
26055
|
+
z-index: 20;
|
|
26056
|
+
}
|
|
26057
|
+
.lm_popin {
|
|
26058
|
+
width: 20px;
|
|
26059
|
+
height: 20px;
|
|
26060
|
+
position: absolute;
|
|
26061
|
+
bottom: 0;
|
|
26062
|
+
right: 0;
|
|
26063
|
+
z-index: 9999;
|
|
26064
|
+
}
|
|
26065
|
+
.lm_popin > * {
|
|
26066
|
+
width: 100%;
|
|
26067
|
+
height: 100%;
|
|
26068
|
+
position: absolute;
|
|
26069
|
+
top: 0;
|
|
26070
|
+
left: 0;
|
|
26071
|
+
}
|
|
26072
|
+
.lm_popin > .lm_bg {
|
|
26073
|
+
z-index: 10;
|
|
26074
|
+
}
|
|
26075
|
+
.lm_popin > .lm_icon {
|
|
26076
|
+
z-index: 20;
|
|
26077
|
+
}
|
|
26078
|
+
|
|
26079
|
+
/* ── Adapted theme (using --gve- CSS variables) ──────────────────────── */
|
|
26080
|
+
.lm_goldenlayout {
|
|
26081
|
+
background: transparent;
|
|
26082
|
+
}
|
|
26083
|
+
.lm_content {
|
|
26084
|
+
background: var(--gve-bg-color, #fafafa);
|
|
26085
|
+
border: none;
|
|
26086
|
+
}
|
|
26087
|
+
.lm_dragProxy .lm_content {
|
|
26088
|
+
box-shadow: 2px 2px 4px rgba(0,0,0,0.2);
|
|
26089
|
+
}
|
|
26090
|
+
.lm_dropTargetIndicator {
|
|
26091
|
+
box-shadow: inset 0 0 30px rgba(0,0,0,0.3);
|
|
26092
|
+
outline: 1px dashed var(--gve-border-color, #ccc);
|
|
26093
|
+
}
|
|
26094
|
+
.lm_dropTargetIndicator .lm_inner {
|
|
26095
|
+
background: #000;
|
|
26096
|
+
opacity: 0.08;
|
|
26097
|
+
}
|
|
26098
|
+
.lm_splitter {
|
|
26099
|
+
background: var(--gve-border-color, #ccc);
|
|
26100
|
+
opacity: 0.4;
|
|
26101
|
+
transition: opacity 200ms ease;
|
|
26102
|
+
}
|
|
26103
|
+
.lm_splitter:hover,
|
|
26104
|
+
.lm_splitter.lm_dragging {
|
|
26105
|
+
background: var(--gve-border-color, #aaa);
|
|
26106
|
+
opacity: 1;
|
|
26107
|
+
}
|
|
26108
|
+
.lm_header {
|
|
26109
|
+
height: 26px;
|
|
26110
|
+
background: var(--gve-toolbar-bg, #f5f5f5);
|
|
26111
|
+
border-bottom: 1px solid var(--gve-border-color, #ddd);
|
|
26112
|
+
}
|
|
26113
|
+
.lm_header .lm_tab {
|
|
26114
|
+
font-family: Arial, sans-serif;
|
|
26115
|
+
font-size: 12px;
|
|
26116
|
+
font-weight: 600;
|
|
26117
|
+
color: var(--gve-text-secondary, #777);
|
|
26118
|
+
background: transparent;
|
|
26119
|
+
margin-right: 2px;
|
|
26120
|
+
padding: 5px 10px 4px;
|
|
26121
|
+
padding-right: 14px;
|
|
26122
|
+
border: none;
|
|
26123
|
+
}
|
|
26124
|
+
.lm_header .lm_tab .lm_title {
|
|
26125
|
+
padding-top: 1px;
|
|
26126
|
+
}
|
|
26127
|
+
.lm_header .lm_tab.lm_active {
|
|
26128
|
+
background: var(--gve-bg-color, #fff);
|
|
26129
|
+
color: var(--gve-text-color, #333);
|
|
26130
|
+
border-bottom: 2px solid var(--gve-primary-color, #007bff);
|
|
26131
|
+
padding-bottom: 3px;
|
|
26132
|
+
}
|
|
26133
|
+
.lm_header .lm_tab:hover {
|
|
26134
|
+
color: var(--gve-text-color, #333);
|
|
26135
|
+
}
|
|
26136
|
+
.lm_header .lm_controls .lm_tabdropdown:before {
|
|
26137
|
+
color: var(--gve-text-color, #333);
|
|
26138
|
+
}
|
|
26139
|
+
.lm_transition_indicator {
|
|
26140
|
+
background: #000;
|
|
26141
|
+
border: 1px dashed #555;
|
|
26142
|
+
}
|
|
26143
|
+
.lm_popin {
|
|
26144
|
+
cursor: pointer;
|
|
26145
|
+
}
|
|
26146
|
+
.lm_popin .lm_bg {
|
|
26147
|
+
background: #000;
|
|
26148
|
+
opacity: 0.7;
|
|
26149
|
+
}
|
|
25960
26150
|
`;
|
|
25961
26151
|
|
|
25962
26152
|
/**
|
|
@@ -25971,7 +26161,7 @@ class GveSnapshotRendition extends HTMLElement {
|
|
|
25971
26161
|
* of the web component is loaded.
|
|
25972
26162
|
*/
|
|
25973
26163
|
static get version() {
|
|
25974
|
-
return "2.0.
|
|
26164
|
+
return "2.0.11";
|
|
25975
26165
|
}
|
|
25976
26166
|
constructor() {
|
|
25977
26167
|
super();
|
|
@@ -25983,6 +26173,12 @@ class GveSnapshotRendition extends HTMLElement {
|
|
|
25983
26173
|
this._currentVersionIndex = 0;
|
|
25984
26174
|
this._autoForwardEnabled = false;
|
|
25985
26175
|
this._autoForwardTimerId = null;
|
|
26176
|
+
// When true, version navigation skips hint/text entrance animations,
|
|
26177
|
+
// prolog panning animation, and the backward fade-out, applying each
|
|
26178
|
+
// version's final state immediately. Lets users jump to any version
|
|
26179
|
+
// without waiting for the (potentially many) animations/delays that
|
|
26180
|
+
// would otherwise play for every operation in between.
|
|
26181
|
+
this._compressedTimeMode = false;
|
|
25986
26182
|
this._renderScheduled = false;
|
|
25987
26183
|
// Guards against re-entrant navigation: a slideshow tick (or a rapid
|
|
25988
26184
|
// double-click/keypress) firing while a previous goToVersionIndex() call
|
|
@@ -26148,6 +26344,7 @@ class GveSnapshotRendition extends HTMLElement {
|
|
|
26148
26344
|
this._toolbar.setCustomButtons(this._settings.customButtons || []);
|
|
26149
26345
|
this._toolbar.setDisabledButtons(this._settings.disabledButtonIds || []);
|
|
26150
26346
|
this._toolbar.setAutoForwardEnabled(this._autoForwardEnabled);
|
|
26347
|
+
this._toolbar.setCompressedTimeModeEnabled(this._compressedTimeMode);
|
|
26151
26348
|
// Listen for toolbar navigation events
|
|
26152
26349
|
toolbarContainer.addEventListener("toolbar-navigate", (e) => {
|
|
26153
26350
|
const customEvent = e;
|
|
@@ -26163,6 +26360,11 @@ class GveSnapshotRendition extends HTMLElement {
|
|
|
26163
26360
|
const customEvent = e;
|
|
26164
26361
|
this.setAutoForwardEnabled(customEvent.detail.enabled);
|
|
26165
26362
|
});
|
|
26363
|
+
// Listen for toolbar compressed-time-mode events
|
|
26364
|
+
toolbarContainer.addEventListener("toolbar-compressed-time", (e) => {
|
|
26365
|
+
const customEvent = e;
|
|
26366
|
+
this.setCompressedTimeModeEnabled(customEvent.detail.enabled);
|
|
26367
|
+
});
|
|
26166
26368
|
// Listen for toolbar display events
|
|
26167
26369
|
toolbarContainer.addEventListener("toolbar-display", (e) => {
|
|
26168
26370
|
const customEvent = e;
|
|
@@ -26297,6 +26499,20 @@ class GveSnapshotRendition extends HTMLElement {
|
|
|
26297
26499
|
* The SVG rendition panel has no header and acts as the fixed background.
|
|
26298
26500
|
*/
|
|
26299
26501
|
initializeGoldenLayout() {
|
|
26502
|
+
// Guard against stale rAF callbacks: render() can run more than once
|
|
26503
|
+
// before the first scheduled callback fires (e.g. a consumer sets
|
|
26504
|
+
// settings/data before the element is connected, then connectedCallback
|
|
26505
|
+
// renders again). Without these checks two GoldenLayout instances would
|
|
26506
|
+
// be created on the same container, duplicating panels and leaking the
|
|
26507
|
+
// first instance's ResizeObserver.
|
|
26508
|
+
if (!this.isConnected) {
|
|
26509
|
+
this._logger.debug("GL", "Skipping GL init - component not connected");
|
|
26510
|
+
return;
|
|
26511
|
+
}
|
|
26512
|
+
if (this._goldenLayout) {
|
|
26513
|
+
this._goldenLayout.destroy();
|
|
26514
|
+
this._goldenLayout = undefined;
|
|
26515
|
+
}
|
|
26300
26516
|
const glContainer = this._shadow.getElementById("gl-container");
|
|
26301
26517
|
if (!glContainer) {
|
|
26302
26518
|
this._logger.error("GL", "gl-container not found in Shadow DOM");
|
|
@@ -26368,6 +26584,18 @@ class GveSnapshotRendition extends HTMLElement {
|
|
|
26368
26584
|
showPopoutIcon: false,
|
|
26369
26585
|
showMaximiseIcon: false,
|
|
26370
26586
|
showCloseIcon: false,
|
|
26587
|
+
// Tab dragging/docking is disabled: the four panels are fixed
|
|
26588
|
+
// functional areas, and users only need the splitters to
|
|
26589
|
+
// redistribute space. This prevents dragging a panel into another
|
|
26590
|
+
// stack (or into limbo) and losing the intended layout. The
|
|
26591
|
+
// "reset layout" toolbar button remains the escape hatch.
|
|
26592
|
+
reorderEnabled: false,
|
|
26593
|
+
},
|
|
26594
|
+
dimensions: {
|
|
26595
|
+
// Prevent panels from being collapsed to zero size via the
|
|
26596
|
+
// splitters, which would make them hard to grab back.
|
|
26597
|
+
defaultMinItemWidth: "80px",
|
|
26598
|
+
defaultMinItemHeight: "60px",
|
|
26371
26599
|
},
|
|
26372
26600
|
root: {
|
|
26373
26601
|
type: "column",
|
|
@@ -26569,12 +26797,13 @@ class GveSnapshotRendition extends HTMLElement {
|
|
|
26569
26797
|
if (refNodes.length === 0) {
|
|
26570
26798
|
this._logger.warn(`No reference nodes found for operation ${operation.id}`);
|
|
26571
26799
|
// Still fire event even if no reference nodes
|
|
26800
|
+
const previousVersion = this._currentVersion;
|
|
26572
26801
|
this._currentVersion = versionTag;
|
|
26573
26802
|
// Extract version index from tag (e.g., "v2" → 2)
|
|
26574
26803
|
const versionMatch = versionTag.match(/v(\d+)/);
|
|
26575
26804
|
this._currentVersionIndex = versionMatch ? parseInt(versionMatch[1]) : 0;
|
|
26576
26805
|
this.dispatchEvent(new CustomEvent("versionTagChange", {
|
|
26577
|
-
detail: { from:
|
|
26806
|
+
detail: { from: previousVersion, to: versionTag },
|
|
26578
26807
|
}));
|
|
26579
26808
|
return;
|
|
26580
26809
|
}
|
|
@@ -26598,7 +26827,7 @@ class GveSnapshotRendition extends HTMLElement {
|
|
|
26598
26827
|
const viewportWidth = this._rootSvg?.clientWidth || 0;
|
|
26599
26828
|
const viewportHeight = this._rootSvg?.clientHeight || 0;
|
|
26600
26829
|
await this._textRenderer.renderAdditionalText(addedNodes, this.getSvgContainer(), // Use viewport container, not root SVG
|
|
26601
|
-
config, versionTag, refNodes, this._baseNodes, this._panZoomInstance, viewportWidth, viewportHeight);
|
|
26830
|
+
config, versionTag, refNodes, this._baseNodes, this._panZoomInstance, viewportWidth, viewportHeight, this._compressedTimeMode);
|
|
26602
26831
|
}
|
|
26603
26832
|
// Update current version
|
|
26604
26833
|
this._currentVersion = versionTag;
|
|
@@ -26684,7 +26913,7 @@ class GveSnapshotRendition extends HTMLElement {
|
|
|
26684
26913
|
const viewportHeight = this._rootSvg?.clientHeight || 0;
|
|
26685
26914
|
await this._hintRenderer.renderHint(hintId, hint, refNodes, this.getSvgContainer(), // Use viewport container, not root SVG
|
|
26686
26915
|
hintVars, operationId, i + 1, // ordinal (1-based)
|
|
26687
|
-
this._baseNodes, versionTag, hintOverrides, this._panZoomInstance, viewportWidth, viewportHeight);
|
|
26916
|
+
this._baseNodes, versionTag, hintOverrides, this._panZoomInstance, viewportWidth, viewportHeight, this._compressedTimeMode);
|
|
26688
26917
|
}
|
|
26689
26918
|
}
|
|
26690
26919
|
/**
|
|
@@ -26933,8 +27162,9 @@ class GveSnapshotRendition extends HTMLElement {
|
|
|
26933
27162
|
return;
|
|
26934
27163
|
}
|
|
26935
27164
|
this._logger.info(`Removing ${elements.length} elements from v${versionIndex}`);
|
|
26936
|
-
// Step 1: If backwardFadeOutTime > 0, animate fadeout
|
|
26937
|
-
|
|
27165
|
+
// Step 1: If backwardFadeOutTime > 0, animate fadeout (skipped in
|
|
27166
|
+
// compressed time mode, where elements are removed immediately)
|
|
27167
|
+
if (this._settings.backwardFadeOutTime > 0 && !this._compressedTimeMode) {
|
|
26938
27168
|
const fadeOutPromises = [];
|
|
26939
27169
|
elements.forEach((el) => {
|
|
26940
27170
|
const promise = gsapWithCSS.to(el, {
|
|
@@ -27396,6 +27626,23 @@ class GveSnapshotRendition extends HTMLElement {
|
|
|
27396
27626
|
getAutoForwardEnabled() {
|
|
27397
27627
|
return this._autoForwardEnabled;
|
|
27398
27628
|
}
|
|
27629
|
+
/**
|
|
27630
|
+
* Toggle compressed time mode. When enabled, navigating to any version
|
|
27631
|
+
* skips hint/text entrance animations, prolog panning animation, and the
|
|
27632
|
+
* backward fade-out, so the target version's final state is shown right
|
|
27633
|
+
* away instead of playing every intervening operation's animations.
|
|
27634
|
+
* @param enabled - Whether to enable compressed time mode
|
|
27635
|
+
*/
|
|
27636
|
+
setCompressedTimeModeEnabled(enabled) {
|
|
27637
|
+
this._compressedTimeMode = enabled;
|
|
27638
|
+
this._logger.info(`Compressed time mode ${enabled ? "enabled" : "disabled"}`);
|
|
27639
|
+
}
|
|
27640
|
+
/**
|
|
27641
|
+
* Get current compressed time mode state.
|
|
27642
|
+
*/
|
|
27643
|
+
getCompressedTimeModeEnabled() {
|
|
27644
|
+
return this._compressedTimeMode;
|
|
27645
|
+
}
|
|
27399
27646
|
/**
|
|
27400
27647
|
* Setup keyboard shortcuts for navigation and zoom.
|
|
27401
27648
|
* Shortcuts:
|
|
@@ -40952,7 +41199,7 @@ function requireD () {
|
|
|
40952
41199
|
+ 'pragma private protected public pure ref return scope shared static struct '
|
|
40953
41200
|
+ 'super switch synchronized template this throw try typedef typeid typeof union '
|
|
40954
41201
|
+ 'unittest version void volatile while with __FILE__ __LINE__ __gshared|10 '
|
|
40955
|
-
+ '__thread __traits __DATE__ __EOF__ __TIME__ __TIMESTAMP__ __VENDOR__ 2.0.
|
|
41202
|
+
+ '__thread __traits __DATE__ __EOF__ __TIME__ __TIMESTAMP__ __VENDOR__ 2.0.11',
|
|
40956
41203
|
built_in:
|
|
40957
41204
|
'bool cdouble cent cfloat char creal dchar delegate double dstring float function '
|
|
40958
41205
|
+ 'idouble ifloat ireal long real short string ubyte ucent uint ulong ushort wchar '
|
|
@@ -90566,23 +90813,36 @@ class GveHintDesigner extends HTMLElement {
|
|
|
90566
90813
|
this._isPlaying = false;
|
|
90567
90814
|
// Create logger
|
|
90568
90815
|
this._logger = new Logger("GVE-HintDesigner", this._settings.debug);
|
|
90569
|
-
|
|
90816
|
+
this.registerGsapPlugins();
|
|
90817
|
+
// Create shadow DOM
|
|
90818
|
+
this._shadow = this.attachShadow({ mode: "open" });
|
|
90819
|
+
this._logger.info("GveHintDesigner component created");
|
|
90820
|
+
}
|
|
90821
|
+
/**
|
|
90822
|
+
* Register GSAP plugins when GSAP is available. Retried in
|
|
90823
|
+
* connectedCallback because GSAP may be loaded after the component
|
|
90824
|
+
* class is constructed.
|
|
90825
|
+
*/
|
|
90826
|
+
registerGsapPlugins() {
|
|
90570
90827
|
const gsap = window.gsap;
|
|
90571
|
-
if (gsap) {
|
|
90828
|
+
if (gsap && !gsap.plugins?.drawSVG) {
|
|
90572
90829
|
gsap.registerPlugin(DrawSVGPlugin);
|
|
90573
90830
|
this._logger.debug("Component", "DrawSVG plugin registered:", gsap.plugins.drawSVG !== undefined);
|
|
90574
90831
|
}
|
|
90575
|
-
// Create shadow DOM
|
|
90576
|
-
this._shadow = this.attachShadow({ mode: "open" });
|
|
90577
|
-
this._logger.info("GveHintDesigner component created");
|
|
90578
90832
|
}
|
|
90579
90833
|
/**
|
|
90580
90834
|
* Called when component is added to the DOM.
|
|
90581
90835
|
*/
|
|
90582
90836
|
connectedCallback() {
|
|
90583
90837
|
this._logger.info("Component connected to DOM");
|
|
90838
|
+
this.registerGsapPlugins();
|
|
90584
90839
|
this.render();
|
|
90585
90840
|
this.initializeEventListeners();
|
|
90841
|
+
// Reflect any state assigned before the element was connected (a common
|
|
90842
|
+
// pattern in vanilla consumers: set data/settings/hintId first, append
|
|
90843
|
+
// later). render() builds empty dropdowns/tables, so populate them now.
|
|
90844
|
+
this.refreshUI();
|
|
90845
|
+
this.refreshVariablesTable();
|
|
90586
90846
|
}
|
|
90587
90847
|
/**
|
|
90588
90848
|
* Called when component is removed from the DOM.
|
|
@@ -90644,9 +90904,10 @@ class GveHintDesigner extends HTMLElement {
|
|
|
90644
90904
|
this._hintVariables = value || [];
|
|
90645
90905
|
}
|
|
90646
90906
|
this.refreshVariablesTable();
|
|
90647
|
-
|
|
90648
|
-
|
|
90649
|
-
|
|
90907
|
+
// NOTE: no hintVariablesChange event here. Change events represent USER
|
|
90908
|
+
// edits (add/edit/delete/populate, which fire it themselves); firing from
|
|
90909
|
+
// the setter would echo programmatic assignments back to the consumer,
|
|
90910
|
+
// which can loop forever with two-way bindings that clone the array.
|
|
90650
90911
|
}
|
|
90651
90912
|
// ==================== RENDERING ====================
|
|
90652
90913
|
/**
|
|
@@ -91261,7 +91522,11 @@ class GveHintDesigner extends HTMLElement {
|
|
|
91261
91522
|
this._lastPanY = mouseEvent.clientY;
|
|
91262
91523
|
panel.style.cursor = "grabbing";
|
|
91263
91524
|
});
|
|
91264
|
-
|
|
91525
|
+
// Remove any handlers from a previous connect cycle before adding new
|
|
91526
|
+
// ones: connectedCallback re-runs initializeEventListeners each time the
|
|
91527
|
+
// element is (re)attached, and document-level listeners would accumulate.
|
|
91528
|
+
this.removeDocumentPanListeners();
|
|
91529
|
+
this._panMoveHandler = (e) => {
|
|
91265
91530
|
if (!this._isPanning)
|
|
91266
91531
|
return;
|
|
91267
91532
|
const deltaX = e.clientX - this._lastPanX;
|
|
@@ -91271,17 +91536,34 @@ class GveHintDesigner extends HTMLElement {
|
|
|
91271
91536
|
this._lastPanX = e.clientX;
|
|
91272
91537
|
this._lastPanY = e.clientY;
|
|
91273
91538
|
this.updateSvgTransform();
|
|
91274
|
-
}
|
|
91275
|
-
document.addEventListener("
|
|
91539
|
+
};
|
|
91540
|
+
document.addEventListener("mousemove", this._panMoveHandler);
|
|
91541
|
+
this._panUpHandler = () => {
|
|
91276
91542
|
if (this._isPanning) {
|
|
91277
91543
|
this._isPanning = false;
|
|
91278
|
-
const panelEl = panel;
|
|
91279
|
-
panelEl
|
|
91544
|
+
const panelEl = this._shadow.querySelector(".svg-display-panel");
|
|
91545
|
+
if (panelEl) {
|
|
91546
|
+
panelEl.style.cursor = "grab";
|
|
91547
|
+
}
|
|
91280
91548
|
}
|
|
91281
|
-
}
|
|
91549
|
+
};
|
|
91550
|
+
document.addEventListener("mouseup", this._panUpHandler);
|
|
91282
91551
|
// Set initial cursor
|
|
91283
91552
|
panel.style.cursor = "grab";
|
|
91284
91553
|
}
|
|
91554
|
+
/**
|
|
91555
|
+
* Remove the document-level pan listeners, if any.
|
|
91556
|
+
*/
|
|
91557
|
+
removeDocumentPanListeners() {
|
|
91558
|
+
if (this._panMoveHandler) {
|
|
91559
|
+
document.removeEventListener("mousemove", this._panMoveHandler);
|
|
91560
|
+
this._panMoveHandler = undefined;
|
|
91561
|
+
}
|
|
91562
|
+
if (this._panUpHandler) {
|
|
91563
|
+
document.removeEventListener("mouseup", this._panUpHandler);
|
|
91564
|
+
this._panUpHandler = undefined;
|
|
91565
|
+
}
|
|
91566
|
+
}
|
|
91285
91567
|
/**
|
|
91286
91568
|
* Initialize zooming with mouse wheel.
|
|
91287
91569
|
*/
|
|
@@ -91325,6 +91607,8 @@ class GveHintDesigner extends HTMLElement {
|
|
|
91325
91607
|
clearInterval(this._progressUpdateInterval);
|
|
91326
91608
|
this._progressUpdateInterval = undefined;
|
|
91327
91609
|
}
|
|
91610
|
+
this.removeDocumentPanListeners();
|
|
91611
|
+
this._isPanning = false;
|
|
91328
91612
|
}
|
|
91329
91613
|
// ==================== UI ACTIONS ====================
|
|
91330
91614
|
/**
|
|
@@ -91693,13 +91977,14 @@ class GveHintDesigner extends HTMLElement {
|
|
|
91693
91977
|
const confirm = window.confirm(`Delete hint "${this._hintId}"?`);
|
|
91694
91978
|
if (!confirm)
|
|
91695
91979
|
return;
|
|
91696
|
-
|
|
91980
|
+
const deletedId = this._hintId;
|
|
91981
|
+
delete this._data.hints[deletedId];
|
|
91697
91982
|
this.hintId = undefined;
|
|
91698
91983
|
this.refreshHintsDropdown();
|
|
91699
91984
|
this.clearHintForm();
|
|
91700
91985
|
this.showMessage("Hint deleted", "success");
|
|
91701
91986
|
this.fireEvent("dataChange", { data: this._data });
|
|
91702
|
-
this._logger.info("Deleted hint",
|
|
91987
|
+
this._logger.info("Deleted hint", deletedId);
|
|
91703
91988
|
}
|
|
91704
91989
|
/**
|
|
91705
91990
|
* Parse offset value (number or string with suffix).
|
|
@@ -91795,26 +92080,29 @@ class GveHintDesigner extends HTMLElement {
|
|
|
91795
92080
|
}
|
|
91796
92081
|
/**
|
|
91797
92082
|
* Resolve variables in SVG code.
|
|
92083
|
+
* Delegates to the same resolver used by the renderer (shared semantics:
|
|
92084
|
+
* literal names — no regex injection — and "??" fallback chains like
|
|
92085
|
+
* {{r_fore-color ?? r_color}}).
|
|
91798
92086
|
*/
|
|
91799
92087
|
resolveVariables(svgCode) {
|
|
91800
|
-
|
|
91801
|
-
|
|
91802
|
-
const placeholder = `{{${variable.name}}}`;
|
|
91803
|
-
resolved = resolved.replace(new RegExp(placeholder, "g"), variable.value);
|
|
91804
|
-
});
|
|
91805
|
-
return resolved;
|
|
92088
|
+
const variables = new Map(this._hintVariables.map((v) => [v.name, v.value]));
|
|
92089
|
+
return resolvePlaceholders(svgCode, variables);
|
|
91806
92090
|
}
|
|
91807
92091
|
/**
|
|
91808
92092
|
* Extract variable names from SVG code (variables in {{...}} placeholders).
|
|
92093
|
+
* Placeholders may contain "??" fallback chains: each name in a chain is
|
|
92094
|
+
* reported as a separate variable.
|
|
91809
92095
|
*/
|
|
91810
92096
|
extractVariablesFromSvg(svgCode) {
|
|
91811
92097
|
const variablePattern = /\{\{([^}]+)\}\}/g;
|
|
91812
92098
|
const variables = [];
|
|
91813
92099
|
let match;
|
|
91814
92100
|
while ((match = variablePattern.exec(svgCode)) !== null) {
|
|
91815
|
-
const
|
|
91816
|
-
|
|
91817
|
-
variables.
|
|
92101
|
+
for (const name of match[1].split("??")) {
|
|
92102
|
+
const varName = name.trim();
|
|
92103
|
+
if (varName && !variables.includes(varName)) {
|
|
92104
|
+
variables.push(varName);
|
|
92105
|
+
}
|
|
91818
92106
|
}
|
|
91819
92107
|
}
|
|
91820
92108
|
return variables;
|