@myrmidon/gve-snapshot-rendition 2.0.1 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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 text position based on r_t-position + offsets
1421
- const position = config.textPosition || "o"; // Default to origin if not specified
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
- // Pre-calculate text width to correctly align the EBR with the RBR target point.
1427
- // calculateTargetPosition returns the RBR alignment point, but the text renderer
1428
- // lays characters left-to-right from baseX, so we must shift left so the intended
1429
- // edge or center of the EBR lands on that point.
1430
- // n/s/c/o: horizontal centers of EBR and RBR must coincide → shift left by half width
1431
- // w/nw/sw: EBR right edge at RBR target X → shift left by full width
1432
- const textWidth = this.calculateTotalTextWidth(nodes);
1433
- let alignedX = targetPos.x;
1434
- switch (position) {
1435
- case "n":
1436
- case "s":
1437
- case "c":
1438
- case "o":
1439
- alignedX -= textWidth / 2;
1440
- break;
1441
- case "w":
1442
- case "nw":
1443
- case "sw":
1444
- alignedX -= textWidth;
1445
- break;
1446
- }
1447
- const baseX = alignedX + offsetX;
1448
- const baseY = targetPos.y + offsetY;
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
- targetPos,
1452
- textWidth,
1453
- alignedX,
1473
+ rbr,
1474
+ ebr,
1475
+ rbrAlignPoint,
1476
+ ebrAlignPoint,
1454
1477
  offsets: { x: offsetX, y: offsetY },
1455
- base: { x: baseX, y: baseY },
1478
+ translation: { dx, dy },
1456
1479
  });
1457
- // 3. Calculate positions for each character
1458
- const positions = this.calculateAdditionalTextPositions(nodes, baseX, baseY);
1459
- // 4. Calculate bounding rectangle for the text (needed for prolog)
1460
- const textBounds = this.calculateTextBounds(nodes, positions);
1461
- // 5. Check if prolog panning is needed (element visibility check)
1462
- // This must happen AFTER positioning, but BEFORE rendering characters
1480
+ // 6. Compute final textBounds for prolog check
1481
+ const textBounds = {
1482
+ x: ebr.x + dx,
1483
+ y: ebr.y + dy,
1484
+ width: ebr.width,
1485
+ height: ebr.height,
1486
+ right: ebr.right + dx,
1487
+ bottom: ebr.bottom + dy,
1488
+ };
1489
+ // 7. Check if prolog panning is needed
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
- // 6. Get animation function if specified
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
- // 7. Render each character with features
1481
- for (let i = 0; i < nodes.length; i++) {
1482
- const node = nodes[i];
1483
- const pos = positions[i];
1484
- // Skip rendering for line breaks and spaces
1485
- if (isLineBreak(node) || isSpace(node)) {
1486
- continue;
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
- // Line breaks in additional text just get stored but don't render
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
- // Space: advance by space width
1513
- const spaceWidth = this._settings.fontSize * 0.33;
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
- // Regular character
1524
- positions.push({
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.1";
25920
+ return "2.0.2";
25949
25921
  }
25950
25922
  constructor() {
25951
25923
  super();
@@ -40832,7 +40804,7 @@ function requireD () {
40832
40804
  + 'pragma private protected public pure ref return scope shared static struct '
40833
40805
  + 'super switch synchronized template this throw try typedef typeid typeof union '
40834
40806
  + 'unittest version void volatile while with __FILE__ __LINE__ __gshared|10 '
40835
- + '__thread __traits __DATE__ __EOF__ __TIME__ __TIMESTAMP__ __VENDOR__ 2.0.1',
40807
+ + '__thread __traits __DATE__ __EOF__ __TIME__ __TIMESTAMP__ __VENDOR__ 2.0.2',
40836
40808
  built_in:
40837
40809
  'bool cdouble cent cfloat char creal dchar delegate double dstring float function '
40838
40810
  + 'idouble ifloat ireal long real short string ubyte ucent uint ulong ushort wchar '