@myrmidon/gve-snapshot-rendition 2.0.1 → 2.0.3
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 +2 -2
- package/dist/index.cjs.min.js.map +1 -1
- package/dist/index.js +259 -171
- package/dist/index.js.map +1 -1
- package/dist/src/core/gve-snapshot-rendition.d.ts +9 -0
- package/dist/src/hint-designer/gve-hint-designer.d.ts +4 -0
- package/dist/src/rendering/text-renderer.d.ts +11 -14
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1385,7 +1385,7 @@ class TextRenderer {
|
|
|
1385
1385
|
}
|
|
1386
1386
|
// Apply r_t-value override if specified
|
|
1387
1387
|
// This overrides the operation's value (the text being added) for display only
|
|
1388
|
-
if (config.textValue) {
|
|
1388
|
+
if (config.textValue !== null && config.textValue !== undefined) {
|
|
1389
1389
|
this._logger.debug("TextRenderer", `Applying r_t-value override: "${config.textValue}" (original text had ${nodes.length} characters)`);
|
|
1390
1390
|
// Replace node data with characters from r_t-value
|
|
1391
1391
|
// If r_t-value has fewer characters than nodes, we only override the first N nodes
|
|
@@ -1417,124 +1417,186 @@ class TextRenderer {
|
|
|
1417
1417
|
// Use first RBR for positioning (additional text doesn't repeat per RBR)
|
|
1418
1418
|
const rbr = rbrs[0];
|
|
1419
1419
|
this._logger.debug("TextRenderer", `Using RBR for positioning`, rbr);
|
|
1420
|
-
// 2. Calculate
|
|
1421
|
-
const position = config.textPosition || "o";
|
|
1422
|
-
const targetPos = this.calculateTargetPosition(position, rbr);
|
|
1423
|
-
// Parse offsets using RBR bounds (offsets can be like "0.5th" = half of RBR height)
|
|
1420
|
+
// 2. Calculate position and offsets
|
|
1421
|
+
const position = config.textPosition || "o";
|
|
1424
1422
|
const offsetX = parseOffset(config.textOffsetX || 0, rbr.height, rbr.width);
|
|
1425
1423
|
const offsetY = parseOffset(config.textOffsetY || 0, rbr.height, rbr.width);
|
|
1426
|
-
//
|
|
1427
|
-
//
|
|
1428
|
-
//
|
|
1429
|
-
//
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1424
|
+
// 3. Render characters at preliminary positions (baseline y=0) into a hidden
|
|
1425
|
+
// temporary group so we can measure the actual visual EBR via getBBox().
|
|
1426
|
+
// This mirrors the hint renderer's approach: render → measure → reposition.
|
|
1427
|
+
// Using config font metrics here ensures correct character widths.
|
|
1428
|
+
const initialPositions = this.calculateAdditionalTextPositions(nodes, 0, 0, config);
|
|
1429
|
+
const tempGroup = createSVGElement("g");
|
|
1430
|
+
tempGroup.setAttribute("visibility", "hidden");
|
|
1431
|
+
rootSvg.appendChild(tempGroup);
|
|
1432
|
+
const measuredElements = [];
|
|
1433
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
1434
|
+
const node = nodes[i];
|
|
1435
|
+
const pos = initialPositions[i];
|
|
1436
|
+
if (isLineBreak(node) || isSpace(node))
|
|
1437
|
+
continue;
|
|
1438
|
+
const textEl = createTextElement(node.data, {
|
|
1439
|
+
id: `n_${node.id}`,
|
|
1440
|
+
class: `node version-${versionTag}`,
|
|
1441
|
+
x: pos.x,
|
|
1442
|
+
y: pos.y,
|
|
1443
|
+
});
|
|
1444
|
+
applyTextStyle(textEl, {
|
|
1445
|
+
fontFamily: config.fontFamily,
|
|
1446
|
+
fontSize: config.fontSize,
|
|
1447
|
+
foreColor: config.foreColor,
|
|
1448
|
+
backColor: config.backColor,
|
|
1449
|
+
italic: config.italic,
|
|
1450
|
+
bold: config.bold,
|
|
1451
|
+
});
|
|
1452
|
+
tempGroup.appendChild(textEl);
|
|
1453
|
+
measuredElements.push({ el: textEl, node, initX: pos.x, initY: pos.y });
|
|
1454
|
+
}
|
|
1455
|
+
// 4. Measure actual visual EBR from the temp group
|
|
1456
|
+
const ebrBbox = getSafeBBox(tempGroup);
|
|
1457
|
+
const ebr = {
|
|
1458
|
+
x: ebrBbox.x,
|
|
1459
|
+
y: ebrBbox.y,
|
|
1460
|
+
width: ebrBbox.width,
|
|
1461
|
+
height: ebrBbox.height,
|
|
1462
|
+
right: ebrBbox.x + ebrBbox.width,
|
|
1463
|
+
bottom: ebrBbox.y + ebrBbox.height,
|
|
1464
|
+
};
|
|
1465
|
+
// 5. Compute translation: align EBR with RBR using the same logic as hints
|
|
1466
|
+
const rbrAlignPoint = this.calculateRBRAlignmentPoint(position, rbr);
|
|
1467
|
+
const ebrAlignPoint = this.calculateEBRAlignmentPoint(position, ebr);
|
|
1468
|
+
const dx = rbrAlignPoint.x - ebrAlignPoint.x + offsetX;
|
|
1469
|
+
const dy = rbrAlignPoint.y - ebrAlignPoint.y + offsetY;
|
|
1470
|
+
tempGroup.remove();
|
|
1449
1471
|
this._logger.debug("TextRenderer", `Text position calculated`, {
|
|
1450
1472
|
position,
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1473
|
+
rbr,
|
|
1474
|
+
ebr,
|
|
1475
|
+
rbrAlignPoint,
|
|
1476
|
+
ebrAlignPoint,
|
|
1454
1477
|
offsets: { x: offsetX, y: offsetY },
|
|
1455
|
-
|
|
1478
|
+
translation: { dx, dy },
|
|
1456
1479
|
});
|
|
1457
|
-
//
|
|
1458
|
-
const
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1480
|
+
// 6. Compute final textBounds for prolog check
|
|
1481
|
+
const textBounds = {
|
|
1482
|
+
x: ebr.x + dx,
|
|
1483
|
+
y: ebr.y + dy,
|
|
1484
|
+
width: ebr.width,
|
|
1485
|
+
height: ebr.height,
|
|
1486
|
+
right: ebr.right + dx,
|
|
1487
|
+
bottom: ebr.bottom + dy,
|
|
1488
|
+
};
|
|
1489
|
+
// 7. Check if prolog panning is needed
|
|
1463
1490
|
if (panZoomInstance &&
|
|
1464
1491
|
viewportWidth &&
|
|
1465
1492
|
viewportHeight &&
|
|
1466
|
-
this._settings.prologDuration > 0
|
|
1467
|
-
textBounds) {
|
|
1493
|
+
this._settings.prologDuration > 0) {
|
|
1468
1494
|
const isVisible = this._animationEngine.isElementVisible(textBounds, panZoomInstance, viewportWidth, viewportHeight);
|
|
1469
1495
|
if (!isVisible) {
|
|
1470
1496
|
this._logger.debug("TextRenderer", `Additional text for ${versionTag} would be outside viewport, executing prolog`);
|
|
1471
1497
|
await this._animationEngine.animateProlog(panZoomInstance, textBounds, viewportWidth, viewportHeight, this._settings.prologDuration);
|
|
1472
1498
|
}
|
|
1473
1499
|
}
|
|
1474
|
-
//
|
|
1500
|
+
// 8. Get animation function if specified
|
|
1475
1501
|
const animationFn = this._settings.charAnimationId
|
|
1476
1502
|
? this._animationEngine
|
|
1477
1503
|
.getFactory()
|
|
1478
1504
|
.resolveAnimation(`#${this._settings.charAnimationId}`, this._settings.animations, "char")
|
|
1479
1505
|
: undefined;
|
|
1480
|
-
//
|
|
1481
|
-
for (
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1506
|
+
// 9. Move each element to its final position and add to the SVG
|
|
1507
|
+
for (const { el, node, initX, initY } of measuredElements) {
|
|
1508
|
+
el.setAttribute("x", String(initX + dx));
|
|
1509
|
+
el.setAttribute("y", String(initY + dy));
|
|
1510
|
+
rootSvg.appendChild(el);
|
|
1511
|
+
const bbox = getSafeBBox(el);
|
|
1512
|
+
this._boundsCache.set(`n_${node.id}`, bbox);
|
|
1513
|
+
if (animationFn) {
|
|
1514
|
+
el.style.opacity = "0";
|
|
1515
|
+
await this._animationEngine.animate(el, animationFn, rootSvg);
|
|
1487
1516
|
}
|
|
1488
|
-
await this.renderCharacter(node, pos, rootSvg, versionTag, animationFn, config);
|
|
1489
1517
|
}
|
|
1490
1518
|
this._logger.timeEnd(`renderAdditionalText-${versionTag}`);
|
|
1491
1519
|
}
|
|
1492
1520
|
/**
|
|
1493
1521
|
* Calculate positions for additional text characters.
|
|
1494
1522
|
* Additional text flows left to right from the base position.
|
|
1523
|
+
* When config is provided its font metrics are used for character width
|
|
1524
|
+
* measurement, matching what applyTextStyle will actually render.
|
|
1495
1525
|
*/
|
|
1496
|
-
calculateAdditionalTextPositions(nodes, baseX, baseY) {
|
|
1526
|
+
calculateAdditionalTextPositions(nodes, baseX, baseY, config) {
|
|
1497
1527
|
const positions = [];
|
|
1498
1528
|
let currentX = baseX;
|
|
1499
1529
|
const currentY = baseY;
|
|
1530
|
+
const fontSize = config?.fontSize ?? this._settings.fontSize;
|
|
1531
|
+
const fontFamily = config?.fontFamily ?? this._settings.fontFamily;
|
|
1532
|
+
const bold = config?.bold ?? this._settings.bold;
|
|
1533
|
+
const italic = config?.italic ?? this._settings.italic;
|
|
1500
1534
|
for (let i = 0; i < nodes.length; i++) {
|
|
1501
1535
|
const node = nodes[i];
|
|
1502
1536
|
if (isLineBreak(node)) {
|
|
1503
|
-
|
|
1504
|
-
positions.push({
|
|
1505
|
-
x: currentX,
|
|
1506
|
-
y: currentY,
|
|
1507
|
-
nodeId: node.id,
|
|
1508
|
-
lineNumber: 0,
|
|
1509
|
-
});
|
|
1537
|
+
positions.push({ x: currentX, y: currentY, nodeId: node.id, lineNumber: 0 });
|
|
1510
1538
|
}
|
|
1511
1539
|
else if (isSpace(node)) {
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
positions.push({
|
|
1515
|
-
x: currentX,
|
|
1516
|
-
y: currentY,
|
|
1517
|
-
nodeId: node.id,
|
|
1518
|
-
lineNumber: 0,
|
|
1519
|
-
});
|
|
1540
|
+
const spaceWidth = fontSize * 0.33;
|
|
1541
|
+
positions.push({ x: currentX, y: currentY, nodeId: node.id, lineNumber: 0 });
|
|
1520
1542
|
currentX += spaceWidth + this._settings.charSpacing;
|
|
1521
1543
|
}
|
|
1522
1544
|
else {
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
x: currentX,
|
|
1526
|
-
y: currentY,
|
|
1527
|
-
nodeId: node.id,
|
|
1528
|
-
lineNumber: 0,
|
|
1529
|
-
});
|
|
1530
|
-
// Calculate character width for next position
|
|
1531
|
-
// Use measurement root for consistent font metrics with actual rendering
|
|
1532
|
-
const charWidth = getTextWidth(node.data, this._settings.fontFamily, this._settings.fontSize, this._settings.bold, this._settings.italic, this._measurementRoot);
|
|
1545
|
+
positions.push({ x: currentX, y: currentY, nodeId: node.id, lineNumber: 0 });
|
|
1546
|
+
const charWidth = getTextWidth(node.data, fontFamily, fontSize, bold, italic, this._measurementRoot);
|
|
1533
1547
|
currentX += charWidth + this._settings.charSpacing;
|
|
1534
1548
|
}
|
|
1535
1549
|
}
|
|
1536
1550
|
return positions;
|
|
1537
1551
|
}
|
|
1552
|
+
/**
|
|
1553
|
+
* Calculate the RBR anchor point for a given position type.
|
|
1554
|
+
* This is the point on the RBR where the corresponding EBR point will land.
|
|
1555
|
+
*/
|
|
1556
|
+
calculateRBRAlignmentPoint(position, rbr) {
|
|
1557
|
+
const centerX = rbr.x + rbr.width / 2;
|
|
1558
|
+
const centerY = rbr.y + rbr.height / 2;
|
|
1559
|
+
switch (position) {
|
|
1560
|
+
case "n": return { x: centerX, y: rbr.y };
|
|
1561
|
+
case "ne": return { x: rbr.right, y: rbr.y };
|
|
1562
|
+
case "e": return { x: rbr.right, y: centerY };
|
|
1563
|
+
case "se": return { x: rbr.right, y: rbr.bottom };
|
|
1564
|
+
case "s": return { x: centerX, y: rbr.bottom };
|
|
1565
|
+
case "sw": return { x: rbr.x, y: rbr.bottom };
|
|
1566
|
+
case "w": return { x: rbr.x, y: centerY };
|
|
1567
|
+
case "nw": return { x: rbr.x, y: rbr.y };
|
|
1568
|
+
case "inw": return { x: rbr.x, y: rbr.y };
|
|
1569
|
+
case "ine": return { x: rbr.right, y: rbr.y };
|
|
1570
|
+
case "isw": return { x: rbr.x, y: rbr.bottom };
|
|
1571
|
+
case "ise": return { x: rbr.right, y: rbr.bottom };
|
|
1572
|
+
case "o":
|
|
1573
|
+
default: return { x: centerX, y: centerY };
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Calculate the EBR anchor point for a given position type.
|
|
1578
|
+
* This is the point on the EBR that should coincide with the RBR anchor point.
|
|
1579
|
+
*/
|
|
1580
|
+
calculateEBRAlignmentPoint(position, ebr) {
|
|
1581
|
+
const centerX = ebr.x + ebr.width / 2;
|
|
1582
|
+
const centerY = ebr.y + ebr.height / 2;
|
|
1583
|
+
switch (position) {
|
|
1584
|
+
case "n": return { x: centerX, y: ebr.bottom };
|
|
1585
|
+
case "ne": return { x: ebr.x, y: ebr.bottom };
|
|
1586
|
+
case "e": return { x: ebr.x, y: centerY };
|
|
1587
|
+
case "se": return { x: ebr.x, y: ebr.y };
|
|
1588
|
+
case "s": return { x: centerX, y: ebr.y };
|
|
1589
|
+
case "sw": return { x: ebr.right, y: ebr.y };
|
|
1590
|
+
case "w": return { x: ebr.right, y: centerY };
|
|
1591
|
+
case "nw": return { x: ebr.right, y: ebr.bottom };
|
|
1592
|
+
case "inw": return { x: ebr.x, y: ebr.y };
|
|
1593
|
+
case "ine": return { x: ebr.right, y: ebr.y };
|
|
1594
|
+
case "isw": return { x: ebr.x, y: ebr.bottom };
|
|
1595
|
+
case "ise": return { x: ebr.right, y: ebr.bottom };
|
|
1596
|
+
case "o":
|
|
1597
|
+
default: return { x: centerX, y: centerY };
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1538
1600
|
/**
|
|
1539
1601
|
* Calculate RBRs from reference nodes.
|
|
1540
1602
|
* Nodes on different lines create separate RBRs.
|
|
@@ -1577,96 +1639,6 @@ class TextRenderer {
|
|
|
1577
1639
|
}
|
|
1578
1640
|
return rbrs;
|
|
1579
1641
|
}
|
|
1580
|
-
/**
|
|
1581
|
-
* Calculate target position on the RBR based on position type.
|
|
1582
|
-
*/
|
|
1583
|
-
calculateTargetPosition(position, rbr) {
|
|
1584
|
-
const centerX = rbr.x + rbr.width / 2;
|
|
1585
|
-
const centerY = rbr.y + rbr.height / 2;
|
|
1586
|
-
switch (position) {
|
|
1587
|
-
case "n": // North (top center)
|
|
1588
|
-
return { x: centerX, y: rbr.y };
|
|
1589
|
-
case "ne": // Northeast (top right)
|
|
1590
|
-
return { x: rbr.right, y: rbr.y };
|
|
1591
|
-
case "e": // East (middle right)
|
|
1592
|
-
return { x: rbr.right, y: centerY };
|
|
1593
|
-
case "se": // Southeast (bottom right)
|
|
1594
|
-
return { x: rbr.right, y: rbr.bottom };
|
|
1595
|
-
case "s": // South (bottom center)
|
|
1596
|
-
return { x: centerX, y: rbr.bottom };
|
|
1597
|
-
case "sw": // Southwest (bottom left)
|
|
1598
|
-
return { x: rbr.x, y: rbr.bottom };
|
|
1599
|
-
case "w": // West (middle left)
|
|
1600
|
-
return { x: rbr.x, y: centerY };
|
|
1601
|
-
case "nw": // Northwest (top left)
|
|
1602
|
-
return { x: rbr.x, y: rbr.y };
|
|
1603
|
-
case "o": // Origin (center)
|
|
1604
|
-
default:
|
|
1605
|
-
return { x: centerX, y: centerY };
|
|
1606
|
-
}
|
|
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
|
-
}
|
|
1635
|
-
/**
|
|
1636
|
-
* Calculate bounding rectangle for a set of positioned characters.
|
|
1637
|
-
*/
|
|
1638
|
-
calculateTextBounds(nodes, positions) {
|
|
1639
|
-
if (nodes.length === 0)
|
|
1640
|
-
return null;
|
|
1641
|
-
// We need to estimate bounds before rendering
|
|
1642
|
-
// Use approximate character dimensions based on settings
|
|
1643
|
-
const charWidth = this._settings.fontSize * 0.6; // Approximate
|
|
1644
|
-
const charHeight = this._settings.fontSize;
|
|
1645
|
-
let minX = Infinity;
|
|
1646
|
-
let minY = Infinity;
|
|
1647
|
-
let maxX = -Infinity;
|
|
1648
|
-
let maxY = -Infinity;
|
|
1649
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
1650
|
-
const pos = positions[i];
|
|
1651
|
-
const node = nodes[i];
|
|
1652
|
-
if (isLineBreak(node) || isSpace(node))
|
|
1653
|
-
continue;
|
|
1654
|
-
minX = Math.min(minX, pos.x);
|
|
1655
|
-
minY = Math.min(minY, pos.y - charHeight);
|
|
1656
|
-
maxX = Math.max(maxX, pos.x + charWidth);
|
|
1657
|
-
maxY = Math.max(maxY, pos.y);
|
|
1658
|
-
}
|
|
1659
|
-
if (!isFinite(minX))
|
|
1660
|
-
return null;
|
|
1661
|
-
return {
|
|
1662
|
-
x: minX,
|
|
1663
|
-
y: minY,
|
|
1664
|
-
width: maxX - minX,
|
|
1665
|
-
height: maxY - minY,
|
|
1666
|
-
right: maxX,
|
|
1667
|
-
bottom: maxY,
|
|
1668
|
-
};
|
|
1669
|
-
}
|
|
1670
1642
|
/**
|
|
1671
1643
|
* Update settings.
|
|
1672
1644
|
*/
|
|
@@ -25945,7 +25917,7 @@ class GveSnapshotRendition extends HTMLElement {
|
|
|
25945
25917
|
* of the web component is loaded.
|
|
25946
25918
|
*/
|
|
25947
25919
|
static get version() {
|
|
25948
|
-
return "2.0.
|
|
25920
|
+
return "2.0.3";
|
|
25949
25921
|
}
|
|
25950
25922
|
constructor() {
|
|
25951
25923
|
super();
|
|
@@ -25957,6 +25929,7 @@ class GveSnapshotRendition extends HTMLElement {
|
|
|
25957
25929
|
this._currentVersionIndex = 0;
|
|
25958
25930
|
this._autoForwardEnabled = false;
|
|
25959
25931
|
this._autoForwardTimerId = null;
|
|
25932
|
+
this._renderScheduled = false;
|
|
25960
25933
|
// Initialize with default settings
|
|
25961
25934
|
this._settings = { ...DEFAULT_SETTINGS };
|
|
25962
25935
|
this._autoForwardEnabled = this._settings.autoForwardOnGroup;
|
|
@@ -26029,14 +26002,20 @@ class GveSnapshotRendition extends HTMLElement {
|
|
|
26029
26002
|
this._settings = { ...DEFAULT_SETTINGS, ...value };
|
|
26030
26003
|
this._logger.setEnabled(this._settings.debug || false);
|
|
26031
26004
|
this._logger.info("Settings updated", this._settings);
|
|
26032
|
-
// When settings change, update renderers
|
|
26033
26005
|
if (this._textRenderer) {
|
|
26034
26006
|
this._textRenderer.updateSettings(this._settings);
|
|
26035
26007
|
}
|
|
26036
26008
|
if (this._hintRenderer) {
|
|
26037
26009
|
this._hintRenderer.updateSettings(this._settings);
|
|
26038
26010
|
}
|
|
26039
|
-
|
|
26011
|
+
// Build the UI shell on first call; after that just schedule a content
|
|
26012
|
+
// re-render so rapid settings+data assignments coalesce into one render.
|
|
26013
|
+
if (!this._container) {
|
|
26014
|
+
this.render();
|
|
26015
|
+
}
|
|
26016
|
+
else {
|
|
26017
|
+
this.scheduleRenderContent();
|
|
26018
|
+
}
|
|
26040
26019
|
}
|
|
26041
26020
|
/**
|
|
26042
26021
|
* Data property (bindable).
|
|
@@ -26138,20 +26117,40 @@ class GveSnapshotRendition extends HTMLElement {
|
|
|
26138
26117
|
});
|
|
26139
26118
|
}
|
|
26140
26119
|
}
|
|
26141
|
-
//
|
|
26120
|
+
// Schedule content rendering. Using scheduleRenderContent() rather than
|
|
26121
|
+
// a direct call ensures that when settings and data are both set in the
|
|
26122
|
+
// same synchronous turn (the common Angular pattern), only one render
|
|
26123
|
+
// fires after both assignments have completed.
|
|
26142
26124
|
if (this._textRenderer && this._rootSvg && this._baseNodes.length > 0) {
|
|
26143
|
-
this.
|
|
26125
|
+
this.scheduleRenderContent();
|
|
26144
26126
|
}
|
|
26145
26127
|
}
|
|
26146
26128
|
/**
|
|
26147
26129
|
* Render the component UI.
|
|
26148
26130
|
*/
|
|
26149
26131
|
render() {
|
|
26132
|
+
// Cancel any pending coalesced render — initializeGoldenLayout() will
|
|
26133
|
+
// trigger renderContent() once the new DOM is actually ready.
|
|
26134
|
+
this._renderScheduled = false;
|
|
26150
26135
|
// Destroy existing GL instance before clearing the DOM
|
|
26151
26136
|
if (this._goldenLayout) {
|
|
26152
26137
|
this._goldenLayout.destroy();
|
|
26153
26138
|
this._goldenLayout = undefined;
|
|
26154
26139
|
}
|
|
26140
|
+
// Destroy pan/zoom — its internal state is tied to the old SVG element.
|
|
26141
|
+
// initializePanZoom() guards against double-init with a null-check,
|
|
26142
|
+
// so it must be reset here or it will silently skip re-initialization.
|
|
26143
|
+
if (this._panZoomInstance) {
|
|
26144
|
+
this._panZoomInstance.destroy();
|
|
26145
|
+
this._panZoomInstance = undefined;
|
|
26146
|
+
}
|
|
26147
|
+
// Kill in-flight GSAP animations on the old SVG tree.
|
|
26148
|
+
if (this._animationEngine) {
|
|
26149
|
+
this._animationEngine.killAll();
|
|
26150
|
+
}
|
|
26151
|
+
// Reset toolbar — processData() only creates it when this is null,
|
|
26152
|
+
// so leaving a stale reference would prevent toolbar rebuild.
|
|
26153
|
+
this._toolbar = undefined;
|
|
26155
26154
|
this._shadow.innerHTML = "";
|
|
26156
26155
|
// Add styles (includes inlined GL CSS for Shadow DOM compatibility)
|
|
26157
26156
|
const style = document.createElement("style");
|
|
@@ -26362,6 +26361,22 @@ class GveSnapshotRendition extends HTMLElement {
|
|
|
26362
26361
|
this._logger.error("GL", "Failed to reset layout", error);
|
|
26363
26362
|
}
|
|
26364
26363
|
}
|
|
26364
|
+
/**
|
|
26365
|
+
* Coalesce concurrent render requests into a single call.
|
|
26366
|
+
* Multiple same-turn invocations (e.g. settings setter + data setter both
|
|
26367
|
+
* firing in the same Angular expression) collapse into one microtask render,
|
|
26368
|
+
* always running after all synchronous assignments have completed so the
|
|
26369
|
+
* latest data is used.
|
|
26370
|
+
*/
|
|
26371
|
+
scheduleRenderContent() {
|
|
26372
|
+
if (this._renderScheduled)
|
|
26373
|
+
return;
|
|
26374
|
+
this._renderScheduled = true;
|
|
26375
|
+
Promise.resolve().then(() => {
|
|
26376
|
+
this._renderScheduled = false;
|
|
26377
|
+
this.renderContent();
|
|
26378
|
+
});
|
|
26379
|
+
}
|
|
26365
26380
|
/**
|
|
26366
26381
|
* Render content (base text only - starts at v0).
|
|
26367
26382
|
* Use goToVersionIndex() to navigate to specific versions.
|
|
@@ -40832,7 +40847,7 @@ function requireD () {
|
|
|
40832
40847
|
+ 'pragma private protected public pure ref return scope shared static struct '
|
|
40833
40848
|
+ 'super switch synchronized template this throw try typedef typeid typeof union '
|
|
40834
40849
|
+ 'unittest version void volatile while with __FILE__ __LINE__ __gshared|10 '
|
|
40835
|
-
+ '__thread __traits __DATE__ __EOF__ __TIME__ __TIMESTAMP__ __VENDOR__ 2.0.
|
|
40850
|
+
+ '__thread __traits __DATE__ __EOF__ __TIME__ __TIMESTAMP__ __VENDOR__ 2.0.3',
|
|
40836
40851
|
built_in:
|
|
40837
40852
|
'bool cdouble cent cfloat char creal dchar delegate double dstring float function '
|
|
40838
40853
|
+ 'idouble ifloat ireal long real short string ubyte ucent uint ulong ushort wchar '
|
|
@@ -90615,9 +90630,11 @@ class GveHintDesigner extends HTMLElement {
|
|
|
90615
90630
|
const loadDataBtn = this.createButton("load-data", "upload", "Load data from file");
|
|
90616
90631
|
const clearDataBtn = this.createButton("clear-data", "x-circle", "Clear all data");
|
|
90617
90632
|
clearDataBtn.classList.add("btn-danger");
|
|
90633
|
+
const exportInkscapeBtn = this.createButton("export-inkscape", "external-link", "Export hint to InkScape SVG");
|
|
90618
90634
|
dataGroup.appendChild(saveDataBtn);
|
|
90619
90635
|
dataGroup.appendChild(loadDataBtn);
|
|
90620
90636
|
dataGroup.appendChild(clearDataBtn);
|
|
90637
|
+
dataGroup.appendChild(exportInkscapeBtn);
|
|
90621
90638
|
toolbar.appendChild(dataGroup);
|
|
90622
90639
|
return toolbar;
|
|
90623
90640
|
}
|
|
@@ -91065,6 +91082,7 @@ class GveHintDesigner extends HTMLElement {
|
|
|
91065
91082
|
this.addClickListener("save-data", () => this.saveData());
|
|
91066
91083
|
this.addClickListener("load-data", () => this.loadData());
|
|
91067
91084
|
this.addClickListener("clear-data", () => this.clearData());
|
|
91085
|
+
this.addClickListener("export-inkscape", () => this.exportToInkscape());
|
|
91068
91086
|
// Timeline player
|
|
91069
91087
|
this.addClickListener("play", () => this.playAnimation());
|
|
91070
91088
|
this.addClickListener("pause", () => this.pauseAnimation());
|
|
@@ -92339,6 +92357,76 @@ class GveHintDesigner extends HTMLElement {
|
|
|
92339
92357
|
this.showMessage("All data cleared", "success");
|
|
92340
92358
|
this._logger.info("Data cleared");
|
|
92341
92359
|
}
|
|
92360
|
+
/**
|
|
92361
|
+
* Export the current hint as an InkScape-compatible SVG file.
|
|
92362
|
+
*/
|
|
92363
|
+
exportToInkscape() {
|
|
92364
|
+
if (!this._hintId) {
|
|
92365
|
+
this.showMessage("No hint selected!", "error");
|
|
92366
|
+
return;
|
|
92367
|
+
}
|
|
92368
|
+
const hint = this._data.hints[this._hintId];
|
|
92369
|
+
if (!hint?.svg) {
|
|
92370
|
+
this.showMessage("No SVG to export!", "error");
|
|
92371
|
+
return;
|
|
92372
|
+
}
|
|
92373
|
+
const w = this._settings.hintDesignWidth;
|
|
92374
|
+
const h = this._settings.hintDesignHeight;
|
|
92375
|
+
const hw = w / 2;
|
|
92376
|
+
const hh = h / 2;
|
|
92377
|
+
const resolvedSvg = this.resolveVariables(hint.svg);
|
|
92378
|
+
const unresolvedMatches = resolvedSvg.match(/\{\{[^}]+\}\}/g);
|
|
92379
|
+
const svgContent = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n` +
|
|
92380
|
+
`<svg\n` +
|
|
92381
|
+
` width="${w}"\n` +
|
|
92382
|
+
` height="${h}"\n` +
|
|
92383
|
+
` viewBox="0 0 ${w} ${h}"\n` +
|
|
92384
|
+
` version="1.1"\n` +
|
|
92385
|
+
` id="svg1"\n` +
|
|
92386
|
+
` sodipodi:docname="${this._hintId}.svg"\n` +
|
|
92387
|
+
` xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"\n` +
|
|
92388
|
+
` xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"\n` +
|
|
92389
|
+
` xmlns="http://www.w3.org/2000/svg"\n` +
|
|
92390
|
+
` xmlns:svg="http://www.w3.org/2000/svg">\n` +
|
|
92391
|
+
` <sodipodi:namedview\n` +
|
|
92392
|
+
` id="namedview1"\n` +
|
|
92393
|
+
` pagecolor="#ffffff"\n` +
|
|
92394
|
+
` bordercolor="#000000"\n` +
|
|
92395
|
+
` borderopacity="0.25"\n` +
|
|
92396
|
+
` inkscape:showpageshadow="2"\n` +
|
|
92397
|
+
` inkscape:pageopacity="0.0"\n` +
|
|
92398
|
+
` inkscape:pagecheckerboard="false"\n` +
|
|
92399
|
+
` inkscape:deskcolor="#d1d1d1"\n` +
|
|
92400
|
+
` inkscape:document-units="px"\n` +
|
|
92401
|
+
` inkscape:zoom="3"\n` +
|
|
92402
|
+
` inkscape:cx="${hw}"\n` +
|
|
92403
|
+
` inkscape:cy="${hh}"\n` +
|
|
92404
|
+
` inkscape:window-maximized="0"\n` +
|
|
92405
|
+
` inkscape:current-layer="g1"\n` +
|
|
92406
|
+
` showgrid="false" />\n` +
|
|
92407
|
+
` <defs id="defs1" />\n` +
|
|
92408
|
+
` <g\n` +
|
|
92409
|
+
` inkscape:label="Layer 1"\n` +
|
|
92410
|
+
` inkscape:groupmode="layer"\n` +
|
|
92411
|
+
` id="layer1" />\n` +
|
|
92412
|
+
` ${resolvedSvg}\n` +
|
|
92413
|
+
`</svg>`;
|
|
92414
|
+
const blob = new Blob([svgContent], { type: "image/svg+xml" });
|
|
92415
|
+
const url = URL.createObjectURL(blob);
|
|
92416
|
+
const link = document.createElement("a");
|
|
92417
|
+
link.href = url;
|
|
92418
|
+
link.download = `${this._hintId}.svg`;
|
|
92419
|
+
link.click();
|
|
92420
|
+
URL.revokeObjectURL(url);
|
|
92421
|
+
if (unresolvedMatches) {
|
|
92422
|
+
const names = [...new Set(unresolvedMatches)].join(", ");
|
|
92423
|
+
this.showMessage(`Exported "${this._hintId}.svg" — warning: unresolved variables: ${names}`, "info");
|
|
92424
|
+
}
|
|
92425
|
+
else {
|
|
92426
|
+
this.showMessage(`Exported "${this._hintId}.svg" for InkScape`, "success");
|
|
92427
|
+
}
|
|
92428
|
+
this._logger.info("Exported hint to InkScape", this._hintId);
|
|
92429
|
+
}
|
|
92342
92430
|
/**
|
|
92343
92431
|
* Load animation code from catalog.
|
|
92344
92432
|
*/
|