@vectojs/core 0.2.2 → 0.2.4
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 +110 -34
- package/dist/{chunk-RW6NC4RB.js → chunk-76NMTLYL.js} +56 -14
- package/dist/{chunk-YA2J5ZH7.mjs → chunk-B3Z3JEJH.mjs} +50 -8
- package/dist/{chunk-72WVPMSJ.js → chunk-IKLYTHAL.js} +8 -8
- package/dist/{chunk-T456DL4P.mjs → chunk-MCULB5VC.mjs} +1 -1
- package/dist/{chunk-H3QIE77O.mjs → chunk-P6MDWIEX.mjs} +216 -61
- package/dist/{chunk-LIX7DJTI.js → chunk-TPNADFNN.js} +143 -15
- package/dist/{chunk-LA3FJLP2.js → chunk-TQ4H357Q.js} +226 -71
- package/dist/{chunk-2Y45S4JK.mjs → chunk-WEQRBXT2.mjs} +142 -14
- package/dist/index.d.ts +1 -0
- package/dist/index.js +185 -119
- package/dist/index.mjs +147 -81
- package/dist/layout/LayoutWorkerManager.d.ts +4 -0
- package/dist/layout/LayoutWorkerSource.d.ts +1 -1
- package/dist/layout.js +3 -3
- package/dist/layout.mjs +2 -2
- package/dist/renderer/CanvasRenderer.d.ts +6 -0
- package/dist/renderer/IRenderer.d.ts +9 -0
- package/dist/renderer/SVGRenderer.d.ts +14 -0
- package/dist/renderer/url.d.ts +31 -0
- package/dist/renderer.js +2 -2
- package/dist/renderer.mjs +1 -1
- package/dist/text.js +3 -3
- package/dist/text.mjs +2 -2
- package/dist/tree/DOMPortalEntity.d.ts +2 -1
- package/dist/tree/Entity.d.ts +48 -7
- package/dist/tree/Scene.d.ts +9 -1
- package/package.json +2 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
LayoutWorkerManager
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-B3Z3JEJH.mjs";
|
|
4
4
|
|
|
5
5
|
// src/math/SpringPhysics.ts
|
|
6
6
|
var SpringPhysics = class {
|
|
@@ -128,14 +128,16 @@ var VectoJSEvent = class {
|
|
|
128
128
|
nativeEvent;
|
|
129
129
|
/** Whether the event bubbles past its target (capture always runs). */
|
|
130
130
|
bubbles;
|
|
131
|
+
explicitScenePoint;
|
|
131
132
|
stopped = false;
|
|
132
133
|
stoppedImmediate = false;
|
|
133
|
-
constructor(type, target, nativeEvent, bubbles = true) {
|
|
134
|
+
constructor(type, target, nativeEvent, bubbles = true, scenePoint) {
|
|
134
135
|
this.type = type;
|
|
135
136
|
this.target = target;
|
|
136
137
|
this.currentTarget = target;
|
|
137
138
|
this.nativeEvent = nativeEvent;
|
|
138
139
|
this.bubbles = bubbles;
|
|
140
|
+
this.explicitScenePoint = scenePoint;
|
|
139
141
|
}
|
|
140
142
|
/** Stop the event from reaching the next node in the propagation path. */
|
|
141
143
|
stopPropagation() {
|
|
@@ -178,6 +180,51 @@ var VectoJSEvent = class {
|
|
|
178
180
|
get clientY() {
|
|
179
181
|
return this.nativeEvent?.clientY;
|
|
180
182
|
}
|
|
183
|
+
get resolvedScenePoint() {
|
|
184
|
+
if (this.explicitScenePoint) return this.explicitScenePoint;
|
|
185
|
+
const native = this.nativeEvent;
|
|
186
|
+
if (native?.vectoSceneX !== void 0 && native.vectoSceneY !== void 0) {
|
|
187
|
+
return { x: native.vectoSceneX, y: native.vectoSceneY };
|
|
188
|
+
}
|
|
189
|
+
if (native?.clientX === void 0 || native.clientY === void 0) return void 0;
|
|
190
|
+
const scene = this.target.scene;
|
|
191
|
+
return scene?.clientToScene?.(native.clientX, native.clientY) ?? {
|
|
192
|
+
x: native.clientX,
|
|
193
|
+
y: native.clientY
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/** Pointer X in the Scene's logical coordinate space. */
|
|
197
|
+
get sceneX() {
|
|
198
|
+
return this.resolvedScenePoint?.x;
|
|
199
|
+
}
|
|
200
|
+
/** Pointer Y in the Scene's logical coordinate space. */
|
|
201
|
+
get sceneY() {
|
|
202
|
+
return this.resolvedScenePoint?.y;
|
|
203
|
+
}
|
|
204
|
+
/** Pointer X local to the entity whose listener is currently running. */
|
|
205
|
+
get localX() {
|
|
206
|
+
const point = this.resolvedScenePoint;
|
|
207
|
+
if (!point) return void 0;
|
|
208
|
+
return this.currentTarget.worldToLocal(point.x, point.y)?.x;
|
|
209
|
+
}
|
|
210
|
+
/** Pointer Y local to the entity whose listener is currently running. */
|
|
211
|
+
get localY() {
|
|
212
|
+
const point = this.resolvedScenePoint;
|
|
213
|
+
if (!point) return void 0;
|
|
214
|
+
return this.currentTarget.worldToLocal(point.x, point.y)?.y;
|
|
215
|
+
}
|
|
216
|
+
get shiftKey() {
|
|
217
|
+
return !!this.nativeEvent?.shiftKey;
|
|
218
|
+
}
|
|
219
|
+
get ctrlKey() {
|
|
220
|
+
return !!this.nativeEvent?.ctrlKey;
|
|
221
|
+
}
|
|
222
|
+
get altKey() {
|
|
223
|
+
return !!this.nativeEvent?.altKey;
|
|
224
|
+
}
|
|
225
|
+
get metaKey() {
|
|
226
|
+
return !!this.nativeEvent?.metaKey;
|
|
227
|
+
}
|
|
181
228
|
/** Native key, if this wraps a keyboard event. */
|
|
182
229
|
get key() {
|
|
183
230
|
return this.nativeEvent?.key;
|
|
@@ -412,17 +459,28 @@ var Entity = class {
|
|
|
412
459
|
* that need to seed a starting state (e.g. the presence helper's enter `from`).
|
|
413
460
|
*/
|
|
414
461
|
setImmediate(prop, v) {
|
|
462
|
+
const existing = this._drivers.get(prop);
|
|
463
|
+
if (existing) this._settleDriver(existing);
|
|
415
464
|
this._drivers.delete(prop);
|
|
416
465
|
this._applyAnimated(prop, v);
|
|
417
466
|
}
|
|
467
|
+
_settleDriver(driver) {
|
|
468
|
+
const active = driver;
|
|
469
|
+
const onDone = active.onDone;
|
|
470
|
+
active.onDone = void 0;
|
|
471
|
+
onDone?.();
|
|
472
|
+
}
|
|
418
473
|
_spawnDriver(prop, to, cfg) {
|
|
419
474
|
if (prop !== "opacity" && this.scene?.prefersReducedMotion) {
|
|
475
|
+
const existing2 = this._drivers.get(prop);
|
|
476
|
+
if (existing2) this._settleDriver(existing2);
|
|
420
477
|
this._drivers.delete(prop);
|
|
421
478
|
this._applyAnimated(prop, to);
|
|
422
479
|
return;
|
|
423
480
|
}
|
|
424
481
|
const existing = this._drivers.get(prop);
|
|
425
482
|
if (existing) {
|
|
483
|
+
this._settleDriver(existing);
|
|
426
484
|
existing.retarget(to);
|
|
427
485
|
return;
|
|
428
486
|
}
|
|
@@ -477,7 +535,7 @@ var Entity = class {
|
|
|
477
535
|
driver.tick(dt);
|
|
478
536
|
if (driver.isDone()) {
|
|
479
537
|
this._applyAnimated(prop, driver.target);
|
|
480
|
-
|
|
538
|
+
this._settleDriver(driver);
|
|
481
539
|
this._drivers.delete(prop);
|
|
482
540
|
} else {
|
|
483
541
|
this._applyAnimated(prop, driver.value);
|
|
@@ -621,19 +679,95 @@ var Entity = class {
|
|
|
621
679
|
* @returns World-space {@link Point} for this entity.
|
|
622
680
|
*/
|
|
623
681
|
getGlobalPosition() {
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
682
|
+
return this.localToWorld(0, 0);
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Return the exact accumulated Canvas `T * S * R` transform for this entity.
|
|
686
|
+
*/
|
|
687
|
+
getWorldTransform() {
|
|
688
|
+
const path = this.id === "root" ? [] : [this];
|
|
689
|
+
let ancestor = this.parent;
|
|
690
|
+
while (ancestor && ancestor.id !== "root") {
|
|
691
|
+
path.push(ancestor);
|
|
692
|
+
ancestor = ancestor.parent;
|
|
635
693
|
}
|
|
636
|
-
|
|
694
|
+
let a = 1;
|
|
695
|
+
let b = 0;
|
|
696
|
+
let c = 0;
|
|
697
|
+
let d = 1;
|
|
698
|
+
let e = 0;
|
|
699
|
+
let f = 0;
|
|
700
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
701
|
+
const node = path[i];
|
|
702
|
+
const cos = Math.cos(node.rotation);
|
|
703
|
+
const sin = Math.sin(node.rotation);
|
|
704
|
+
const la = node.scaleX * cos;
|
|
705
|
+
const lb = node.scaleY * sin;
|
|
706
|
+
const lc = -node.scaleX * sin;
|
|
707
|
+
const ld = node.scaleY * cos;
|
|
708
|
+
const le = node.x;
|
|
709
|
+
const lf = node.y;
|
|
710
|
+
const nextA = a * la + c * lb;
|
|
711
|
+
const nextB = b * la + d * lb;
|
|
712
|
+
const nextC = a * lc + c * ld;
|
|
713
|
+
const nextD = b * lc + d * ld;
|
|
714
|
+
const nextE = a * le + c * lf + e;
|
|
715
|
+
const nextF = b * le + d * lf + f;
|
|
716
|
+
a = nextA;
|
|
717
|
+
b = nextB;
|
|
718
|
+
c = nextC;
|
|
719
|
+
d = nextD;
|
|
720
|
+
e = nextE;
|
|
721
|
+
f = nextF;
|
|
722
|
+
}
|
|
723
|
+
return { a, b, c, d, e, f };
|
|
724
|
+
}
|
|
725
|
+
/** Convert a point from this entity's local space to Scene/world space. */
|
|
726
|
+
localToWorld(localX, localY) {
|
|
727
|
+
const { a, b, c, d, e, f } = this.getWorldTransform();
|
|
728
|
+
return {
|
|
729
|
+
x: a * localX + c * localY + e,
|
|
730
|
+
y: b * localX + d * localY + f
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Convert a Scene/world point into this entity's local space.
|
|
735
|
+
* Returns `null` when the accumulated transform is singular.
|
|
736
|
+
*/
|
|
737
|
+
worldToLocal(worldX, worldY) {
|
|
738
|
+
const { a, b, c, d, e, f } = this.getWorldTransform();
|
|
739
|
+
const determinant = a * d - b * c;
|
|
740
|
+
if (!Number.isFinite(determinant) || Math.abs(determinant) < 1e-12) return null;
|
|
741
|
+
const x = worldX - e;
|
|
742
|
+
const y = worldY - f;
|
|
743
|
+
return {
|
|
744
|
+
x: (d * x - c * y) / determinant,
|
|
745
|
+
y: (-b * x + a * y) / determinant
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Return the entity's local bounds transformed into a world-space AABB.
|
|
750
|
+
* Falls back to the entity's `[0, 0, width, height]` box when `getBounds()`
|
|
751
|
+
* does not provide a render-specific box.
|
|
752
|
+
*/
|
|
753
|
+
getWorldBounds() {
|
|
754
|
+
const bounds = this.getBounds() ?? { x: 0, y: 0, width: this.width, height: this.height };
|
|
755
|
+
const { a, b, c, d, e, f } = this.getWorldTransform();
|
|
756
|
+
let minX = Infinity;
|
|
757
|
+
let minY = Infinity;
|
|
758
|
+
let maxX = -Infinity;
|
|
759
|
+
let maxY = -Infinity;
|
|
760
|
+
for (let i = 0; i < 4; i++) {
|
|
761
|
+
const localX = i & 1 ? bounds.x + bounds.width : bounds.x;
|
|
762
|
+
const localY = i & 2 ? bounds.y + bounds.height : bounds.y;
|
|
763
|
+
const worldX = a * localX + c * localY + e;
|
|
764
|
+
const worldY = b * localX + d * localY + f;
|
|
765
|
+
minX = Math.min(minX, worldX);
|
|
766
|
+
minY = Math.min(minY, worldY);
|
|
767
|
+
maxX = Math.max(maxX, worldX);
|
|
768
|
+
maxY = Math.max(maxY, worldY);
|
|
769
|
+
}
|
|
770
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
637
771
|
}
|
|
638
772
|
/**
|
|
639
773
|
* Accumulated world scale factors: this entity's own `scaleX`/`scaleY` times
|
|
@@ -705,12 +839,12 @@ var Entity = class {
|
|
|
705
839
|
* Opt into the renderer's draw-call batching fast-path for point-cloud /
|
|
706
840
|
* particle entities that draw as a single filled circle at their local origin.
|
|
707
841
|
*
|
|
708
|
-
* When a leaf entity returns a {@link BatchCircle} and
|
|
709
|
-
* {@link Scene}
|
|
710
|
-
* `restore` and
|
|
711
|
-
* {@link
|
|
712
|
-
*
|
|
713
|
-
* frame, so
|
|
842
|
+
* When a leaf entity returns a {@link BatchCircle} and its accumulated
|
|
843
|
+
* transform is representable by the selected batch backend, the {@link Scene}
|
|
844
|
+
* skips its per-entity `save`/`translate`/`scale`/`rotate`/`restore` and
|
|
845
|
+
* {@link render}. Canvas mode or an unsupported affine transform uses the
|
|
846
|
+
* normal render path, so implementations must keep {@link render} correct.
|
|
847
|
+
* Returns `null` by default. Read each frame, so animated color/radius works.
|
|
714
848
|
*
|
|
715
849
|
* @returns The circle to batch, or `null` to use the normal {@link render} path.
|
|
716
850
|
*/
|
|
@@ -893,22 +1027,21 @@ var MSDFTextEntity = class extends Entity {
|
|
|
893
1027
|
}
|
|
894
1028
|
isPointInside(globalX, globalY) {
|
|
895
1029
|
if (!this.layoutResult) return false;
|
|
896
|
-
const
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
const ly = (globalY - pos.y) / scale.y;
|
|
900
|
-
return lx >= 0 && lx <= this.layoutResult.width && ly >= 0 && ly <= this.layoutResult.height;
|
|
1030
|
+
const local = this.worldToLocal(globalX, globalY);
|
|
1031
|
+
if (!local) return false;
|
|
1032
|
+
return local.x >= 0 && local.x <= this.layoutResult.width && local.y >= 0 && local.y <= this.layoutResult.height;
|
|
901
1033
|
}
|
|
902
1034
|
render(renderer) {
|
|
903
1035
|
if (!this.layoutResult) return;
|
|
904
1036
|
const scene = this.scene;
|
|
905
|
-
|
|
1037
|
+
const world = this.getWorldTransform();
|
|
1038
|
+
const worldScaleX = Math.hypot(world.a, world.b);
|
|
1039
|
+
const worldScaleY = Math.hypot(world.c, world.d);
|
|
1040
|
+
const orthogonalTolerance = Math.max(1, worldScaleX * worldScaleY) * 1e-6;
|
|
1041
|
+
const canUsePointGlyphs = Number.isFinite(worldScaleX) && Number.isFinite(worldScaleY) && world.a * world.d - world.b * world.c >= 0 && Math.abs(world.a * world.c + world.b * world.d) <= orthogonalTolerance;
|
|
1042
|
+
if (scene && scene.pointRenderer && scene.glCanvas && canUsePointGlyphs) {
|
|
906
1043
|
scene.pointRenderer.setMSDFTexture(this.texture, this.font.distanceRange);
|
|
907
|
-
const
|
|
908
|
-
const scale = this.getWorldScale();
|
|
909
|
-
const worldRot = this.getWorldRotation();
|
|
910
|
-
const rCos = Math.cos(worldRot);
|
|
911
|
-
const rSin = Math.sin(worldRot);
|
|
1044
|
+
const worldRot = Math.atan2(world.b, world.a);
|
|
912
1045
|
const len2 = this.layoutResult.codePoints.length;
|
|
913
1046
|
for (let i = 0; i < len2; i++) {
|
|
914
1047
|
const code = this.layoutResult.codePoints[i];
|
|
@@ -920,12 +1053,12 @@ var MSDFTextEntity = class extends Entity {
|
|
|
920
1053
|
const { atlasBounds: ab, planeBounds: pb } = def;
|
|
921
1054
|
const aw = this.font.atlasWidth;
|
|
922
1055
|
const ah = this.font.atlasHeight;
|
|
923
|
-
const lx =
|
|
924
|
-
const ly =
|
|
925
|
-
const glyphX =
|
|
926
|
-
const glyphY =
|
|
927
|
-
const glyphW = (pb.right - pb.left) * this.fontSize *
|
|
928
|
-
const glyphH = (pb.top - pb.bottom) * this.fontSize *
|
|
1056
|
+
const lx = nodeX + pb.left * this.fontSize;
|
|
1057
|
+
const ly = nodeY - pb.top * this.fontSize;
|
|
1058
|
+
const glyphX = world.a * lx + world.c * ly + world.e;
|
|
1059
|
+
const glyphY = world.b * lx + world.d * ly + world.f;
|
|
1060
|
+
const glyphW = (pb.right - pb.left) * this.fontSize * worldScaleX;
|
|
1061
|
+
const glyphH = (pb.top - pb.bottom) * this.fontSize * worldScaleY;
|
|
929
1062
|
const v0 = this.font.data.atlas.yOrigin === "bottom" ? 1 - ab.top / ah : ab.top / ah;
|
|
930
1063
|
const v1 = this.font.data.atlas.yOrigin === "bottom" ? 1 - ab.bottom / ah : ab.bottom / ah;
|
|
931
1064
|
const colorVal = packedStyle >>> 8;
|
|
@@ -985,6 +1118,36 @@ var MSDFTextEntity = class extends Entity {
|
|
|
985
1118
|
};
|
|
986
1119
|
|
|
987
1120
|
// src/text/SVGEntity.ts
|
|
1121
|
+
function isSvgWhitespace(ch) {
|
|
1122
|
+
return ch === " " || ch === " " || ch === "\n" || ch === "\r";
|
|
1123
|
+
}
|
|
1124
|
+
function readSvgAttribute(source, name) {
|
|
1125
|
+
const lowerSource = source.toLowerCase();
|
|
1126
|
+
const svgStart = lowerSource.indexOf("<svg");
|
|
1127
|
+
if (svgStart < 0) return null;
|
|
1128
|
+
const tagEnd = source.indexOf(">", svgStart + 4);
|
|
1129
|
+
if (tagEnd < 0) return null;
|
|
1130
|
+
const tag = source.slice(svgStart + 4, tagEnd);
|
|
1131
|
+
const lowerTag = tag.toLowerCase();
|
|
1132
|
+
const lowerName = name.toLowerCase();
|
|
1133
|
+
for (let i = 0; i < tag.length; i++) {
|
|
1134
|
+
const before = i === 0 ? " " : tag[i - 1];
|
|
1135
|
+
if (!isSvgWhitespace(before)) continue;
|
|
1136
|
+
if (!lowerTag.startsWith(lowerName, i)) continue;
|
|
1137
|
+
let cursor = i + lowerName.length;
|
|
1138
|
+
while (cursor < tag.length && isSvgWhitespace(tag[cursor])) cursor++;
|
|
1139
|
+
if (tag[cursor] !== "=") continue;
|
|
1140
|
+
cursor++;
|
|
1141
|
+
while (cursor < tag.length && isSvgWhitespace(tag[cursor])) cursor++;
|
|
1142
|
+
const quote = tag[cursor];
|
|
1143
|
+
if (quote !== '"' && quote !== "'") continue;
|
|
1144
|
+
const valueStart = cursor + 1;
|
|
1145
|
+
const valueEnd = tag.indexOf(quote, valueStart);
|
|
1146
|
+
if (valueEnd < 0) return null;
|
|
1147
|
+
return tag.slice(valueStart, valueEnd);
|
|
1148
|
+
}
|
|
1149
|
+
return null;
|
|
1150
|
+
}
|
|
988
1151
|
var SVGEntity = class extends Entity {
|
|
989
1152
|
svgSource = "";
|
|
990
1153
|
imageBitmap = null;
|
|
@@ -1036,17 +1199,17 @@ var SVGEntity = class extends Entity {
|
|
|
1036
1199
|
}
|
|
1037
1200
|
}
|
|
1038
1201
|
} catch (e) {
|
|
1039
|
-
console.error("Failed parsing SVG via DOMParser, falling back to
|
|
1202
|
+
console.error("Failed parsing SVG via DOMParser, falling back to attribute scan:", e);
|
|
1040
1203
|
}
|
|
1041
1204
|
} else {
|
|
1042
|
-
const
|
|
1043
|
-
const
|
|
1044
|
-
const
|
|
1045
|
-
if (
|
|
1046
|
-
width = parseFloat(
|
|
1047
|
-
height = parseFloat(
|
|
1048
|
-
} else if (
|
|
1049
|
-
const parts =
|
|
1205
|
+
const wAttr = readSvgAttribute(this.svgSource, "width");
|
|
1206
|
+
const hAttr = readSvgAttribute(this.svgSource, "height");
|
|
1207
|
+
const vbAttr = readSvgAttribute(this.svgSource, "viewBox");
|
|
1208
|
+
if (wAttr && hAttr) {
|
|
1209
|
+
width = parseFloat(wAttr) || 100;
|
|
1210
|
+
height = parseFloat(hAttr) || 100;
|
|
1211
|
+
} else if (vbAttr) {
|
|
1212
|
+
const parts = vbAttr.split(/[\s,]+/).map(parseFloat);
|
|
1050
1213
|
if (parts.length === 4) {
|
|
1051
1214
|
width = parts[2];
|
|
1052
1215
|
height = parts[3];
|
|
@@ -1138,22 +1301,14 @@ var SVGEntity = class extends Entity {
|
|
|
1138
1301
|
img.src = this.blobURL;
|
|
1139
1302
|
}
|
|
1140
1303
|
isPointInside(globalX, globalY) {
|
|
1141
|
-
const
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
const dx = globalX - pos.x;
|
|
1145
|
-
const dy = globalY - pos.y;
|
|
1146
|
-
const cos = Math.cos(-rot);
|
|
1147
|
-
const sin = Math.sin(-rot);
|
|
1148
|
-
const lx = (dx * cos - dy * sin) / scale.x;
|
|
1149
|
-
const ly = (dx * sin + dy * cos) / scale.y;
|
|
1150
|
-
return lx >= 0 && lx <= this.width && ly >= 0 && ly <= this.height;
|
|
1304
|
+
const local = this.worldToLocal(globalX, globalY);
|
|
1305
|
+
if (!local) return false;
|
|
1306
|
+
return local.x >= 0 && local.x <= this.width && local.y >= 0 && local.y <= this.height;
|
|
1151
1307
|
}
|
|
1152
1308
|
render(r) {
|
|
1153
|
-
const
|
|
1154
|
-
if (
|
|
1155
|
-
|
|
1156
|
-
r.drawImage({ src: dataUri }, 0, 0, this.width, this.height);
|
|
1309
|
+
const svgRenderer = r;
|
|
1310
|
+
if (typeof svgRenderer.drawSVG === "function") {
|
|
1311
|
+
svgRenderer.drawSVG(this.svgSource, 0, 0, this.width, this.height);
|
|
1157
1312
|
return;
|
|
1158
1313
|
}
|
|
1159
1314
|
const scale = this.getWorldScale();
|
|
@@ -185,9 +185,100 @@ var CanvasRenderer = (_class = class _CanvasRenderer {
|
|
|
185
185
|
}
|
|
186
186
|
return grad;
|
|
187
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Canvas2D drawing contexts are automatically released when their
|
|
190
|
+
* `<canvas>` element is GC'd, so there's no explicit GPU handle to free.
|
|
191
|
+
* This method clears our internal batch state and is idempotent.
|
|
192
|
+
*/
|
|
193
|
+
dispose() {
|
|
194
|
+
this.batchCount = 0;
|
|
195
|
+
this.batchColor = "";
|
|
196
|
+
this.batchAlpha = 1;
|
|
197
|
+
this.batchActive = false;
|
|
198
|
+
}
|
|
188
199
|
}, _class.__initStatic(), _class);
|
|
189
200
|
|
|
201
|
+
// src/renderer/url.ts
|
|
202
|
+
var SAFE_SCHEMES = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:", "ftp:"]);
|
|
203
|
+
var SCHEME_PATTERN = /^[a-z][a-z\d+.-]*$/i;
|
|
204
|
+
function normalizeSchemeCandidate(value) {
|
|
205
|
+
let result = "";
|
|
206
|
+
for (const character of value) {
|
|
207
|
+
const code = character.charCodeAt(0);
|
|
208
|
+
if (code > 32 && (code < 127 || code > 159)) result += character;
|
|
209
|
+
}
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
function hasSafeSchemeOrIsRelative(url) {
|
|
213
|
+
const colonIndex = url.indexOf(":");
|
|
214
|
+
if (colonIndex < 0) return true;
|
|
215
|
+
const candidate = normalizeSchemeCandidate(url.slice(0, colonIndex));
|
|
216
|
+
if (!SCHEME_PATTERN.test(candidate)) return true;
|
|
217
|
+
return SAFE_SCHEMES.has(`${candidate.toLowerCase()}:`);
|
|
218
|
+
}
|
|
219
|
+
function sanitizeUrl(href) {
|
|
220
|
+
if (typeof href !== "string") return "";
|
|
221
|
+
const trimmed = href.trim();
|
|
222
|
+
if (trimmed === "") return "";
|
|
223
|
+
return hasSafeSchemeOrIsRelative(trimmed) ? trimmed : "#";
|
|
224
|
+
}
|
|
225
|
+
function isSafeUrl(urlStr) {
|
|
226
|
+
if (typeof urlStr !== "string") return false;
|
|
227
|
+
const trimmed = urlStr.trim();
|
|
228
|
+
if (trimmed === "") return false;
|
|
229
|
+
return hasSafeSchemeOrIsRelative(trimmed);
|
|
230
|
+
}
|
|
231
|
+
|
|
190
232
|
// src/renderer/SVGRenderer.ts
|
|
233
|
+
function parseFontSizeToken(font) {
|
|
234
|
+
for (let i = 0; i < font.length; i++) {
|
|
235
|
+
const ch = font[i];
|
|
236
|
+
if (!(ch >= "0" && ch <= "9" || ch === ".")) continue;
|
|
237
|
+
let j = i + 1;
|
|
238
|
+
while (j < font.length) {
|
|
239
|
+
const next = font[j];
|
|
240
|
+
if (next >= "0" && next <= "9" || next === ".") {
|
|
241
|
+
j++;
|
|
242
|
+
} else {
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const unit = font.startsWith("rem", j) ? "rem" : font.startsWith("em", j) ? "em" : font.startsWith("px", j) ? "px" : null;
|
|
247
|
+
if (!unit) {
|
|
248
|
+
i = j;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
let end = j + unit.length;
|
|
252
|
+
if (font[end] === "/") {
|
|
253
|
+
end++;
|
|
254
|
+
while (end < font.length && font[end] !== " " && font[end] !== " ") end++;
|
|
255
|
+
}
|
|
256
|
+
const value = Number.parseFloat(font.slice(i, j));
|
|
257
|
+
return Number.isFinite(value) ? { value, unit, start: i, end } : null;
|
|
258
|
+
}
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
function fontFamilyFromShorthand(font, sizeToken) {
|
|
262
|
+
const withoutSize = sizeToken ? `${font.slice(0, sizeToken.start)} ${font.slice(sizeToken.end)}`.trim() : font.trim();
|
|
263
|
+
const family = withoutSize.split(" ").map((part) => part.trim()).filter(
|
|
264
|
+
(part) => part && ![
|
|
265
|
+
"bold",
|
|
266
|
+
"italic",
|
|
267
|
+
"oblique",
|
|
268
|
+
"normal",
|
|
269
|
+
"900",
|
|
270
|
+
"800",
|
|
271
|
+
"700",
|
|
272
|
+
"600",
|
|
273
|
+
"500",
|
|
274
|
+
"400",
|
|
275
|
+
"300",
|
|
276
|
+
"200",
|
|
277
|
+
"100"
|
|
278
|
+
].includes(part.toLowerCase())
|
|
279
|
+
).join(" ");
|
|
280
|
+
return family || "sans-serif";
|
|
281
|
+
}
|
|
191
282
|
var SVGRenderer = (_class2 = class {
|
|
192
283
|
|
|
193
284
|
|
|
@@ -395,7 +486,7 @@ var SVGRenderer = (_class2 = class {
|
|
|
395
486
|
}
|
|
396
487
|
fill(colorOrGradient) {
|
|
397
488
|
this.flush();
|
|
398
|
-
const fillVal = this.resolveGradient(colorOrGradient);
|
|
489
|
+
const fillVal = this.escapeXML(this.resolveGradient(colorOrGradient));
|
|
399
490
|
const dStr = this.currentPath.join(" ");
|
|
400
491
|
const transformStr = `matrix(${this.ma},${this.mb},${this.mc},${this.md},${this.me},${this.mf})`;
|
|
401
492
|
this.buffer.push(
|
|
@@ -404,7 +495,7 @@ var SVGRenderer = (_class2 = class {
|
|
|
404
495
|
}
|
|
405
496
|
stroke(colorOrGradient, lineWidth = 1) {
|
|
406
497
|
this.flush();
|
|
407
|
-
const strokeVal = this.resolveGradient(colorOrGradient);
|
|
498
|
+
const strokeVal = this.escapeXML(this.resolveGradient(colorOrGradient));
|
|
408
499
|
const dStr = this.currentPath.join(" ");
|
|
409
500
|
const transformStr = `matrix(${this.ma},${this.mb},${this.mc},${this.md},${this.me},${this.mf})`;
|
|
410
501
|
this.buffer.push(
|
|
@@ -413,18 +504,18 @@ var SVGRenderer = (_class2 = class {
|
|
|
413
504
|
}
|
|
414
505
|
fillText(text, x, y, font, color) {
|
|
415
506
|
this.flush();
|
|
416
|
-
const
|
|
417
|
-
let fontSize =
|
|
418
|
-
if (
|
|
507
|
+
const sizeToken = parseFontSizeToken(font);
|
|
508
|
+
let fontSize = sizeToken ? sizeToken.value : 16;
|
|
509
|
+
if (sizeToken && sizeToken.unit !== "px") {
|
|
419
510
|
fontSize = fontSize * 16;
|
|
420
511
|
}
|
|
421
|
-
const
|
|
422
|
-
const
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const fontFamily =
|
|
427
|
-
const fillVal = this.resolveGradient(color);
|
|
512
|
+
const lowerFont = font.toLowerCase();
|
|
513
|
+
const fontStyle = lowerFont.includes("italic") ? "italic" : lowerFont.includes("oblique") ? "oblique" : "normal";
|
|
514
|
+
const fontWeight = lowerFont.includes("bold") ? "bold" : _nullishCoalesce(["900", "800", "700", "600", "500", "400", "300", "200", "100"].find(
|
|
515
|
+
(weight) => lowerFont.includes(weight)
|
|
516
|
+
), () => ( "normal"));
|
|
517
|
+
const fontFamily = fontFamilyFromShorthand(font, sizeToken);
|
|
518
|
+
const fillVal = this.escapeXML(this.resolveGradient(color));
|
|
428
519
|
const transformStr = `matrix(${this.ma},${this.mb},${this.mc},${this.md},${this.me},${this.mf})`;
|
|
429
520
|
this.buffer.push(
|
|
430
521
|
`<g transform="${transformStr}"><text x="${x}" y="${y}" font-size="${fontSize}" font-weight="${fontWeight}" font-style="${fontStyle}" font-family="${this.escapeXML(fontFamily)}" fill="${fillVal}" opacity="${this.globalAlpha}">${this.escapeXML(text)}</text></g>`
|
|
@@ -446,7 +537,9 @@ var SVGRenderer = (_class2 = class {
|
|
|
446
537
|
}
|
|
447
538
|
drawImage(source, dx, dy, dw, dh) {
|
|
448
539
|
this.flush();
|
|
449
|
-
const
|
|
540
|
+
const fromCanvas2 = typeof _optionalChain([source, 'optionalAccess', _ => _.toDataURL]) === "function";
|
|
541
|
+
const rawHref = fromCanvas2 ? source.toDataURL() : _optionalChain([source, 'optionalAccess', _2 => _2.src]) || "";
|
|
542
|
+
const href = fromCanvas2 && this.isSafeRasterDataUrl(rawHref) ? rawHref : sanitizeUrl(String(rawHref));
|
|
450
543
|
const transformStr = `matrix(${this.ma},${this.mb},${this.mc},${this.md},${this.me},${this.mf})`;
|
|
451
544
|
if (href) {
|
|
452
545
|
this.buffer.push(
|
|
@@ -459,6 +552,21 @@ var SVGRenderer = (_class2 = class {
|
|
|
459
552
|
console.warn("drawImage source fallback triggered");
|
|
460
553
|
}
|
|
461
554
|
}
|
|
555
|
+
/**
|
|
556
|
+
* Embed SVG markup as an isolated nested image.
|
|
557
|
+
*
|
|
558
|
+
* This explicit path is used by `SVGEntity` during vector export. General
|
|
559
|
+
* `drawImage()` input remains subject to the URL policy and therefore still
|
|
560
|
+
* rejects caller-provided SVG data URLs.
|
|
561
|
+
*/
|
|
562
|
+
drawSVG(source, dx, dy, dw, dh) {
|
|
563
|
+
this.flush();
|
|
564
|
+
const href = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(source)}`;
|
|
565
|
+
const transformStr = `matrix(${this.ma},${this.mb},${this.mc},${this.md},${this.me},${this.mf})`;
|
|
566
|
+
this.buffer.push(
|
|
567
|
+
`<image href="${this.escapeXML(href)}" x="${dx}" y="${dy}" width="${dw}" height="${dh}" transform="${transformStr}" />`
|
|
568
|
+
);
|
|
569
|
+
}
|
|
462
570
|
flush() {
|
|
463
571
|
if (!this.batchActive || this.batchCircles.length === 0) return;
|
|
464
572
|
let d = "";
|
|
@@ -566,6 +674,17 @@ var SVGRenderer = (_class2 = class {
|
|
|
566
674
|
}
|
|
567
675
|
});
|
|
568
676
|
}
|
|
677
|
+
isSafeRasterDataUrl(value) {
|
|
678
|
+
return typeof value === "string" && /^data:image\/(?:png|jpeg|gif|webp);base64,[a-z\d+/=\s]*$/i.test(value);
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* SVGRenderer accumulates strings in memory; nothing external is allocated.
|
|
682
|
+
* Drop the buffers for GC and become idempotent.
|
|
683
|
+
*/
|
|
684
|
+
dispose() {
|
|
685
|
+
this.clear();
|
|
686
|
+
this.gradientCache.clear();
|
|
687
|
+
}
|
|
569
688
|
}, _class2);
|
|
570
689
|
|
|
571
690
|
// src/renderer/colorParse.ts
|
|
@@ -831,6 +950,7 @@ function createWebGLPointRenderer(canvas) {
|
|
|
831
950
|
let logicalW = 0;
|
|
832
951
|
let logicalH = 0;
|
|
833
952
|
let dpr = 1;
|
|
953
|
+
let destroyed = false;
|
|
834
954
|
return {
|
|
835
955
|
resize(width, height) {
|
|
836
956
|
logicalW = width;
|
|
@@ -1037,6 +1157,8 @@ function createWebGLPointRenderer(canvas) {
|
|
|
1037
1157
|
gl.bindVertexArray(null);
|
|
1038
1158
|
},
|
|
1039
1159
|
destroy() {
|
|
1160
|
+
if (destroyed) return;
|
|
1161
|
+
destroyed = true;
|
|
1040
1162
|
gl.deleteBuffer(pointBuffer);
|
|
1041
1163
|
gl.deleteBuffer(rectBuffer);
|
|
1042
1164
|
gl.deleteBuffer(spriteBuffer);
|
|
@@ -1326,10 +1448,14 @@ var WebGPUParticleSystemManager = (_class3 = class {
|
|
|
1326
1448
|
if (!this.computePipeline || !entity.computeBindGroup) return;
|
|
1327
1449
|
const uniformArray = new Float32Array(20);
|
|
1328
1450
|
const color = parseColorToRGBA(entity.baseColor);
|
|
1451
|
+
let opacity = 1;
|
|
1452
|
+
for (let current = entity; current; current = current.parent) {
|
|
1453
|
+
opacity *= current.opacity;
|
|
1454
|
+
}
|
|
1329
1455
|
uniformArray[0] = color[0];
|
|
1330
1456
|
uniformArray[1] = color[1];
|
|
1331
1457
|
uniformArray[2] = color[2];
|
|
1332
|
-
uniformArray[3] = color[3];
|
|
1458
|
+
uniformArray[3] = color[3] * opacity;
|
|
1333
1459
|
const mActive = !isNaN(mouseX) && !isNaN(mouseY) && mouseX > -9e3 && mouseY > -9e3;
|
|
1334
1460
|
uniformArray[4] = mActive ? mouseX : -9999;
|
|
1335
1461
|
uniformArray[5] = mActive ? mouseY : -9999;
|
|
@@ -1377,4 +1503,6 @@ var WebGPUParticleSystemManager = (_class3 = class {
|
|
|
1377
1503
|
|
|
1378
1504
|
|
|
1379
1505
|
|
|
1380
|
-
|
|
1506
|
+
|
|
1507
|
+
|
|
1508
|
+
exports.CanvasRenderer = CanvasRenderer; exports.sanitizeUrl = sanitizeUrl; exports.isSafeUrl = isSafeUrl; exports.SVGRenderer = SVGRenderer; exports.parseColorToRGBA = parseColorToRGBA; exports.createWebGLPointRenderer = createWebGLPointRenderer; exports.WebGPUParticleSystemManager = WebGPUParticleSystemManager;
|