@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 +43 -135
- package/dist/{chunk-QVTCKWDO.mjs → chunk-7PYD5UDX.mjs} +93 -43
- package/dist/{chunk-LWEMD34D.mjs → chunk-AISSDI6U.mjs} +132 -33
- package/dist/{chunk-ZYYP2M5D.js → chunk-BKXERWAD.js} +181 -82
- package/dist/{chunk-SRXU3ZXK.js → chunk-POPXUPKR.js} +141 -91
- package/dist/index.js +772 -401
- package/dist/index.mjs +459 -88
- package/dist/renderer/CanvasRenderer.d.ts +6 -0
- package/dist/renderer/SVGRenderer.d.ts +17 -2
- package/dist/renderer/WebGPUParticleSystemManager.d.ts +2 -0
- package/dist/renderer.js +2 -2
- package/dist/renderer.mjs +1 -1
- package/dist/text/MSDFTextEntity.d.ts +0 -1
- package/dist/text.js +2 -2
- package/dist/text.mjs +1 -1
- package/dist/tree/Entity.d.ts +8 -2
- package/dist/tree/Scene.d.ts +123 -0
- package/dist/tree/scene/A11yProjectionManager.d.ts +12 -0
- package/dist/tree/scene/HitTester.d.ts +14 -2
- package/dist/tree/scene/WasmBackendFacade.d.ts +17 -0
- package/dist/tree/scene/keyboard.d.ts +56 -0
- package/dist/wasm/backend.d.ts +7 -7
- package/dist/wasm/vectojs_core.wasm +0 -0
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
# @vectojs/core
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
##
|
|
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
|
-
##
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
scene.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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" ||
|
|
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
|
|
1442
|
-
|
|
1443
|
-
|
|
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 ?
|
|
1460
|
-
const end = glyphs.length > 0 ?
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1670
|
-
if (
|
|
1671
|
-
width =
|
|
1672
|
-
height =
|
|
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
|
|
1689
|
-
if (
|
|
1690
|
-
width =
|
|
1691
|
-
height =
|
|
1738
|
+
const dims = viewBoxDimensions(vbAttr);
|
|
1739
|
+
if (dims) {
|
|
1740
|
+
width = dims.width;
|
|
1741
|
+
height = dims.height;
|
|
1692
1742
|
}
|
|
1693
1743
|
}
|
|
1694
1744
|
}
|