@vectojs/three 0.1.3 → 0.1.5
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 +32 -0
- package/dist/index.js +114 -36
- package/dist/index.mjs +114 -36
- 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
|
@@ -51,13 +51,45 @@ export declare class ThreeRenderer implements IRenderer {
|
|
|
51
51
|
closePath(): void;
|
|
52
52
|
arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, counterclockwise?: boolean): void;
|
|
53
53
|
roundRect(x: number, y: number, width: number, height: number, radii: number | number[]): void;
|
|
54
|
+
/**
|
|
55
|
+
* Textures cached per drawImage source (identity-keyed). A frame that
|
|
56
|
+
* redraws the same image no longer re-uploads it. Mutable sources (a canvas
|
|
57
|
+
* you repaint) must call {@link invalidateImage} after changing pixels.
|
|
58
|
+
* Entries are skipped by the per-frame disposer and released on
|
|
59
|
+
* invalidation or {@link dispose}.
|
|
60
|
+
*/
|
|
61
|
+
private imageTextureCache;
|
|
62
|
+
/** Drop the cached texture for a source whose pixels changed. */
|
|
63
|
+
invalidateImage(source: CanvasImageSource): void;
|
|
54
64
|
drawImage(source: CanvasImageSource, dx: number, dy: number, dw: number, dh: number): void;
|
|
55
65
|
fill(colorOrGradient: string | any): void;
|
|
56
66
|
private fillSolidShape;
|
|
57
67
|
stroke(colorOrGradient: string | any, lineWidth?: number): void;
|
|
68
|
+
/**
|
|
69
|
+
* Rasterized fillText textures keyed by `font|color|text`, LRU-evicted.
|
|
70
|
+
* HUD-style UIs redraw the same labels every frame; without the cache each
|
|
71
|
+
* call allocated a canvas, rasterized it, and uploaded a fresh GPU texture.
|
|
72
|
+
* Entries are skipped by {@link disposeActiveObjects} (flagged via
|
|
73
|
+
* `userData.vectoCached`) and released on eviction or {@link dispose}.
|
|
74
|
+
*/
|
|
75
|
+
private textTextureCache;
|
|
76
|
+
private textTextureCacheLimit;
|
|
58
77
|
fillText(text: string, x: number, y: number, font: string, color: string | any): void;
|
|
59
78
|
fillCircle(cx: number, cy: number, radius: number, color: string, alpha?: number): void;
|
|
79
|
+
private frameDirty;
|
|
80
|
+
private presentScheduled;
|
|
81
|
+
/**
|
|
82
|
+
* The Scene calls flush() around every non-batched node (it commits the
|
|
83
|
+
* Canvas2D circle batch there). Rendering the whole Three scene on each of
|
|
84
|
+
* those calls made a frame cost O(N²) in entity count, so flush() only marks
|
|
85
|
+
* the frame dirty; the actual GL render happens once, in {@link present}.
|
|
86
|
+
*
|
|
87
|
+
* A microtask fallback keeps older `@vectojs/core` Scenes (that never call
|
|
88
|
+
* `present()`) painting: the many same-tick flushes coalesce into one render.
|
|
89
|
+
*/
|
|
60
90
|
flush(): void;
|
|
91
|
+
/** Render the accumulated frame once. Called by Scene at the end of each render pass. */
|
|
92
|
+
present(): void;
|
|
61
93
|
createLinearGradient(x0: number, y0: number, x1: number, y1: number, colorStops: {
|
|
62
94
|
stop: number;
|
|
63
95
|
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;
|
|
@@ -310,15 +310,35 @@ var ThreeRenderer = class {
|
|
|
310
310
|
this.currentPath.lineTo(x, y + r_tl);
|
|
311
311
|
this.currentPath.arc(x + r_tl, y + r_tl, r_tl, Math.PI, -Math.PI / 2, false);
|
|
312
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* Textures cached per drawImage source (identity-keyed). A frame that
|
|
315
|
+
* redraws the same image no longer re-uploads it. Mutable sources (a canvas
|
|
316
|
+
* you repaint) must call {@link invalidateImage} after changing pixels.
|
|
317
|
+
* Entries are skipped by the per-frame disposer and released on
|
|
318
|
+
* invalidation or {@link dispose}.
|
|
319
|
+
*/
|
|
320
|
+
imageTextureCache = /* @__PURE__ */ new Map();
|
|
321
|
+
/** Drop the cached texture for a source whose pixels changed. */
|
|
322
|
+
invalidateImage(source) {
|
|
323
|
+
const cached = this.imageTextureCache.get(source);
|
|
324
|
+
if (cached) {
|
|
325
|
+
cached.dispose();
|
|
326
|
+
this.imageTextureCache.delete(source);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
313
329
|
drawImage(source, dx, dy, dw, dh) {
|
|
314
|
-
let texture;
|
|
315
|
-
if (
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
330
|
+
let texture = this.imageTextureCache.get(source);
|
|
331
|
+
if (!texture) {
|
|
332
|
+
if (source instanceof HTMLImageElement || source instanceof HTMLCanvasElement || typeof ImageBitmap !== "undefined" && source instanceof ImageBitmap) {
|
|
333
|
+
texture = new THREE.CanvasTexture(source);
|
|
334
|
+
} else {
|
|
335
|
+
texture = new THREE.Texture(source);
|
|
336
|
+
texture.needsUpdate = true;
|
|
337
|
+
}
|
|
338
|
+
texture.minFilter = THREE.LinearFilter;
|
|
339
|
+
texture.userData.vectoCached = true;
|
|
340
|
+
this.imageTextureCache.set(source, texture);
|
|
320
341
|
}
|
|
321
|
-
texture.minFilter = THREE.LinearFilter;
|
|
322
342
|
const material = new THREE.MeshBasicMaterial({
|
|
323
343
|
map: texture,
|
|
324
344
|
transparent: true,
|
|
@@ -481,33 +501,35 @@ var ThreeRenderer = class {
|
|
|
481
501
|
color = grad.colorStops[0].color;
|
|
482
502
|
}
|
|
483
503
|
}
|
|
484
|
-
const points = this.currentPath.getPoints();
|
|
485
|
-
if (points.length === 0) return;
|
|
486
|
-
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
|
487
504
|
const parsed = parseThreeColor(color);
|
|
488
505
|
const opacity = this.globalAlpha * parsed.alpha;
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
506
|
+
for (const shape of this.currentPath.toShapes()) {
|
|
507
|
+
const points = shape.getPoints();
|
|
508
|
+
if (points.length < 2) continue;
|
|
509
|
+
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
|
510
|
+
const material = new THREE.LineBasicMaterial({
|
|
511
|
+
color: parsed.color,
|
|
512
|
+
transparent: opacity < 1,
|
|
513
|
+
opacity,
|
|
514
|
+
linewidth: lineWidth
|
|
515
|
+
});
|
|
516
|
+
const line = new THREE.Line(geometry, material);
|
|
517
|
+
line.applyMatrix4(this.matrix);
|
|
518
|
+
this.scene.add(line);
|
|
519
|
+
this.activeObjects.push(line);
|
|
520
|
+
}
|
|
499
521
|
}
|
|
522
|
+
/**
|
|
523
|
+
* Rasterized fillText textures keyed by `font|color|text`, LRU-evicted.
|
|
524
|
+
* HUD-style UIs redraw the same labels every frame; without the cache each
|
|
525
|
+
* call allocated a canvas, rasterized it, and uploaded a fresh GPU texture.
|
|
526
|
+
* Entries are skipped by {@link disposeActiveObjects} (flagged via
|
|
527
|
+
* `userData.vectoCached`) and released on eviction or {@link dispose}.
|
|
528
|
+
*/
|
|
529
|
+
textTextureCache = /* @__PURE__ */ new Map();
|
|
530
|
+
textTextureCacheLimit = 256;
|
|
500
531
|
fillText(text, x, y, font, color) {
|
|
501
532
|
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
533
|
let fillCol = "#ffffff";
|
|
512
534
|
if (typeof color === "string") {
|
|
513
535
|
fillCol = color;
|
|
@@ -517,11 +539,36 @@ var ThreeRenderer = class {
|
|
|
517
539
|
fillCol = grad.colorStops[0].color;
|
|
518
540
|
}
|
|
519
541
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
542
|
+
const cacheKey = `${font}|${fillCol}|${text}`;
|
|
543
|
+
let entry = this.textTextureCache.get(cacheKey);
|
|
544
|
+
if (entry) {
|
|
545
|
+
this.textTextureCache.delete(cacheKey);
|
|
546
|
+
this.textTextureCache.set(cacheKey, entry);
|
|
547
|
+
} else {
|
|
548
|
+
const canvas = document.createElement("canvas");
|
|
549
|
+
const ctx = canvas.getContext("2d");
|
|
550
|
+
ctx.font = font;
|
|
551
|
+
const width2 = Math.max(1, Math.ceil(ctx.measureText(text).width));
|
|
552
|
+
const fontSize2 = parseInt(font) || 16;
|
|
553
|
+
const height2 = Math.max(1, Math.ceil(fontSize2 * 1.5));
|
|
554
|
+
canvas.width = width2;
|
|
555
|
+
canvas.height = height2;
|
|
556
|
+
ctx.font = font;
|
|
557
|
+
ctx.fillStyle = fillCol;
|
|
558
|
+
ctx.textBaseline = "alphabetic";
|
|
559
|
+
ctx.fillText(text, 0, fontSize2);
|
|
560
|
+
const texture2 = new THREE.CanvasTexture(canvas);
|
|
561
|
+
texture2.minFilter = THREE.LinearFilter;
|
|
562
|
+
texture2.userData.vectoCached = true;
|
|
563
|
+
entry = { texture: texture2, width: width2, height: height2, fontSize: fontSize2 };
|
|
564
|
+
this.textTextureCache.set(cacheKey, entry);
|
|
565
|
+
while (this.textTextureCache.size > Math.max(1, this.textTextureCacheLimit)) {
|
|
566
|
+
const oldestKey = this.textTextureCache.keys().next().value;
|
|
567
|
+
this.textTextureCache.get(oldestKey).texture.dispose();
|
|
568
|
+
this.textTextureCache.delete(oldestKey);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
const { texture, width, height, fontSize } = entry;
|
|
525
572
|
const material = new THREE.MeshBasicMaterial({
|
|
526
573
|
map: texture,
|
|
527
574
|
transparent: true,
|
|
@@ -551,7 +598,30 @@ var ThreeRenderer = class {
|
|
|
551
598
|
this.scene.add(mesh);
|
|
552
599
|
this.activeObjects.push(mesh);
|
|
553
600
|
}
|
|
601
|
+
frameDirty = false;
|
|
602
|
+
presentScheduled = false;
|
|
603
|
+
/**
|
|
604
|
+
* The Scene calls flush() around every non-batched node (it commits the
|
|
605
|
+
* Canvas2D circle batch there). Rendering the whole Three scene on each of
|
|
606
|
+
* those calls made a frame cost O(N²) in entity count, so flush() only marks
|
|
607
|
+
* the frame dirty; the actual GL render happens once, in {@link present}.
|
|
608
|
+
*
|
|
609
|
+
* A microtask fallback keeps older `@vectojs/core` Scenes (that never call
|
|
610
|
+
* `present()`) painting: the many same-tick flushes coalesce into one render.
|
|
611
|
+
*/
|
|
554
612
|
flush() {
|
|
613
|
+
this.frameDirty = true;
|
|
614
|
+
if (this.presentScheduled) return;
|
|
615
|
+
this.presentScheduled = true;
|
|
616
|
+
queueMicrotask(() => {
|
|
617
|
+
this.presentScheduled = false;
|
|
618
|
+
if (this.frameDirty && !this.disposed) this.present();
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
/** Render the accumulated frame once. Called by Scene at the end of each render pass. */
|
|
622
|
+
present() {
|
|
623
|
+
if (this.disposed) return;
|
|
624
|
+
this.frameDirty = false;
|
|
555
625
|
this.renderer.render(this.scene, this.camera);
|
|
556
626
|
}
|
|
557
627
|
createLinearGradient(x0, y0, x1, y1, colorStops) {
|
|
@@ -562,6 +632,14 @@ var ThreeRenderer = class {
|
|
|
562
632
|
if (this.disposed) return;
|
|
563
633
|
this.disposed = true;
|
|
564
634
|
this.disposeActiveObjects();
|
|
635
|
+
for (const entry of this.textTextureCache.values()) {
|
|
636
|
+
entry.texture.dispose();
|
|
637
|
+
}
|
|
638
|
+
this.textTextureCache.clear();
|
|
639
|
+
for (const texture of this.imageTextureCache.values()) {
|
|
640
|
+
texture.dispose();
|
|
641
|
+
}
|
|
642
|
+
this.imageTextureCache.clear();
|
|
565
643
|
this.currentPath = null;
|
|
566
644
|
this.stack.length = 0;
|
|
567
645
|
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;
|
|
@@ -272,15 +272,35 @@ var ThreeRenderer = class {
|
|
|
272
272
|
this.currentPath.lineTo(x, y + r_tl);
|
|
273
273
|
this.currentPath.arc(x + r_tl, y + r_tl, r_tl, Math.PI, -Math.PI / 2, false);
|
|
274
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* Textures cached per drawImage source (identity-keyed). A frame that
|
|
277
|
+
* redraws the same image no longer re-uploads it. Mutable sources (a canvas
|
|
278
|
+
* you repaint) must call {@link invalidateImage} after changing pixels.
|
|
279
|
+
* Entries are skipped by the per-frame disposer and released on
|
|
280
|
+
* invalidation or {@link dispose}.
|
|
281
|
+
*/
|
|
282
|
+
imageTextureCache = /* @__PURE__ */ new Map();
|
|
283
|
+
/** Drop the cached texture for a source whose pixels changed. */
|
|
284
|
+
invalidateImage(source) {
|
|
285
|
+
const cached = this.imageTextureCache.get(source);
|
|
286
|
+
if (cached) {
|
|
287
|
+
cached.dispose();
|
|
288
|
+
this.imageTextureCache.delete(source);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
275
291
|
drawImage(source, dx, dy, dw, dh) {
|
|
276
|
-
let texture;
|
|
277
|
-
if (
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
292
|
+
let texture = this.imageTextureCache.get(source);
|
|
293
|
+
if (!texture) {
|
|
294
|
+
if (source instanceof HTMLImageElement || source instanceof HTMLCanvasElement || typeof ImageBitmap !== "undefined" && source instanceof ImageBitmap) {
|
|
295
|
+
texture = new THREE.CanvasTexture(source);
|
|
296
|
+
} else {
|
|
297
|
+
texture = new THREE.Texture(source);
|
|
298
|
+
texture.needsUpdate = true;
|
|
299
|
+
}
|
|
300
|
+
texture.minFilter = THREE.LinearFilter;
|
|
301
|
+
texture.userData.vectoCached = true;
|
|
302
|
+
this.imageTextureCache.set(source, texture);
|
|
282
303
|
}
|
|
283
|
-
texture.minFilter = THREE.LinearFilter;
|
|
284
304
|
const material = new THREE.MeshBasicMaterial({
|
|
285
305
|
map: texture,
|
|
286
306
|
transparent: true,
|
|
@@ -443,33 +463,35 @@ var ThreeRenderer = class {
|
|
|
443
463
|
color = grad.colorStops[0].color;
|
|
444
464
|
}
|
|
445
465
|
}
|
|
446
|
-
const points = this.currentPath.getPoints();
|
|
447
|
-
if (points.length === 0) return;
|
|
448
|
-
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
|
449
466
|
const parsed = parseThreeColor(color);
|
|
450
467
|
const opacity = this.globalAlpha * parsed.alpha;
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
468
|
+
for (const shape of this.currentPath.toShapes()) {
|
|
469
|
+
const points = shape.getPoints();
|
|
470
|
+
if (points.length < 2) continue;
|
|
471
|
+
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
|
472
|
+
const material = new THREE.LineBasicMaterial({
|
|
473
|
+
color: parsed.color,
|
|
474
|
+
transparent: opacity < 1,
|
|
475
|
+
opacity,
|
|
476
|
+
linewidth: lineWidth
|
|
477
|
+
});
|
|
478
|
+
const line = new THREE.Line(geometry, material);
|
|
479
|
+
line.applyMatrix4(this.matrix);
|
|
480
|
+
this.scene.add(line);
|
|
481
|
+
this.activeObjects.push(line);
|
|
482
|
+
}
|
|
461
483
|
}
|
|
484
|
+
/**
|
|
485
|
+
* Rasterized fillText textures keyed by `font|color|text`, LRU-evicted.
|
|
486
|
+
* HUD-style UIs redraw the same labels every frame; without the cache each
|
|
487
|
+
* call allocated a canvas, rasterized it, and uploaded a fresh GPU texture.
|
|
488
|
+
* Entries are skipped by {@link disposeActiveObjects} (flagged via
|
|
489
|
+
* `userData.vectoCached`) and released on eviction or {@link dispose}.
|
|
490
|
+
*/
|
|
491
|
+
textTextureCache = /* @__PURE__ */ new Map();
|
|
492
|
+
textTextureCacheLimit = 256;
|
|
462
493
|
fillText(text, x, y, font, color) {
|
|
463
494
|
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
495
|
let fillCol = "#ffffff";
|
|
474
496
|
if (typeof color === "string") {
|
|
475
497
|
fillCol = color;
|
|
@@ -479,11 +501,36 @@ var ThreeRenderer = class {
|
|
|
479
501
|
fillCol = grad.colorStops[0].color;
|
|
480
502
|
}
|
|
481
503
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
504
|
+
const cacheKey = `${font}|${fillCol}|${text}`;
|
|
505
|
+
let entry = this.textTextureCache.get(cacheKey);
|
|
506
|
+
if (entry) {
|
|
507
|
+
this.textTextureCache.delete(cacheKey);
|
|
508
|
+
this.textTextureCache.set(cacheKey, entry);
|
|
509
|
+
} else {
|
|
510
|
+
const canvas = document.createElement("canvas");
|
|
511
|
+
const ctx = canvas.getContext("2d");
|
|
512
|
+
ctx.font = font;
|
|
513
|
+
const width2 = Math.max(1, Math.ceil(ctx.measureText(text).width));
|
|
514
|
+
const fontSize2 = parseInt(font) || 16;
|
|
515
|
+
const height2 = Math.max(1, Math.ceil(fontSize2 * 1.5));
|
|
516
|
+
canvas.width = width2;
|
|
517
|
+
canvas.height = height2;
|
|
518
|
+
ctx.font = font;
|
|
519
|
+
ctx.fillStyle = fillCol;
|
|
520
|
+
ctx.textBaseline = "alphabetic";
|
|
521
|
+
ctx.fillText(text, 0, fontSize2);
|
|
522
|
+
const texture2 = new THREE.CanvasTexture(canvas);
|
|
523
|
+
texture2.minFilter = THREE.LinearFilter;
|
|
524
|
+
texture2.userData.vectoCached = true;
|
|
525
|
+
entry = { texture: texture2, width: width2, height: height2, fontSize: fontSize2 };
|
|
526
|
+
this.textTextureCache.set(cacheKey, entry);
|
|
527
|
+
while (this.textTextureCache.size > Math.max(1, this.textTextureCacheLimit)) {
|
|
528
|
+
const oldestKey = this.textTextureCache.keys().next().value;
|
|
529
|
+
this.textTextureCache.get(oldestKey).texture.dispose();
|
|
530
|
+
this.textTextureCache.delete(oldestKey);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
const { texture, width, height, fontSize } = entry;
|
|
487
534
|
const material = new THREE.MeshBasicMaterial({
|
|
488
535
|
map: texture,
|
|
489
536
|
transparent: true,
|
|
@@ -513,7 +560,30 @@ var ThreeRenderer = class {
|
|
|
513
560
|
this.scene.add(mesh);
|
|
514
561
|
this.activeObjects.push(mesh);
|
|
515
562
|
}
|
|
563
|
+
frameDirty = false;
|
|
564
|
+
presentScheduled = false;
|
|
565
|
+
/**
|
|
566
|
+
* The Scene calls flush() around every non-batched node (it commits the
|
|
567
|
+
* Canvas2D circle batch there). Rendering the whole Three scene on each of
|
|
568
|
+
* those calls made a frame cost O(N²) in entity count, so flush() only marks
|
|
569
|
+
* the frame dirty; the actual GL render happens once, in {@link present}.
|
|
570
|
+
*
|
|
571
|
+
* A microtask fallback keeps older `@vectojs/core` Scenes (that never call
|
|
572
|
+
* `present()`) painting: the many same-tick flushes coalesce into one render.
|
|
573
|
+
*/
|
|
516
574
|
flush() {
|
|
575
|
+
this.frameDirty = true;
|
|
576
|
+
if (this.presentScheduled) return;
|
|
577
|
+
this.presentScheduled = true;
|
|
578
|
+
queueMicrotask(() => {
|
|
579
|
+
this.presentScheduled = false;
|
|
580
|
+
if (this.frameDirty && !this.disposed) this.present();
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
/** Render the accumulated frame once. Called by Scene at the end of each render pass. */
|
|
584
|
+
present() {
|
|
585
|
+
if (this.disposed) return;
|
|
586
|
+
this.frameDirty = false;
|
|
517
587
|
this.renderer.render(this.scene, this.camera);
|
|
518
588
|
}
|
|
519
589
|
createLinearGradient(x0, y0, x1, y1, colorStops) {
|
|
@@ -524,6 +594,14 @@ var ThreeRenderer = class {
|
|
|
524
594
|
if (this.disposed) return;
|
|
525
595
|
this.disposed = true;
|
|
526
596
|
this.disposeActiveObjects();
|
|
597
|
+
for (const entry of this.textTextureCache.values()) {
|
|
598
|
+
entry.texture.dispose();
|
|
599
|
+
}
|
|
600
|
+
this.textTextureCache.clear();
|
|
601
|
+
for (const texture of this.imageTextureCache.values()) {
|
|
602
|
+
texture.dispose();
|
|
603
|
+
}
|
|
604
|
+
this.imageTextureCache.clear();
|
|
527
605
|
this.currentPath = null;
|
|
528
606
|
this.stack.length = 0;
|
|
529
607
|
this.alphaStack.length = 0;
|