@vectojs/three 0.1.3 → 0.1.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 +94 -0
- package/dist/ThreeRenderer.d.ts +22 -0
- package/dist/index.js +83 -29
- package/dist/index.mjs +83 -29
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# @vectojs/three
|
|
2
|
+
|
|
3
|
+
> Put a live VectoJS 2D interface into Three.js/WebXR and route 3D pointer input back to the canvas.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@vectojs/three)
|
|
6
|
+
[](https://github.com/vectojs/vectojs/actions/workflows/ci.yml)
|
|
7
|
+
[](https://github.com/vectojs/vectojs/blob/main/LICENSE)
|
|
8
|
+
|
|
9
|
+
`@vectojs/three` renders a VectoJS `Scene` into a canvas-backed Three.js texture. The adapter maps
|
|
10
|
+
raycast UV coordinates into the Scene's logical coordinate space, forwards pointer/hover/wheel
|
|
11
|
+
events, and keeps texture uploads synchronized with VectoJS rendering.
|
|
12
|
+
|
|
13
|
+
[Live 3D demo](https://vectojs.xuepoo.xyz/demos/dimension/) ·
|
|
14
|
+
[Reference](https://vectojs.xuepoo.xyz/reference/three/) ·
|
|
15
|
+
[Main repository](https://github.com/vectojs/vectojs)
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bun add @vectojs/core @vectojs/ui @vectojs/three three
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`@vectojs/core` and `three` are peer dependencies. `@vectojs/ui` is used by the example and is
|
|
24
|
+
optional when you supply your own core entities.
|
|
25
|
+
|
|
26
|
+
## Basic usage
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { Button, Stack, Text } from '@vectojs/ui';
|
|
30
|
+
import { ThreeAdapter } from '@vectojs/three';
|
|
31
|
+
import * as THREE from 'three';
|
|
32
|
+
|
|
33
|
+
const scene3d = new THREE.Scene();
|
|
34
|
+
const camera = new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 0.1, 100);
|
|
35
|
+
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
36
|
+
|
|
37
|
+
const adapter = new ThreeAdapter({
|
|
38
|
+
width: 800,
|
|
39
|
+
height: 500,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const panel = new Stack({ direction: 'vertical', gap: 16 });
|
|
43
|
+
panel.setPosition(36, 36);
|
|
44
|
+
panel.add(new Text('VectoJS in 3D', { font: '700 28px Inter' }));
|
|
45
|
+
panel.add(new Button('Select', { onClick: () => console.log('selected') }));
|
|
46
|
+
adapter.vectoScene.add(panel);
|
|
47
|
+
adapter.vectoScene.start();
|
|
48
|
+
|
|
49
|
+
// Use the adapter's texture/material with a mesh in your Three.js scene.
|
|
50
|
+
scene3d.add(adapter.mesh);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
See the [reference](https://vectojs.xuepoo.xyz/reference/three/) for the exact constructor and
|
|
54
|
+
mesh/material customization supported by the installed version.
|
|
55
|
+
|
|
56
|
+
## Coordinate and event model
|
|
57
|
+
|
|
58
|
+
- VectoJS layout uses the logical `width`/`height` passed to the adapter.
|
|
59
|
+
- The backing canvas may be larger on HiDPI displays; raycast UVs are still mapped to logical space.
|
|
60
|
+
- Pointer intersections are translated into VectoJS events and routed through its normal hit-test,
|
|
61
|
+
capture, target, and bubble phases.
|
|
62
|
+
- Hover state is tracked per pointer, including pointer leave boundaries.
|
|
63
|
+
- WebXR controllers can use the same raycast-to-2D route when supplied by the host application.
|
|
64
|
+
|
|
65
|
+
The adapter does not own the application's Three.js render loop, camera, controls, or raycaster.
|
|
66
|
+
|
|
67
|
+
## Texture synchronization
|
|
68
|
+
|
|
69
|
+
The adapter wraps the VectoJS Scene render path so `texture.needsUpdate` is set after a dirty frame.
|
|
70
|
+
On-demand VectoJS scenes therefore upload only when their visual state changes; the Three.js host
|
|
71
|
+
still decides when to render its own frame.
|
|
72
|
+
|
|
73
|
+
## Lifecycle
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
adapter.dispose();
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`dispose()` restores the Scene render hook, releases adapter-owned Three.js resources, destroys the
|
|
80
|
+
inner VectoJS Scene, and detaches event state. Call it when removing the panel or unmounting the host
|
|
81
|
+
component.
|
|
82
|
+
|
|
83
|
+
## Constraints
|
|
84
|
+
|
|
85
|
+
- The default output is a flat textured plane; it is not DOM rendered in 3D.
|
|
86
|
+
- Canvas clipping and logical hit-testing remain 2D even when the mesh is rotated in world space.
|
|
87
|
+
- The host must provide correct raycast intersections and account for occlusion.
|
|
88
|
+
- Texture resolution affects sharpness and upload cost. Choose logical size and DPR for the target
|
|
89
|
+
viewing distance rather than always maximizing both.
|
|
90
|
+
- Three.js releases outside the declared peer range are not guaranteed.
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
[MIT](https://github.com/vectojs/vectojs/blob/main/LICENSE) © 2026 Xuepoo
|
package/dist/ThreeRenderer.d.ts
CHANGED
|
@@ -55,9 +55,31 @@ export declare class ThreeRenderer implements IRenderer {
|
|
|
55
55
|
fill(colorOrGradient: string | any): void;
|
|
56
56
|
private fillSolidShape;
|
|
57
57
|
stroke(colorOrGradient: string | any, lineWidth?: number): void;
|
|
58
|
+
/**
|
|
59
|
+
* Rasterized fillText textures keyed by `font|color|text`, LRU-evicted.
|
|
60
|
+
* HUD-style UIs redraw the same labels every frame; without the cache each
|
|
61
|
+
* call allocated a canvas, rasterized it, and uploaded a fresh GPU texture.
|
|
62
|
+
* Entries are skipped by {@link disposeActiveObjects} (flagged via
|
|
63
|
+
* `userData.vectoCached`) and released on eviction or {@link dispose}.
|
|
64
|
+
*/
|
|
65
|
+
private textTextureCache;
|
|
66
|
+
private textTextureCacheLimit;
|
|
58
67
|
fillText(text: string, x: number, y: number, font: string, color: string | any): void;
|
|
59
68
|
fillCircle(cx: number, cy: number, radius: number, color: string, alpha?: number): void;
|
|
69
|
+
private frameDirty;
|
|
70
|
+
private presentScheduled;
|
|
71
|
+
/**
|
|
72
|
+
* The Scene calls flush() around every non-batched node (it commits the
|
|
73
|
+
* Canvas2D circle batch there). Rendering the whole Three scene on each of
|
|
74
|
+
* those calls made a frame cost O(N²) in entity count, so flush() only marks
|
|
75
|
+
* the frame dirty; the actual GL render happens once, in {@link present}.
|
|
76
|
+
*
|
|
77
|
+
* A microtask fallback keeps older `@vectojs/core` Scenes (that never call
|
|
78
|
+
* `present()`) painting: the many same-tick flushes coalesce into one render.
|
|
79
|
+
*/
|
|
60
80
|
flush(): void;
|
|
81
|
+
/** Render the accumulated frame once. Called by Scene at the end of each render pass. */
|
|
82
|
+
present(): void;
|
|
61
83
|
createLinearGradient(x0: number, y0: number, x1: number, y1: number, colorStops: {
|
|
62
84
|
stop: number;
|
|
63
85
|
color: string;
|
package/dist/index.js
CHANGED
|
@@ -161,7 +161,7 @@ var ThreeRenderer = class {
|
|
|
161
161
|
const materials = Array.isArray(object.material) ? object.material : [object.material];
|
|
162
162
|
for (const material of materials) {
|
|
163
163
|
const map = material.map;
|
|
164
|
-
if (map && !disposedTextures.has(map)) {
|
|
164
|
+
if (map && !map.userData.vectoCached && !disposedTextures.has(map)) {
|
|
165
165
|
disposedTextures.add(map);
|
|
166
166
|
map.dispose();
|
|
167
167
|
}
|
|
@@ -218,7 +218,7 @@ var ThreeRenderer = class {
|
|
|
218
218
|
const minY = Math.min(...corners.map((point) => point.y));
|
|
219
219
|
const maxX = Math.max(...corners.map((point) => point.x));
|
|
220
220
|
const maxY = Math.max(...corners.map((point) => point.y));
|
|
221
|
-
const dpr =
|
|
221
|
+
const dpr = this.renderer.getPixelRatio() || 1;
|
|
222
222
|
const canvasHeight = this.renderer.domElement.height / dpr;
|
|
223
223
|
let scissorX = minX * dpr;
|
|
224
224
|
let scissorY = (canvasHeight - maxY) * dpr;
|
|
@@ -481,33 +481,35 @@ var ThreeRenderer = class {
|
|
|
481
481
|
color = grad.colorStops[0].color;
|
|
482
482
|
}
|
|
483
483
|
}
|
|
484
|
-
const points = this.currentPath.getPoints();
|
|
485
|
-
if (points.length === 0) return;
|
|
486
|
-
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
|
487
484
|
const parsed = parseThreeColor(color);
|
|
488
485
|
const opacity = this.globalAlpha * parsed.alpha;
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
486
|
+
for (const shape of this.currentPath.toShapes()) {
|
|
487
|
+
const points = shape.getPoints();
|
|
488
|
+
if (points.length < 2) continue;
|
|
489
|
+
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
|
490
|
+
const material = new THREE.LineBasicMaterial({
|
|
491
|
+
color: parsed.color,
|
|
492
|
+
transparent: opacity < 1,
|
|
493
|
+
opacity,
|
|
494
|
+
linewidth: lineWidth
|
|
495
|
+
});
|
|
496
|
+
const line = new THREE.Line(geometry, material);
|
|
497
|
+
line.applyMatrix4(this.matrix);
|
|
498
|
+
this.scene.add(line);
|
|
499
|
+
this.activeObjects.push(line);
|
|
500
|
+
}
|
|
499
501
|
}
|
|
502
|
+
/**
|
|
503
|
+
* Rasterized fillText textures keyed by `font|color|text`, LRU-evicted.
|
|
504
|
+
* HUD-style UIs redraw the same labels every frame; without the cache each
|
|
505
|
+
* call allocated a canvas, rasterized it, and uploaded a fresh GPU texture.
|
|
506
|
+
* Entries are skipped by {@link disposeActiveObjects} (flagged via
|
|
507
|
+
* `userData.vectoCached`) and released on eviction or {@link dispose}.
|
|
508
|
+
*/
|
|
509
|
+
textTextureCache = /* @__PURE__ */ new Map();
|
|
510
|
+
textTextureCacheLimit = 256;
|
|
500
511
|
fillText(text, x, y, font, color) {
|
|
501
512
|
if (typeof document === "undefined") return;
|
|
502
|
-
const canvas = document.createElement("canvas");
|
|
503
|
-
const ctx = canvas.getContext("2d");
|
|
504
|
-
ctx.font = font;
|
|
505
|
-
const width = Math.max(1, Math.ceil(ctx.measureText(text).width));
|
|
506
|
-
const fontSize = parseInt(font) || 16;
|
|
507
|
-
const height = Math.max(1, Math.ceil(fontSize * 1.5));
|
|
508
|
-
canvas.width = width;
|
|
509
|
-
canvas.height = height;
|
|
510
|
-
ctx.font = font;
|
|
511
513
|
let fillCol = "#ffffff";
|
|
512
514
|
if (typeof color === "string") {
|
|
513
515
|
fillCol = color;
|
|
@@ -517,11 +519,36 @@ var ThreeRenderer = class {
|
|
|
517
519
|
fillCol = grad.colorStops[0].color;
|
|
518
520
|
}
|
|
519
521
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
522
|
+
const cacheKey = `${font}|${fillCol}|${text}`;
|
|
523
|
+
let entry = this.textTextureCache.get(cacheKey);
|
|
524
|
+
if (entry) {
|
|
525
|
+
this.textTextureCache.delete(cacheKey);
|
|
526
|
+
this.textTextureCache.set(cacheKey, entry);
|
|
527
|
+
} else {
|
|
528
|
+
const canvas = document.createElement("canvas");
|
|
529
|
+
const ctx = canvas.getContext("2d");
|
|
530
|
+
ctx.font = font;
|
|
531
|
+
const width2 = Math.max(1, Math.ceil(ctx.measureText(text).width));
|
|
532
|
+
const fontSize2 = parseInt(font) || 16;
|
|
533
|
+
const height2 = Math.max(1, Math.ceil(fontSize2 * 1.5));
|
|
534
|
+
canvas.width = width2;
|
|
535
|
+
canvas.height = height2;
|
|
536
|
+
ctx.font = font;
|
|
537
|
+
ctx.fillStyle = fillCol;
|
|
538
|
+
ctx.textBaseline = "alphabetic";
|
|
539
|
+
ctx.fillText(text, 0, fontSize2);
|
|
540
|
+
const texture2 = new THREE.CanvasTexture(canvas);
|
|
541
|
+
texture2.minFilter = THREE.LinearFilter;
|
|
542
|
+
texture2.userData.vectoCached = true;
|
|
543
|
+
entry = { texture: texture2, width: width2, height: height2, fontSize: fontSize2 };
|
|
544
|
+
this.textTextureCache.set(cacheKey, entry);
|
|
545
|
+
while (this.textTextureCache.size > Math.max(1, this.textTextureCacheLimit)) {
|
|
546
|
+
const oldestKey = this.textTextureCache.keys().next().value;
|
|
547
|
+
this.textTextureCache.get(oldestKey).texture.dispose();
|
|
548
|
+
this.textTextureCache.delete(oldestKey);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const { texture, width, height, fontSize } = entry;
|
|
525
552
|
const material = new THREE.MeshBasicMaterial({
|
|
526
553
|
map: texture,
|
|
527
554
|
transparent: true,
|
|
@@ -551,7 +578,30 @@ var ThreeRenderer = class {
|
|
|
551
578
|
this.scene.add(mesh);
|
|
552
579
|
this.activeObjects.push(mesh);
|
|
553
580
|
}
|
|
581
|
+
frameDirty = false;
|
|
582
|
+
presentScheduled = false;
|
|
583
|
+
/**
|
|
584
|
+
* The Scene calls flush() around every non-batched node (it commits the
|
|
585
|
+
* Canvas2D circle batch there). Rendering the whole Three scene on each of
|
|
586
|
+
* those calls made a frame cost O(N²) in entity count, so flush() only marks
|
|
587
|
+
* the frame dirty; the actual GL render happens once, in {@link present}.
|
|
588
|
+
*
|
|
589
|
+
* A microtask fallback keeps older `@vectojs/core` Scenes (that never call
|
|
590
|
+
* `present()`) painting: the many same-tick flushes coalesce into one render.
|
|
591
|
+
*/
|
|
554
592
|
flush() {
|
|
593
|
+
this.frameDirty = true;
|
|
594
|
+
if (this.presentScheduled) return;
|
|
595
|
+
this.presentScheduled = true;
|
|
596
|
+
queueMicrotask(() => {
|
|
597
|
+
this.presentScheduled = false;
|
|
598
|
+
if (this.frameDirty && !this.disposed) this.present();
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
/** Render the accumulated frame once. Called by Scene at the end of each render pass. */
|
|
602
|
+
present() {
|
|
603
|
+
if (this.disposed) return;
|
|
604
|
+
this.frameDirty = false;
|
|
555
605
|
this.renderer.render(this.scene, this.camera);
|
|
556
606
|
}
|
|
557
607
|
createLinearGradient(x0, y0, x1, y1, colorStops) {
|
|
@@ -562,6 +612,10 @@ var ThreeRenderer = class {
|
|
|
562
612
|
if (this.disposed) return;
|
|
563
613
|
this.disposed = true;
|
|
564
614
|
this.disposeActiveObjects();
|
|
615
|
+
for (const entry of this.textTextureCache.values()) {
|
|
616
|
+
entry.texture.dispose();
|
|
617
|
+
}
|
|
618
|
+
this.textTextureCache.clear();
|
|
565
619
|
this.currentPath = null;
|
|
566
620
|
this.stack.length = 0;
|
|
567
621
|
this.alphaStack.length = 0;
|
package/dist/index.mjs
CHANGED
|
@@ -123,7 +123,7 @@ var ThreeRenderer = class {
|
|
|
123
123
|
const materials = Array.isArray(object.material) ? object.material : [object.material];
|
|
124
124
|
for (const material of materials) {
|
|
125
125
|
const map = material.map;
|
|
126
|
-
if (map && !disposedTextures.has(map)) {
|
|
126
|
+
if (map && !map.userData.vectoCached && !disposedTextures.has(map)) {
|
|
127
127
|
disposedTextures.add(map);
|
|
128
128
|
map.dispose();
|
|
129
129
|
}
|
|
@@ -180,7 +180,7 @@ var ThreeRenderer = class {
|
|
|
180
180
|
const minY = Math.min(...corners.map((point) => point.y));
|
|
181
181
|
const maxX = Math.max(...corners.map((point) => point.x));
|
|
182
182
|
const maxY = Math.max(...corners.map((point) => point.y));
|
|
183
|
-
const dpr =
|
|
183
|
+
const dpr = this.renderer.getPixelRatio() || 1;
|
|
184
184
|
const canvasHeight = this.renderer.domElement.height / dpr;
|
|
185
185
|
let scissorX = minX * dpr;
|
|
186
186
|
let scissorY = (canvasHeight - maxY) * dpr;
|
|
@@ -443,33 +443,35 @@ var ThreeRenderer = class {
|
|
|
443
443
|
color = grad.colorStops[0].color;
|
|
444
444
|
}
|
|
445
445
|
}
|
|
446
|
-
const points = this.currentPath.getPoints();
|
|
447
|
-
if (points.length === 0) return;
|
|
448
|
-
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
|
449
446
|
const parsed = parseThreeColor(color);
|
|
450
447
|
const opacity = this.globalAlpha * parsed.alpha;
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
448
|
+
for (const shape of this.currentPath.toShapes()) {
|
|
449
|
+
const points = shape.getPoints();
|
|
450
|
+
if (points.length < 2) continue;
|
|
451
|
+
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
|
452
|
+
const material = new THREE.LineBasicMaterial({
|
|
453
|
+
color: parsed.color,
|
|
454
|
+
transparent: opacity < 1,
|
|
455
|
+
opacity,
|
|
456
|
+
linewidth: lineWidth
|
|
457
|
+
});
|
|
458
|
+
const line = new THREE.Line(geometry, material);
|
|
459
|
+
line.applyMatrix4(this.matrix);
|
|
460
|
+
this.scene.add(line);
|
|
461
|
+
this.activeObjects.push(line);
|
|
462
|
+
}
|
|
461
463
|
}
|
|
464
|
+
/**
|
|
465
|
+
* Rasterized fillText textures keyed by `font|color|text`, LRU-evicted.
|
|
466
|
+
* HUD-style UIs redraw the same labels every frame; without the cache each
|
|
467
|
+
* call allocated a canvas, rasterized it, and uploaded a fresh GPU texture.
|
|
468
|
+
* Entries are skipped by {@link disposeActiveObjects} (flagged via
|
|
469
|
+
* `userData.vectoCached`) and released on eviction or {@link dispose}.
|
|
470
|
+
*/
|
|
471
|
+
textTextureCache = /* @__PURE__ */ new Map();
|
|
472
|
+
textTextureCacheLimit = 256;
|
|
462
473
|
fillText(text, x, y, font, color) {
|
|
463
474
|
if (typeof document === "undefined") return;
|
|
464
|
-
const canvas = document.createElement("canvas");
|
|
465
|
-
const ctx = canvas.getContext("2d");
|
|
466
|
-
ctx.font = font;
|
|
467
|
-
const width = Math.max(1, Math.ceil(ctx.measureText(text).width));
|
|
468
|
-
const fontSize = parseInt(font) || 16;
|
|
469
|
-
const height = Math.max(1, Math.ceil(fontSize * 1.5));
|
|
470
|
-
canvas.width = width;
|
|
471
|
-
canvas.height = height;
|
|
472
|
-
ctx.font = font;
|
|
473
475
|
let fillCol = "#ffffff";
|
|
474
476
|
if (typeof color === "string") {
|
|
475
477
|
fillCol = color;
|
|
@@ -479,11 +481,36 @@ var ThreeRenderer = class {
|
|
|
479
481
|
fillCol = grad.colorStops[0].color;
|
|
480
482
|
}
|
|
481
483
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
484
|
+
const cacheKey = `${font}|${fillCol}|${text}`;
|
|
485
|
+
let entry = this.textTextureCache.get(cacheKey);
|
|
486
|
+
if (entry) {
|
|
487
|
+
this.textTextureCache.delete(cacheKey);
|
|
488
|
+
this.textTextureCache.set(cacheKey, entry);
|
|
489
|
+
} else {
|
|
490
|
+
const canvas = document.createElement("canvas");
|
|
491
|
+
const ctx = canvas.getContext("2d");
|
|
492
|
+
ctx.font = font;
|
|
493
|
+
const width2 = Math.max(1, Math.ceil(ctx.measureText(text).width));
|
|
494
|
+
const fontSize2 = parseInt(font) || 16;
|
|
495
|
+
const height2 = Math.max(1, Math.ceil(fontSize2 * 1.5));
|
|
496
|
+
canvas.width = width2;
|
|
497
|
+
canvas.height = height2;
|
|
498
|
+
ctx.font = font;
|
|
499
|
+
ctx.fillStyle = fillCol;
|
|
500
|
+
ctx.textBaseline = "alphabetic";
|
|
501
|
+
ctx.fillText(text, 0, fontSize2);
|
|
502
|
+
const texture2 = new THREE.CanvasTexture(canvas);
|
|
503
|
+
texture2.minFilter = THREE.LinearFilter;
|
|
504
|
+
texture2.userData.vectoCached = true;
|
|
505
|
+
entry = { texture: texture2, width: width2, height: height2, fontSize: fontSize2 };
|
|
506
|
+
this.textTextureCache.set(cacheKey, entry);
|
|
507
|
+
while (this.textTextureCache.size > Math.max(1, this.textTextureCacheLimit)) {
|
|
508
|
+
const oldestKey = this.textTextureCache.keys().next().value;
|
|
509
|
+
this.textTextureCache.get(oldestKey).texture.dispose();
|
|
510
|
+
this.textTextureCache.delete(oldestKey);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const { texture, width, height, fontSize } = entry;
|
|
487
514
|
const material = new THREE.MeshBasicMaterial({
|
|
488
515
|
map: texture,
|
|
489
516
|
transparent: true,
|
|
@@ -513,7 +540,30 @@ var ThreeRenderer = class {
|
|
|
513
540
|
this.scene.add(mesh);
|
|
514
541
|
this.activeObjects.push(mesh);
|
|
515
542
|
}
|
|
543
|
+
frameDirty = false;
|
|
544
|
+
presentScheduled = false;
|
|
545
|
+
/**
|
|
546
|
+
* The Scene calls flush() around every non-batched node (it commits the
|
|
547
|
+
* Canvas2D circle batch there). Rendering the whole Three scene on each of
|
|
548
|
+
* those calls made a frame cost O(N²) in entity count, so flush() only marks
|
|
549
|
+
* the frame dirty; the actual GL render happens once, in {@link present}.
|
|
550
|
+
*
|
|
551
|
+
* A microtask fallback keeps older `@vectojs/core` Scenes (that never call
|
|
552
|
+
* `present()`) painting: the many same-tick flushes coalesce into one render.
|
|
553
|
+
*/
|
|
516
554
|
flush() {
|
|
555
|
+
this.frameDirty = true;
|
|
556
|
+
if (this.presentScheduled) return;
|
|
557
|
+
this.presentScheduled = true;
|
|
558
|
+
queueMicrotask(() => {
|
|
559
|
+
this.presentScheduled = false;
|
|
560
|
+
if (this.frameDirty && !this.disposed) this.present();
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
/** Render the accumulated frame once. Called by Scene at the end of each render pass. */
|
|
564
|
+
present() {
|
|
565
|
+
if (this.disposed) return;
|
|
566
|
+
this.frameDirty = false;
|
|
517
567
|
this.renderer.render(this.scene, this.camera);
|
|
518
568
|
}
|
|
519
569
|
createLinearGradient(x0, y0, x1, y1, colorStops) {
|
|
@@ -524,6 +574,10 @@ var ThreeRenderer = class {
|
|
|
524
574
|
if (this.disposed) return;
|
|
525
575
|
this.disposed = true;
|
|
526
576
|
this.disposeActiveObjects();
|
|
577
|
+
for (const entry of this.textTextureCache.values()) {
|
|
578
|
+
entry.texture.dispose();
|
|
579
|
+
}
|
|
580
|
+
this.textTextureCache.clear();
|
|
527
581
|
this.currentPath = null;
|
|
528
582
|
this.stack.length = 0;
|
|
529
583
|
this.alphaStack.length = 0;
|