@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
|
@@ -63,7 +63,8 @@ function emptyDrawCounters() {
|
|
|
63
63
|
}
|
|
64
64
|
var TWO_PI = Math.PI * 2;
|
|
65
65
|
function getDevicePixelRatio() {
|
|
66
|
-
|
|
66
|
+
const raw = typeof window !== "undefined" ? window.devicePixelRatio : 1;
|
|
67
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 1;
|
|
67
68
|
}
|
|
68
69
|
var CanvasRenderer = class _CanvasRenderer {
|
|
69
70
|
ctx;
|
|
@@ -107,6 +108,17 @@ var CanvasRenderer = class _CanvasRenderer {
|
|
|
107
108
|
batchCount = 0;
|
|
108
109
|
_cachedFont = "";
|
|
109
110
|
_cachedFill = "";
|
|
111
|
+
// Stroke-side counterparts of the fill/font caches, read by stroke()'s
|
|
112
|
+
// style-elision branch. Reset wherever the context state can change behind
|
|
113
|
+
// our back: restore(), resize(), contextrestored, dispose().
|
|
114
|
+
_cachedStroke = "";
|
|
115
|
+
_cachedLineWidth = -1;
|
|
116
|
+
_cachedLineCap = "";
|
|
117
|
+
_cachedLineJoin = "";
|
|
118
|
+
// Context-loss listeners, held for removal in dispose() so a canvas that
|
|
119
|
+
// outlives the renderer cannot retain it through these closures.
|
|
120
|
+
_onContextLost;
|
|
121
|
+
_onContextRestored;
|
|
110
122
|
/** Backend discriminator; see {@link IRenderer.kind}. */
|
|
111
123
|
kind = "canvas2d";
|
|
112
124
|
/**
|
|
@@ -160,11 +172,11 @@ var CanvasRenderer = class _CanvasRenderer {
|
|
|
160
172
|
*/
|
|
161
173
|
setupContextLossRecovery() {
|
|
162
174
|
if (typeof this.canvas.addEventListener !== "function") return;
|
|
163
|
-
this.
|
|
175
|
+
this._onContextLost = (e) => {
|
|
164
176
|
e.preventDefault();
|
|
165
177
|
this.contextLost = true;
|
|
166
|
-
}
|
|
167
|
-
this.
|
|
178
|
+
};
|
|
179
|
+
this._onContextRestored = () => {
|
|
168
180
|
const ctx = this.canvas.getContext("2d");
|
|
169
181
|
if (!ctx) return;
|
|
170
182
|
this.ctx = ctx;
|
|
@@ -174,11 +186,17 @@ var CanvasRenderer = class _CanvasRenderer {
|
|
|
174
186
|
ctx.scale(restoredDPR, restoredDPR);
|
|
175
187
|
this._cachedFont = "";
|
|
176
188
|
this._cachedFill = "";
|
|
189
|
+
this._cachedStroke = "";
|
|
190
|
+
this._cachedLineWidth = -1;
|
|
191
|
+
this._cachedLineCap = "";
|
|
192
|
+
this._cachedLineJoin = "";
|
|
177
193
|
this.batchActive = false;
|
|
178
194
|
this.batchCount = 0;
|
|
179
195
|
this.contextLost = false;
|
|
180
196
|
this.contextRestoredCb?.();
|
|
181
|
-
}
|
|
197
|
+
};
|
|
198
|
+
this.canvas.addEventListener("contextlost", this._onContextLost);
|
|
199
|
+
this.canvas.addEventListener("contextrestored", this._onContextRestored);
|
|
182
200
|
}
|
|
183
201
|
/**
|
|
184
202
|
* Expose the underlying `CanvasRenderingContext2D` for operations not
|
|
@@ -192,7 +210,9 @@ var CanvasRenderer = class _CanvasRenderer {
|
|
|
192
210
|
/** Real `devicePixelRatio`, clamped to {@link maxDPR} when set. */
|
|
193
211
|
effectiveDPR() {
|
|
194
212
|
const real = getDevicePixelRatio();
|
|
195
|
-
|
|
213
|
+
if (this.maxDPR === void 0) return real;
|
|
214
|
+
if (!Number.isFinite(this.maxDPR) || this.maxDPR <= 0) return real;
|
|
215
|
+
return Math.min(real, this.maxDPR);
|
|
196
216
|
}
|
|
197
217
|
/**
|
|
198
218
|
* @inheritdoc
|
|
@@ -223,16 +243,26 @@ var CanvasRenderer = class _CanvasRenderer {
|
|
|
223
243
|
*/
|
|
224
244
|
resize(width, height) {
|
|
225
245
|
const dpr = this.effectiveDPR();
|
|
246
|
+
const safeWidth = Number.isFinite(width) && width >= 0 ? width : this.width;
|
|
247
|
+
const safeHeight = Number.isFinite(height) && height >= 0 ? height : this.height;
|
|
248
|
+
const backingW = Number.isFinite(safeWidth * dpr) ? Math.max(1, Math.round(safeWidth * dpr)) : 1;
|
|
249
|
+
const backingH = Number.isFinite(safeHeight * dpr) ? Math.max(1, Math.round(safeHeight * dpr)) : 1;
|
|
226
250
|
this.appliedDPR = dpr;
|
|
227
|
-
this.width =
|
|
228
|
-
this.height =
|
|
229
|
-
this.ctx.canvas.width =
|
|
230
|
-
this.ctx.canvas.height =
|
|
231
|
-
this.ctx.canvas.style
|
|
232
|
-
|
|
251
|
+
this.width = safeWidth;
|
|
252
|
+
this.height = safeHeight;
|
|
253
|
+
this.ctx.canvas.width = backingW;
|
|
254
|
+
this.ctx.canvas.height = backingH;
|
|
255
|
+
if (this.ctx.canvas.style) {
|
|
256
|
+
this.ctx.canvas.style.width = `${safeWidth}px`;
|
|
257
|
+
this.ctx.canvas.style.height = `${safeHeight}px`;
|
|
258
|
+
}
|
|
233
259
|
this.ctx.scale(dpr, dpr);
|
|
234
260
|
this._cachedFont = "";
|
|
235
261
|
this._cachedFill = "";
|
|
262
|
+
this._cachedStroke = "";
|
|
263
|
+
this._cachedLineWidth = -1;
|
|
264
|
+
this._cachedLineCap = "";
|
|
265
|
+
this._cachedLineJoin = "";
|
|
236
266
|
this.batchActive = false;
|
|
237
267
|
this.batchCount = 0;
|
|
238
268
|
}
|
|
@@ -286,6 +316,10 @@ var CanvasRenderer = class _CanvasRenderer {
|
|
|
286
316
|
this.ctx.restore();
|
|
287
317
|
this._cachedFont = "";
|
|
288
318
|
this._cachedFill = "";
|
|
319
|
+
this._cachedStroke = "";
|
|
320
|
+
this._cachedLineWidth = -1;
|
|
321
|
+
this._cachedLineCap = "";
|
|
322
|
+
this._cachedLineJoin = "";
|
|
289
323
|
}
|
|
290
324
|
/** @inheritdoc */
|
|
291
325
|
translate(x, y) {
|
|
@@ -410,10 +444,17 @@ var CanvasRenderer = class _CanvasRenderer {
|
|
|
410
444
|
stroke(color, lineWidth = 1) {
|
|
411
445
|
this.flush();
|
|
412
446
|
if (this.counters) this.counters.strokes++;
|
|
413
|
-
this.
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
447
|
+
if (this._cachedStroke !== color || this._cachedLineWidth !== lineWidth || this._cachedLineCap !== "round" || this._cachedLineJoin !== "round") {
|
|
448
|
+
if (this.counters) this.counters.stateSwitches++;
|
|
449
|
+
this.ctx.strokeStyle = color;
|
|
450
|
+
this.ctx.lineWidth = lineWidth;
|
|
451
|
+
this.ctx.lineCap = "round";
|
|
452
|
+
this.ctx.lineJoin = "round";
|
|
453
|
+
this._cachedStroke = color;
|
|
454
|
+
this._cachedLineWidth = lineWidth;
|
|
455
|
+
this._cachedLineCap = "round";
|
|
456
|
+
this._cachedLineJoin = "round";
|
|
457
|
+
}
|
|
417
458
|
this.ctx.stroke();
|
|
418
459
|
}
|
|
419
460
|
/** @inheritdoc */
|
|
@@ -452,6 +493,17 @@ var CanvasRenderer = class _CanvasRenderer {
|
|
|
452
493
|
this.batchActive = false;
|
|
453
494
|
this._cachedFont = "";
|
|
454
495
|
this._cachedFill = "";
|
|
496
|
+
this._cachedStroke = "";
|
|
497
|
+
this._cachedLineWidth = -1;
|
|
498
|
+
this._cachedLineCap = "";
|
|
499
|
+
this._cachedLineJoin = "";
|
|
500
|
+
if (typeof this.canvas.removeEventListener === "function") {
|
|
501
|
+
if (this._onContextLost) this.canvas.removeEventListener("contextlost", this._onContextLost);
|
|
502
|
+
if (this._onContextRestored)
|
|
503
|
+
this.canvas.removeEventListener("contextrestored", this._onContextRestored);
|
|
504
|
+
}
|
|
505
|
+
this._onContextLost = void 0;
|
|
506
|
+
this._onContextRestored = void 0;
|
|
455
507
|
}
|
|
456
508
|
};
|
|
457
509
|
|
|
@@ -474,9 +526,13 @@ function hasSafeSchemeOrIsRelative(url) {
|
|
|
474
526
|
return SAFE_SCHEMES.has(`${candidate.toLowerCase()}:`);
|
|
475
527
|
}
|
|
476
528
|
var CHAR_REF = /&(?:#\d+|#x[\da-f]+|colon|tab|newline);/i;
|
|
529
|
+
var MAX_CODE_POINT = 1114111;
|
|
530
|
+
function decodeCodePoint(value) {
|
|
531
|
+
return value <= MAX_CODE_POINT ? String.fromCodePoint(value) : "\uFFFD";
|
|
532
|
+
}
|
|
477
533
|
function decodeCharacterReferences(value) {
|
|
478
534
|
if (value.indexOf("&") < 0 || !CHAR_REF.test(value)) return value;
|
|
479
|
-
return value.replace(/&#x([\da-f]+);/gi, (_, hex) =>
|
|
535
|
+
return value.replace(/&#x([\da-f]+);/gi, (_, hex) => decodeCodePoint(parseInt(hex, 16))).replace(/&#(\d+);/g, (_, dec) => decodeCodePoint(parseInt(dec, 10))).replace(/:/gi, ":").replace(/&tab;/gi, " ").replace(/&newline;/gi, "\n");
|
|
480
536
|
}
|
|
481
537
|
function sanitizeUrl(href) {
|
|
482
538
|
if (typeof href !== "string") return "";
|
|
@@ -578,9 +634,12 @@ var SVGRenderer = class {
|
|
|
578
634
|
// Cache for generated gradient defs
|
|
579
635
|
gradientCounter = 0;
|
|
580
636
|
gradientCache = /* @__PURE__ */ new Map();
|
|
581
|
-
|
|
637
|
+
/** Root font size used to resolve `em`/`rem` font sizes (default 16px). */
|
|
638
|
+
rootFontSize;
|
|
639
|
+
constructor(width, height, options) {
|
|
582
640
|
this.width = width;
|
|
583
641
|
this.height = height;
|
|
642
|
+
this.rootFontSize = options?.rootFontSize ?? 16;
|
|
584
643
|
installRendererDevTraps(this, "SVGRenderer");
|
|
585
644
|
}
|
|
586
645
|
clear() {
|
|
@@ -768,12 +827,22 @@ var SVGRenderer = class {
|
|
|
768
827
|
`<path d="${dStr}" transform="${transformStr}" fill="none" stroke="${strokeVal}" stroke-width="${lineWidth}" stroke-opacity="${this.globalAlpha}" />`
|
|
769
828
|
);
|
|
770
829
|
}
|
|
830
|
+
/**
|
|
831
|
+
* Emit a `<text>` element for a text run.
|
|
832
|
+
*
|
|
833
|
+
* Font-size unit handling: `px` sizes pass through; `em` and `rem` are both
|
|
834
|
+
* scaled against the renderer's `rootFontSize` (constructor option, default
|
|
835
|
+
* 16 — matching the browser default, not necessarily the host page's root,
|
|
836
|
+
* so pass `{ rootFontSize }` when exporting against a non-default root).
|
|
837
|
+
* A percentage size (`'100% Inter'`) has no absolute meaning in this
|
|
838
|
+
* context and silently falls back to the default 16px font size.
|
|
839
|
+
*/
|
|
771
840
|
fillText(text, x, y, font, color) {
|
|
772
841
|
this.flush();
|
|
773
842
|
const sizeToken = parseFontSizeToken(font);
|
|
774
843
|
let fontSize = sizeToken ? sizeToken.value : 16;
|
|
775
|
-
if (sizeToken && sizeToken.unit
|
|
776
|
-
fontSize = fontSize *
|
|
844
|
+
if (sizeToken && (sizeToken.unit === "em" || sizeToken.unit === "rem")) {
|
|
845
|
+
fontSize = fontSize * this.rootFontSize;
|
|
777
846
|
}
|
|
778
847
|
const lowerFont = font.toLowerCase();
|
|
779
848
|
const fontStyle = lowerFont.includes("italic") ? "italic" : lowerFont.includes("oblique") ? "oblique" : "normal";
|
|
@@ -955,11 +1024,11 @@ var SVGRenderer = class {
|
|
|
955
1024
|
}
|
|
956
1025
|
/**
|
|
957
1026
|
* SVGRenderer accumulates strings in memory; nothing external is allocated.
|
|
958
|
-
* Drop the buffers for GC and become idempotent.
|
|
1027
|
+
* Drop the buffers for GC and become idempotent. `clear()` already empties
|
|
1028
|
+
* the gradient cache, so no second clear is needed here.
|
|
959
1029
|
*/
|
|
960
1030
|
dispose() {
|
|
961
1031
|
this.clear();
|
|
962
|
-
this.gradientCache.clear();
|
|
963
1032
|
}
|
|
964
1033
|
};
|
|
965
1034
|
|
|
@@ -1331,13 +1400,22 @@ function createWebGLPointRenderer(canvas) {
|
|
|
1331
1400
|
premultipliedAlpha: false
|
|
1332
1401
|
});
|
|
1333
1402
|
if (!gl) return null;
|
|
1334
|
-
const
|
|
1335
|
-
const
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1403
|
+
const programs = [];
|
|
1404
|
+
const linkTracked = (vsSrc, fsSrc) => {
|
|
1405
|
+
const program = link(gl, vsSrc, fsSrc);
|
|
1406
|
+
if (program) programs.push(program);
|
|
1407
|
+
return program;
|
|
1408
|
+
};
|
|
1409
|
+
const pointProgram = linkTracked(POINT_VERT, POINT_FRAG);
|
|
1410
|
+
const rectProgram = linkTracked(RECT_VERT, RECT_FRAG);
|
|
1411
|
+
const spriteProgram = linkTracked(SPRITE_VERT, SPRITE_FRAG);
|
|
1412
|
+
const msdfProgram = linkTracked(SPRITE_VERT, MSDF_FRAG);
|
|
1413
|
+
const circleQuadProgram = linkTracked(SPRITE_VERT, CIRCLE_QUAD_FRAG);
|
|
1414
|
+
if (!pointProgram || !rectProgram || !spriteProgram || !msdfProgram || !circleQuadProgram) {
|
|
1415
|
+
for (const program of programs) gl.deleteProgram(program);
|
|
1416
|
+
gl.getExtension("WEBGL_lose_context")?.loseContext();
|
|
1340
1417
|
return null;
|
|
1418
|
+
}
|
|
1341
1419
|
gl.enable(gl.BLEND);
|
|
1342
1420
|
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
1343
1421
|
const pAPos = gl.getAttribLocation(pointProgram, "a_pos");
|
|
@@ -1517,6 +1595,7 @@ function createWebGLPointRenderer(canvas) {
|
|
|
1517
1595
|
ensureQuadIndices(spriteCount);
|
|
1518
1596
|
gl.drawElements(gl.TRIANGLES, spriteCount * INDICES_PER_QUAD, gl.UNSIGNED_INT, 0);
|
|
1519
1597
|
frameDrawCalls++;
|
|
1598
|
+
gl.bindVertexArray(null);
|
|
1520
1599
|
spriteCount = 0;
|
|
1521
1600
|
};
|
|
1522
1601
|
const drawGlyphs = () => {
|
|
@@ -1795,6 +1874,7 @@ function createWebGLPointRenderer(canvas) {
|
|
|
1795
1874
|
}
|
|
1796
1875
|
|
|
1797
1876
|
// src/renderer/WebGPUParticleSystemManager.ts
|
|
1877
|
+
var recordComputePassScratch = new Float32Array(20);
|
|
1798
1878
|
var COMPUTE_SHADER = `
|
|
1799
1879
|
struct Particle {
|
|
1800
1880
|
position: vec2<f32>,
|
|
@@ -1971,14 +2051,20 @@ var WebGPUParticleSystemManager = class {
|
|
|
1971
2051
|
device;
|
|
1972
2052
|
computePipeline = null;
|
|
1973
2053
|
renderPipeline = null;
|
|
2054
|
+
// Held so destroy() can release them — modules were previously unreachable
|
|
2055
|
+
// after initPipelines and lived until context loss (#684).
|
|
2056
|
+
computeModule = null;
|
|
2057
|
+
renderModule = null;
|
|
1974
2058
|
computeBindGroupLayout = null;
|
|
1975
2059
|
renderBindGroupLayout = null;
|
|
1976
2060
|
constructor(device) {
|
|
1977
2061
|
this.device = device;
|
|
1978
2062
|
}
|
|
1979
2063
|
initPipelines(format) {
|
|
1980
|
-
|
|
1981
|
-
|
|
2064
|
+
this.computeModule = this.device.createShaderModule({ code: COMPUTE_SHADER });
|
|
2065
|
+
this.renderModule = this.device.createShaderModule({ code: RENDER_SHADER });
|
|
2066
|
+
const computeModule = this.computeModule;
|
|
2067
|
+
const renderModule = this.renderModule;
|
|
1982
2068
|
this.computeBindGroupLayout = this.device.createBindGroupLayout({
|
|
1983
2069
|
entries: [
|
|
1984
2070
|
{
|
|
@@ -2037,6 +2123,8 @@ var WebGPUParticleSystemManager = class {
|
|
|
2037
2123
|
});
|
|
2038
2124
|
}
|
|
2039
2125
|
setupEntityResources(entity) {
|
|
2126
|
+
releaseGpuObject(entity.gpuStorageBuffer);
|
|
2127
|
+
releaseGpuObject(entity.gpuUniformBuffer);
|
|
2040
2128
|
const storageSize = entity.maxParticles * 32;
|
|
2041
2129
|
entity.gpuStorageBuffer = this.device.createBuffer({
|
|
2042
2130
|
size: storageSize,
|
|
@@ -2063,7 +2151,7 @@ var WebGPUParticleSystemManager = class {
|
|
|
2063
2151
|
}
|
|
2064
2152
|
recordComputePass(pass, entity, dt, mouseX, mouseY, width, height) {
|
|
2065
2153
|
if (!this.computePipeline || !entity.computeBindGroup) return;
|
|
2066
|
-
const uniformArray =
|
|
2154
|
+
const uniformArray = recordComputePassScratch;
|
|
2067
2155
|
const color = parseColorToRGBA(entity.baseColor);
|
|
2068
2156
|
let opacity = 1;
|
|
2069
2157
|
for (let current = entity; current; current = current.parent) {
|
|
@@ -2107,12 +2195,23 @@ var WebGPUParticleSystemManager = class {
|
|
|
2107
2195
|
pass.draw(6, entity.maxParticles);
|
|
2108
2196
|
}
|
|
2109
2197
|
destroy() {
|
|
2198
|
+
releaseGpuObject(this.computePipeline);
|
|
2199
|
+
releaseGpuObject(this.renderPipeline);
|
|
2200
|
+
releaseGpuObject(this.computeModule);
|
|
2201
|
+
releaseGpuObject(this.renderModule);
|
|
2110
2202
|
this.computePipeline = null;
|
|
2111
2203
|
this.renderPipeline = null;
|
|
2204
|
+
this.computeModule = null;
|
|
2205
|
+
this.renderModule = null;
|
|
2112
2206
|
this.computeBindGroupLayout = null;
|
|
2113
2207
|
this.renderBindGroupLayout = null;
|
|
2114
2208
|
}
|
|
2115
2209
|
};
|
|
2210
|
+
function releaseGpuObject(obj) {
|
|
2211
|
+
if (obj && typeof obj.destroy === "function") {
|
|
2212
|
+
obj.destroy();
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2116
2215
|
|
|
2117
2216
|
// src/renderer/GlyphRasterAtlas.ts
|
|
2118
2217
|
var HARD_MAX_SIZE = 8192;
|
|
@@ -2193,7 +2292,7 @@ var GlyphRasterAtlas = class {
|
|
|
2193
2292
|
* (headless, unrasterizable, or too large to pack).
|
|
2194
2293
|
*/
|
|
2195
2294
|
get(font, color, glyph) {
|
|
2196
|
-
const key = font
|
|
2295
|
+
const key = `${font.length}\0${font}${color.length}\0${color}${glyph.length}\0${glyph}`;
|
|
2197
2296
|
const existing = this.slots.get(key);
|
|
2198
2297
|
if (existing !== void 0) {
|
|
2199
2298
|
if (existing) this._hits++;
|
|
@@ -2347,7 +2446,7 @@ var TextRasterCache = class {
|
|
|
2347
2446
|
* fall back to {@link IRenderer.fillText}).
|
|
2348
2447
|
*/
|
|
2349
2448
|
get(font, color, text) {
|
|
2350
|
-
const key = font
|
|
2449
|
+
const key = `${font.length}\0${font}${color.length}\0${color}${text.length}\0${text}`;
|
|
2351
2450
|
const hit = this.cache.get(key);
|
|
2352
2451
|
if (hit) {
|
|
2353
2452
|
this._hits++;
|