@vectojs/core 1.38.0 → 1.39.0

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.
@@ -398,6 +398,18 @@ var Entity = class {
398
398
  }
399
399
  /** Attach a single child (the O(1) common path). See {@link add}. */
400
400
  _addOne(child) {
401
+ if (child === this) {
402
+ throw new Error(`Entity.add(): cannot add entity "${child.id}" under itself.`);
403
+ }
404
+ let ancestor = this.parent;
405
+ while (ancestor) {
406
+ if (ancestor === child) {
407
+ throw new Error(
408
+ `Entity.add(): cannot add entity "${child.id}" under "${this.id}" \u2014 an entity cannot be added under its own descendant.`
409
+ );
410
+ }
411
+ ancestor = ancestor.parent;
412
+ }
401
413
  if (child.parent) child.parent.remove(child);
402
414
  child.parent = this;
403
415
  this.children.push(child);
@@ -485,6 +497,19 @@ var Entity = class {
485
497
  * @example entity.animate({ x: 400, opacity: 0 }, 500);
486
498
  */
487
499
  animate(targetProps, durationMs) {
500
+ if (!Number.isFinite(durationMs) || durationMs <= 0) {
501
+ for (const key in targetProps) {
502
+ const end = targetProps[key];
503
+ if (typeof end !== "number") continue;
504
+ if (ANIMATABLE_PROPS.has(key)) {
505
+ this._applyAnimated(key, end);
506
+ } else {
507
+ this[key] = end;
508
+ }
509
+ }
510
+ this.scene?.markDirty({ entity: this.id, reason: "animation-start" });
511
+ return this;
512
+ }
488
513
  (this.animations ??= []).push({
489
514
  target: targetProps,
490
515
  duration: durationMs,
@@ -566,6 +591,11 @@ var Entity = class {
566
591
  const from = this._currentOf(prop);
567
592
  const driver = isTweenConfig(cfg) ? new TweenDriver(from, to, cfg) : new SpringDriver(from, to, cfg === "spring" ? {} : cfg);
568
593
  (this._drivers ??= /* @__PURE__ */ new Map()).set(prop, driver);
594
+ const s = this.scene;
595
+ if (s && this._driversTickedFrame === s.currentFrame && s._updateWalkDt !== null) {
596
+ driver.tick(s._updateWalkDt);
597
+ this._applyDriverTick(prop, driver);
598
+ }
569
599
  this.scene?.markDirty({ entity: this.id, reason: "driver-added" });
570
600
  this.scene?._registerActiveDriverEntity(this);
571
601
  }
@@ -652,10 +682,6 @@ var Entity = class {
652
682
  this._applyAnimated(prop, driver.value);
653
683
  }
654
684
  }
655
- /** Internal: true if this entity currently has any active property driver. */
656
- _hasActiveDrivers() {
657
- return !!this._drivers && this._drivers.size > 0;
658
- }
659
685
  /**
660
686
  * Advance the entity's internal state for one frame.
661
687
  *
@@ -1024,6 +1050,14 @@ var Entity = class {
1024
1050
  * Accumulated world rotation: this entity's own `rotation` plus
1025
1051
  * that of every ancestor.
1026
1052
  *
1053
+ * Valid only under positive scales: the sum models the composed matrix
1054
+ * `T*S*R` correctly while every ancestor's `scaleX`/`scaleY` is positive,
1055
+ * but a mirrored (negative-scale) ancestor flips handedness, which an
1056
+ * additive sum cannot represent — the result is then off by the mirror.
1057
+ * For mirror-safe rotation, derive the angle from
1058
+ * {@link getWorldTransform}'s matrix (e.g. `atan2(b, a)`), as SVGEntity's
1059
+ * signed-scale handling already does.
1060
+ *
1027
1061
  * @returns The accumulated world rotation in radians.
1028
1062
  */
1029
1063
  getWorldRotation() {
@@ -1242,7 +1276,6 @@ var MSDFTextEntity = class extends Entity {
1242
1276
  // long-lived shared atlas image would retain the whole entity.
1243
1277
  atlasDecodeTarget = null;
1244
1278
  atlasDecodeHandler = null;
1245
- rgbColorCache = /* @__PURE__ */ new Map();
1246
1279
  fontStringCache = [];
1247
1280
  layoutResult = null;
1248
1281
  /**
@@ -1429,7 +1462,12 @@ var MSDFTextEntity = class extends Entity {
1429
1462
  shaped.push(String.fromCodePoint(res.codePoints[i]));
1430
1463
  }
1431
1464
  const sourceWithoutNewlines = this.text.replace(/\n/g, "");
1432
- if (this.textAlign !== "left" || shaped.join("") !== sourceWithoutNewlines || res.yCoords.length !== res.codePoints.length) {
1465
+ if (this.textAlign !== "left" || // The layout worker breaks lines on \n only, so a \r survives as a real
1466
+ // glyph — a phantom ~1em advance at every CRLF line end — yet would
1467
+ // still compare equal below after \n is stripped from both sides. The
1468
+ // contract promises the coarse branch whenever the reply cannot
1469
+ // reproduce the source exactly, and that includes any \r (#692).
1470
+ this.text.includes("\r") || shaped.join("") !== sourceWithoutNewlines || res.yCoords.length !== res.codePoints.length) {
1433
1471
  this.projectionLines = [];
1434
1472
  return;
1435
1473
  }
@@ -1438,9 +1476,19 @@ var MSDFTextEntity = class extends Entity {
1438
1476
  const desc = metrics?.descender ?? -0.2;
1439
1477
  const actualLineHeight = this.lineHeight ?? this.fontSize * (asc - desc);
1440
1478
  const baseline = asc * this.fontSize;
1441
- const srcIdx = [];
1442
- for (let i = 0; i < this.text.length; i++) {
1443
- if (this.text[i] !== "\n") srcIdx.push(i);
1479
+ const srcStart = [];
1480
+ const srcEnd = [];
1481
+ {
1482
+ let cursor = 0;
1483
+ while (cursor < this.text.length) {
1484
+ const code = this.text.codePointAt(cursor);
1485
+ const width = code > 65535 ? 2 : 1;
1486
+ if (code !== 10) {
1487
+ srcStart.push(cursor);
1488
+ srcEnd.push(cursor + width);
1489
+ }
1490
+ cursor += width;
1491
+ }
1444
1492
  }
1445
1493
  const glyphsByLine = /* @__PURE__ */ new Map();
1446
1494
  let maxIdx = -1;
@@ -1456,8 +1504,8 @@ var MSDFTextEntity = class extends Entity {
1456
1504
  let previousEnd = 0;
1457
1505
  for (let i = 0; i <= maxIdx; i++) {
1458
1506
  const glyphs = glyphsByLine.get(i) ?? [];
1459
- const start = glyphs.length > 0 ? srcIdx[glyphs[0]] : previousEnd;
1460
- const end = glyphs.length > 0 ? srcIdx[glyphs[glyphs.length - 1]] + 1 : start;
1507
+ const start = glyphs.length > 0 ? srcStart[glyphs[0]] : previousEnd;
1508
+ const end = glyphs.length > 0 ? srcEnd[glyphs[glyphs.length - 1]] : start;
1461
1509
  starts.push(start);
1462
1510
  ends.push(end);
1463
1511
  previousEnd = Math.max(previousEnd, end);
@@ -1510,7 +1558,6 @@ var MSDFTextEntity = class extends Entity {
1510
1558
  const code = this.layoutResult.codePoints[i];
1511
1559
  const nodeX = this.layoutResult.xCoords[i];
1512
1560
  const nodeY = this.layoutResult.yCoords[i];
1513
- const packedStyle = this.layoutResult.packedStyles[i];
1514
1561
  const def = this.font.getGlyph(code);
1515
1562
  if (!def || !def.atlasBounds || !def.planeBounds) continue;
1516
1563
  const { atlasBounds: ab, planeBounds: pb } = def;
@@ -1524,15 +1571,6 @@ var MSDFTextEntity = class extends Entity {
1524
1571
  const glyphH = (pb.top - pb.bottom) * this.fontSize * worldScaleY;
1525
1572
  const v0 = this.font.data.atlas.yOrigin === "bottom" ? 1 - ab.top / ah : ab.top / ah;
1526
1573
  const v1 = this.font.data.atlas.yOrigin === "bottom" ? 1 - ab.bottom / ah : ab.bottom / ah;
1527
- const colorVal = packedStyle >>> 8;
1528
- let runColor = this.rgbColorCache.get(colorVal);
1529
- if (!runColor) {
1530
- const r = colorVal >> 16 & 255;
1531
- const g = colorVal >> 8 & 255;
1532
- const b = colorVal & 255;
1533
- runColor = `rgb(${r},${g},${b})`;
1534
- this.rgbColorCache.set(colorVal, runColor);
1535
- }
1536
1574
  scene.pointRenderer.addGlyph(
1537
1575
  glyphX,
1538
1576
  glyphY,
@@ -1542,7 +1580,7 @@ var MSDFTextEntity = class extends Entity {
1542
1580
  v0,
1543
1581
  ab.right / aw,
1544
1582
  v1,
1545
- runColor,
1583
+ this.color,
1546
1584
  worldOpacity,
1547
1585
  worldRot
1548
1586
  );
@@ -1562,16 +1600,7 @@ var MSDFTextEntity = class extends Entity {
1562
1600
  const nodeY = this.layoutResult.yCoords[i];
1563
1601
  const packedStyle = this.layoutResult.packedStyles[i];
1564
1602
  const fontString = this.fontStringCache[packedStyle & 3];
1565
- const colorVal = packedStyle >>> 8;
1566
- let runColor = this.rgbColorCache.get(colorVal);
1567
- if (!runColor) {
1568
- const r = colorVal >> 16 & 255;
1569
- const g = colorVal >> 8 & 255;
1570
- const b = colorVal & 255;
1571
- runColor = `rgb(${r},${g},${b})`;
1572
- this.rgbColorCache.set(colorVal, runColor);
1573
- }
1574
- renderer.fillText(String.fromCodePoint(code), nodeX, nodeY, fontString, runColor);
1603
+ renderer.fillText(String.fromCodePoint(code), nodeX, nodeY, fontString, this.color);
1575
1604
  }
1576
1605
  }
1577
1606
  destroy() {
@@ -1586,11 +1615,32 @@ var SVG_NAMESPACE = "http://www.w3.org/2000/svg";
1586
1615
  function isSvgWhitespace(ch) {
1587
1616
  return ch === " " || ch === " " || ch === "\n" || ch === "\r";
1588
1617
  }
1618
+ function findSvgTagEnd(source, from) {
1619
+ let quote = null;
1620
+ for (let i = from; i < source.length; i++) {
1621
+ const ch = source[i];
1622
+ if (quote !== null) {
1623
+ if (ch === quote) quote = null;
1624
+ } else if (ch === '"' || ch === "'") {
1625
+ quote = ch;
1626
+ } else if (ch === ">") {
1627
+ return i;
1628
+ }
1629
+ }
1630
+ return -1;
1631
+ }
1632
+ function isPercentDimension(value) {
1633
+ return value.trim().endsWith("%");
1634
+ }
1635
+ function viewBoxDimensions(value) {
1636
+ const parts = value.split(/[\s,]+/).map(parseFloat);
1637
+ return parts.length === 4 && parts.every(Number.isFinite) ? { width: parts[2], height: parts[3] } : null;
1638
+ }
1589
1639
  function readSvgAttribute(source, name) {
1590
1640
  const lowerSource = source.toLowerCase();
1591
1641
  const svgStart = lowerSource.indexOf("<svg");
1592
1642
  if (svgStart < 0) return null;
1593
- const tagEnd = source.indexOf(">", svgStart + 4);
1643
+ const tagEnd = findSvgTagEnd(source, svgStart + 4);
1594
1644
  if (tagEnd < 0) return null;
1595
1645
  const tag = source.slice(svgStart + 4, tagEnd);
1596
1646
  const lowerTag = tag.toLowerCase();
@@ -1662,14 +1712,14 @@ var SVGEntity = class extends Entity {
1662
1712
  const wAttr = svgEl.getAttribute("width");
1663
1713
  const hAttr = svgEl.getAttribute("height");
1664
1714
  const vbAttr = svgEl.getAttribute("viewBox");
1665
- if (wAttr && hAttr) {
1715
+ if (wAttr && hAttr && !isPercentDimension(wAttr) && !isPercentDimension(hAttr)) {
1666
1716
  width = parseFloat(wAttr) || 100;
1667
1717
  height = parseFloat(hAttr) || 100;
1668
1718
  } else if (vbAttr) {
1669
- const parts = vbAttr.split(/[\s,]+/).map(parseFloat);
1670
- if (parts.length === 4) {
1671
- width = parts[2];
1672
- height = parts[3];
1719
+ const dims = viewBoxDimensions(vbAttr);
1720
+ if (dims) {
1721
+ width = dims.width;
1722
+ height = dims.height;
1673
1723
  }
1674
1724
  }
1675
1725
  }
@@ -1681,14 +1731,14 @@ var SVGEntity = class extends Entity {
1681
1731
  const wAttr = readSvgAttribute(this.svgSource, "width");
1682
1732
  const hAttr = readSvgAttribute(this.svgSource, "height");
1683
1733
  const vbAttr = readSvgAttribute(this.svgSource, "viewBox");
1684
- if (wAttr && hAttr) {
1734
+ if (wAttr && hAttr && !isPercentDimension(wAttr) && !isPercentDimension(hAttr)) {
1685
1735
  width = parseFloat(wAttr) || 100;
1686
1736
  height = parseFloat(hAttr) || 100;
1687
1737
  } else if (vbAttr) {
1688
- const parts = vbAttr.split(/[\s,]+/).map(parseFloat);
1689
- if (parts.length === 4) {
1690
- width = parts[2];
1691
- height = parts[3];
1738
+ const dims = viewBoxDimensions(vbAttr);
1739
+ if (dims) {
1740
+ width = dims.width;
1741
+ height = dims.height;
1692
1742
  }
1693
1743
  }
1694
1744
  }