@vectojs/core 1.38.1 → 1.39.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,18 +1,12 @@
1
1
  # @vectojs/core
2
2
 
3
- > Scene, layout, interaction, text, rendering, and semantic projection for canvas-native interfaces.
4
-
5
- [![npm](https://img.shields.io/npm/v/@vectojs/core?color=22d3ee)](https://www.npmjs.com/package/@vectojs/core)
6
- [![CI](https://github.com/vectojs/vectojs/actions/workflows/ci.yml/badge.svg)](https://github.com/vectojs/vectojs/actions/workflows/ci.yml)
7
- [![MIT](https://img.shields.io/badge/license-MIT-6366f1.svg)](https://github.com/vectojs/vectojs/blob/main/LICENSE)
8
-
9
- `@vectojs/core` is the runtime beneath VectoJS. It owns the retained `Scene`/`Entity` tree, affine
10
- transforms, render scheduling, layout, text flow, spatial hit-testing, event propagation, renderer
11
- backends, and the accessibility/automation projection layer.
12
-
13
- [Core guide](https://vectojs.org/learn/core-scene/) ·
14
- [API reference](https://vectojs.org/reference/core-api/) ·
15
- [Main repository](https://github.com/vectojs/vectojs)
3
+ `@vectojs/core` is the runtime at the root of the VectoJS dependency graph: a retained
4
+ `Scene`/`Entity` scene graph (the Virtual Math Tree) that renders to a single canvas, with
5
+ transforms, render scheduling, spatial hit-testing, DOM-like event propagation, layout and text
6
+ engines, Canvas/SVG/WebGL/WebGPU renderer backends, and the accessibility/automation projection
7
+ layer. It is also the composition point of the framework — it depends on and re-exports
8
+ `@vectojs/layout`, `@vectojs/text`, `@vectojs/math`, and `@vectojs/animation`, so `@vectojs/ui`
9
+ and every higher-level package build directly on it.
16
10
 
17
11
  ## Install
18
12
 
@@ -20,7 +14,7 @@ backends, and the accessibility/automation projection layer.
20
14
  bun add @vectojs/core
21
15
  ```
22
16
 
23
- ## Minimal scene
17
+ ## Usage
24
18
 
25
19
  ```ts
26
20
  import { Entity, type IRenderer, Scene } from '@vectojs/core';
@@ -57,124 +51,38 @@ scene.add(new Dot().setPosition(80, 80));
57
51
  scene.start();
58
52
  ```
59
53
 
60
- ## Runtime building blocks
61
-
62
- | Area | Main APIs | Purpose |
63
- | ----------- | ----------------------------------------------- | ---------------------------------------------------------------------- |
64
- | Scene graph | `Scene`, `Entity` | ownership, transforms, lifecycle, update/render traversal |
65
- | Layout | `LayoutEngine`, layout subpath | prepared text/rich-text layout, wrapping, exclusions, reusable buffers |
66
- | Interaction | entity events, `SpatialHashGrid` | hit-testing and DOM-like capture/bubble dispatch |
67
- | Text | `TextEntity`, `MSDFTextEntity`, `SplineEntity` | Canvas text, GPU text, and mathematical curves |
68
- | Rendering | `IRenderer`, `CanvasRenderer`, renderer subpath | backend-neutral drawing contract and concrete renderers |
69
- | GPU paths | WebGL point batching, WebGPU particles | high-volume points/rects and compute-driven particles |
70
- | Semantics | `A11yAttributes`, Scene projection | role/name/state, native inputs, screen readers, Playwright/agents |
71
- | Animation | `animate`, springs, transitions, `Scene.step()` | real-time or deterministic fixed-step motion |
72
-
73
- ## Scene lifecycle
74
-
75
- ```ts
76
- const scene = new Scene(canvas, {
77
- maxFPS: 60,
78
- pointBackend: 'canvas', // or 'webgl'
79
- particleBackend: 'cpu', // or 'webgpu' when supported
80
- });
81
- scene.renderMode = 'onDemand'; // redraw only when dirty
82
-
83
- scene.resize(width, height); // logical CSS pixels
84
- scene.markDirty();
85
- scene.start();
86
- scene.stop();
87
- scene.step(1000 / 60); // deterministic single step
88
- scene.destroy(); // release renderers, workers, observers, and semantic DOM
89
- ```
90
-
91
- Always call `destroy()` when a framework component unmounts. A `Scene` owns browser observers,
92
- renderer resources, layout work, and projected DOM nodes.
93
-
94
- ## Package entry points
95
-
96
- ```ts
97
- import { Scene, Entity } from '@vectojs/core';
98
- import { LayoutEngine } from '@vectojs/core/layout';
99
- import type { IRenderer } from '@vectojs/core/renderer';
100
- import { TextEntity } from '@vectojs/core/text';
101
- ```
102
-
103
- Both ESM and CommonJS outputs are published with TypeScript declarations.
104
-
105
- ## Accessibility and automation
106
-
107
- Canvas pixels have no semantics. Interactive entities can implement `getA11yAttributes()`; the
108
- Scene projects transparent DOM nodes over their world-space bounds and forwards native input back
109
- into the VectoJS event system.
110
-
111
- This projection is intentionally thin. Applications still own accessible names, keyboard behavior,
112
- focus order, contrast, and correct control semantics. See the
113
- [accessibility guide](https://vectojs.org/learn/accessibility/).
114
-
115
- Non-control workspaces that own keyboard shortcuts can opt into focus order
116
- explicitly: return `{ role: 'region', label: 'Canvas workspace', tabIndex: 0 }`.
117
- The Scene refreshes the projected `tabindex` when attributes change. Keep native
118
- text inputs and editable content in charge of their own editing shortcuts.
119
-
120
- Interactive projected nodes capture the active pointer on `pointerdown` and route
121
- `pointermove`, `pointerup`, and `pointercancel` through normal VMT capture/bubble propagation.
122
- Treat `pointercancel` as rollback: discard transient gesture state and do not create durable
123
- history. Pointer capture is released safely on both completion paths.
124
-
125
- ## Static content projection
126
-
127
- Canvas-rendered text can opt into browser-native find, translation, selection, and copy without
128
- turning the application into a DOM layout. Override `getContentProjection()` on the owning Entity;
129
- the Scene creates a transparent, position-synchronized mirror while the VMT remains authoritative:
130
-
131
- ```ts
132
- class SelectableLabel extends Entity {
133
- getContentProjection() {
134
- return {
135
- text: 'Selectable canvas text',
136
- font: '18px Inter',
137
- lineHeight: 24,
138
- selectable: true,
139
- };
140
- }
141
- }
142
- ```
143
-
144
- `selectable` controls whether the mirror receives pointer input. The Scene keeps projection order
145
- aligned with VMT order, removes descendant mirrors with their subtree, and hides mirrors fully
146
- outside the viewport or a `clipChildren` ancestor. Tooling can inspect the currently materialized
147
- node with `scene.getContentElement(entityId)`. Virtualized or non-materialized off-viewport text is
148
- not searchable until the application brings it into the active scene.
149
-
150
- Code-like renderers can compile their logical source once with `prepareContentGrid()` and return the
151
- same immutable plan as `ContentProjection.grid`. The plan retains UTF-16 source ranges, legal
152
- grapheme carets, CR/LF ownership, tab stops, wide CJK/emoji advances, Arabic shaping, and Unicode
153
- bidi positions. Scene projects those cells in logical source order, performs font calibration in a
154
- cold offscreen batch, and uses the plan's local geometry for pointer selection even when the entity
155
- is rotated, scaled, or the page is zoomed.
156
-
157
- Selection routing preserves forward/reverse drag direction, Shift extension, word and line
158
- selection, and exact clipboard source. Projection rebuild/removal/destroy paths release active
159
- selection and pending calibration ownership before replacing DOM carriers.
160
-
161
- ## Performance model
162
-
163
- Useful levers include on-demand rendering, viewport culling, spatial hashing, prepared text layout,
164
- typed reusable buffers, batched WebGL points, and optional WebGPU particle compute. None makes every
165
- workload allocation-free or GPU-bound; profile the renderer and entity types used by your app.
166
-
167
- Run the repository benchmarks with `bun run benchmark`, `bun run compare:dom`, and
168
- `bun run compare`. Prepared-grid scaling can be measured independently with
169
- `bun run --cwd packages/core benchmark:grid`; benchmark timing is release evidence rather than a
170
- wall-clock CI gate.
171
-
172
- ## Related packages
173
-
174
- - [`@vectojs/ui`](https://github.com/vectojs/vectojs/tree/main/packages/ui) — high-level accessible components
175
- - [`@vectojs/three`](https://github.com/vectojs/vectojs/tree/main/packages/three) — Three.js/WebXR projection and raycast routing
176
- - [`@vectojs/video-exporter`](https://github.com/vectojs/vectojs/tree/main/packages/video-exporter) — deterministic H.264 capture
177
-
178
- ## License
179
-
180
- [MIT](https://github.com/vectojs/vectojs/blob/main/LICENSE) © 2026 Xuepoo
54
+ ## Highlights
55
+
56
+ - Retained `Scene`/`Entity` tree with affine transforms, capture/bubble event dispatch,
57
+ viewport culling, and dirty-flag `renderMode: 'onDemand'` rendering; `scene.step(dt)` drives a
58
+ deterministic frame for tests and video export.
59
+ - Backend-neutral `IRenderer` drawing contract with modular backends: `CanvasRenderer`,
60
+ `SVGRenderer`, batched WebGL points (`WebGLPointRenderer`), and WebGPU particle compute
61
+ (`WebGPUParticleSystemManager`) registered on load via `Scene.register*`, selected through
62
+ `pointBackend` / `particleBackend` options.
63
+ - Optional Rust WASM kernels (`crates/vectojs-core-rs`) hot-swapped per subsystem with
64
+ `scene.enableWasmTransforms / enableWasmHitTest / enableWasmAnimBatching / enableWasmParticles`;
65
+ fallible exports return `WASM_STATUS` codes (`OK`/`CAPACITY`/`UNINITIALIZED`/`BAD_RUN`/
66
+ `OVERFLOW`) so any rejected batch degrades to the JS path instead of rendering half-written state.
67
+ - Semantic accessibility projection: entities implementing `getA11yAttributes()` get transparent,
68
+ position-synchronized DOM mirrors for screen readers, keyboard users, Playwright, and AI agents;
69
+ static text opts into browser-native selection/find/copy through `getContentProjection()` and
70
+ Core's prepared content grid (`prepareContentGrid()`).
71
+ - Entity-based text renderers stay here because they extend `Entity`: `TextEntity`,
72
+ `GridTextEntity`, GPU-resolved `MSDFTextEntity` (off-thread layout via `LayoutWorkerManager`),
73
+ `SVGEntity`, and `DOMPortalEntity`.
74
+ - The standalone engines are re-exported from this barrel and remain available as subpaths, so
75
+ existing imports keep working:
76
+ `@vectojs/core/layout`, `@vectojs/core/text`, `@vectojs/core/renderer`.
77
+ - Lifecycle ownership is explicit: a `Scene` owns renderers, workers, observers, and projected DOM
78
+ nodes; `scene.destroy()` releases all of them.
79
+
80
+ > Documents @vectojs/core@1.39.0.
81
+
82
+ ## Documentation
83
+
84
+ - [Core Scene architecture](https://vectojs.org/learn/core-scene/)
85
+ - [`@vectojs/core` API reference](https://vectojs.org/reference/core-api/)
86
+ - [`Entity` reference](https://vectojs.org/reference/core-entity/)
87
+ - [Renderers reference](https://vectojs.org/reference/core-renderer/)
88
+ - [Accessibility & automation guide](https://vectojs.org/learn/accessibility/)
@@ -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
  }