@vectojs/three 0.1.2 → 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/dist/index.js CHANGED
@@ -39,6 +39,10 @@ module.exports = __toCommonJS(index_exports);
39
39
  // src/ThreeRenderer.ts
40
40
  var import_core = require("@vectojs/core");
41
41
  var THREE = __toESM(require("three"));
42
+ function parseThreeColor(value) {
43
+ const [r, g, b, alpha] = (0, import_core.parseColorToRGBA)(value);
44
+ return { color: new THREE.Color(r, g, b), alpha };
45
+ }
42
46
  var WebGLGradient = class {
43
47
  constructor(x0, y0, x1, y1, colorStops) {
44
48
  this.x0 = x0;
@@ -111,14 +115,16 @@ var ThreeRenderer = class {
111
115
  activeObjects = [];
112
116
  scissorStack = [];
113
117
  constructor(canvas) {
114
- this.width = window.innerWidth;
115
- this.height = window.innerHeight;
118
+ const cw = canvas.width > 0 ? canvas.width : typeof window !== "undefined" ? window.innerWidth : 1024;
119
+ const ch = canvas.height > 0 ? canvas.height : typeof window !== "undefined" ? window.innerHeight : 1024;
120
+ this.width = cw;
121
+ this.height = ch;
116
122
  this.scene = new THREE.Scene();
117
123
  this.camera = new THREE.OrthographicCamera(0, this.width, 0, this.height, 0.1, 1e3);
118
124
  this.camera.position.z = 1;
119
125
  this.renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true });
120
126
  this.renderer.setSize(this.width, this.height);
121
- this.renderer.setPixelRatio(window.devicePixelRatio || 1);
127
+ this.renderer.setPixelRatio(typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1);
122
128
  this.matrix = new THREE.Matrix4().identity();
123
129
  }
124
130
  resize(width, height) {
@@ -130,26 +136,42 @@ var ThreeRenderer = class {
130
136
  this.camera.updateProjectionMatrix();
131
137
  }
132
138
  clear() {
133
- for (const obj of this.activeObjects) {
134
- this.scene.remove(obj);
135
- if (obj instanceof THREE.Mesh || obj instanceof THREE.Line) {
136
- obj.geometry.dispose();
137
- if (Array.isArray(obj.material)) {
138
- for (const mat of obj.material) {
139
- mat.dispose();
140
- if (mat.map) mat.map.dispose();
141
- }
142
- } else {
143
- obj.material.dispose();
144
- if (obj.material.map) {
145
- obj.material.map.dispose();
146
- }
139
+ this.disposeActiveObjects();
140
+ this.currentPath = null;
141
+ this.matrix.identity();
142
+ this.stack.length = 0;
143
+ this.globalAlpha = 1;
144
+ this.alphaStack.length = 0;
145
+ this.scissorStack.length = 0;
146
+ this.renderer.clear();
147
+ this.renderer.setScissorTest(false);
148
+ }
149
+ disposeActiveObjects() {
150
+ const disposedGeometries = /* @__PURE__ */ new Set();
151
+ const disposedMaterials = /* @__PURE__ */ new Set();
152
+ const disposedTextures = /* @__PURE__ */ new Set();
153
+ for (const object of this.activeObjects) {
154
+ this.scene.remove(object);
155
+ if (!(object instanceof THREE.Mesh || object instanceof THREE.Line)) continue;
156
+ const geometry = object.geometry;
157
+ if (!disposedGeometries.has(geometry)) {
158
+ disposedGeometries.add(geometry);
159
+ geometry.dispose();
160
+ }
161
+ const materials = Array.isArray(object.material) ? object.material : [object.material];
162
+ for (const material of materials) {
163
+ const map = material.map;
164
+ if (map && !map.userData.vectoCached && !disposedTextures.has(map)) {
165
+ disposedTextures.add(map);
166
+ map.dispose();
167
+ }
168
+ if (!disposedMaterials.has(material)) {
169
+ disposedMaterials.add(material);
170
+ material.dispose();
147
171
  }
148
172
  }
149
173
  }
150
- this.activeObjects = [];
151
- this.renderer.clear();
152
- this.renderer.setScissorTest(false);
174
+ this.activeObjects.length = 0;
153
175
  }
154
176
  save() {
155
177
  this.stack.push(this.matrix.clone());
@@ -186,16 +208,33 @@ var ThreeRenderer = class {
186
208
  this.globalAlpha = alpha;
187
209
  }
188
210
  clip(x, y, width, height) {
189
- const pos = new THREE.Vector3(x, y, 0).applyMatrix4(this.matrix);
190
- const size = new THREE.Vector3(width, height, 0).applyMatrix4(this.matrix).sub(new THREE.Vector3(0, 0, 0).applyMatrix4(this.matrix));
191
- const dpr = window.devicePixelRatio || 1;
211
+ const corners = [
212
+ new THREE.Vector3(x, y, 0),
213
+ new THREE.Vector3(x + width, y, 0),
214
+ new THREE.Vector3(x, y + height, 0),
215
+ new THREE.Vector3(x + width, y + height, 0)
216
+ ].map((point) => point.applyMatrix4(this.matrix));
217
+ const minX = Math.min(...corners.map((point) => point.x));
218
+ const minY = Math.min(...corners.map((point) => point.y));
219
+ const maxX = Math.max(...corners.map((point) => point.x));
220
+ const maxY = Math.max(...corners.map((point) => point.y));
221
+ const dpr = this.renderer.getPixelRatio() || 1;
192
222
  const canvasHeight = this.renderer.domElement.height / dpr;
193
- this.renderer.setScissor(
194
- pos.x * dpr,
195
- (canvasHeight - (pos.y + size.y)) * dpr,
196
- size.x * dpr,
197
- size.y * dpr
198
- );
223
+ let scissorX = minX * dpr;
224
+ let scissorY = (canvasHeight - maxY) * dpr;
225
+ let scissorWidth = (maxX - minX) * dpr;
226
+ let scissorHeight = (maxY - minY) * dpr;
227
+ if (this.renderer.getScissorTest()) {
228
+ const current = new THREE.Vector4();
229
+ this.renderer.getScissor(current);
230
+ const right = Math.min(scissorX + scissorWidth, current.x + current.z);
231
+ const top = Math.min(scissorY + scissorHeight, current.y + current.w);
232
+ scissorX = Math.max(scissorX, current.x);
233
+ scissorY = Math.max(scissorY, current.y);
234
+ scissorWidth = Math.max(0, right - scissorX);
235
+ scissorHeight = Math.max(0, top - scissorY);
236
+ }
237
+ this.renderer.setScissor(scissorX, scissorY, scissorWidth, scissorHeight);
199
238
  this.renderer.setScissorTest(true);
200
239
  }
201
240
  beginPath() {
@@ -414,13 +453,15 @@ var ThreeRenderer = class {
414
453
  fillSolidShape(colorOrGradient) {
415
454
  if (!this.currentPath) return;
416
455
  const color = typeof colorOrGradient === "string" ? colorOrGradient : "#ffffff";
456
+ const parsed = parseThreeColor(color);
417
457
  const shapes = this.currentPath.toShapes();
418
458
  for (const shape of shapes) {
419
459
  const geometry = new THREE.ShapeGeometry(shape);
460
+ const opacity = this.globalAlpha * parsed.alpha;
420
461
  const material = new THREE.MeshBasicMaterial({
421
- color: new THREE.Color(color),
422
- transparent: this.globalAlpha < 1,
423
- opacity: this.globalAlpha,
462
+ color: parsed.color,
463
+ transparent: opacity < 1,
464
+ opacity,
424
465
  depthWrite: false
425
466
  });
426
467
  const mesh = new THREE.Mesh(geometry, material);
@@ -440,31 +481,35 @@ var ThreeRenderer = class {
440
481
  color = grad.colorStops[0].color;
441
482
  }
442
483
  }
443
- const points = this.currentPath.getPoints();
444
- if (points.length === 0) return;
445
- const geometry = new THREE.BufferGeometry().setFromPoints(points);
446
- const material = new THREE.LineBasicMaterial({
447
- color: new THREE.Color(color),
448
- transparent: this.globalAlpha < 1,
449
- opacity: this.globalAlpha,
450
- linewidth: lineWidth
451
- });
452
- const line = new THREE.Line(geometry, material);
453
- line.applyMatrix4(this.matrix);
454
- this.scene.add(line);
455
- this.activeObjects.push(line);
484
+ const parsed = parseThreeColor(color);
485
+ const opacity = this.globalAlpha * parsed.alpha;
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
+ }
456
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;
457
511
  fillText(text, x, y, font, color) {
458
512
  if (typeof document === "undefined") return;
459
- const canvas = document.createElement("canvas");
460
- const ctx = canvas.getContext("2d");
461
- ctx.font = font;
462
- const width = Math.max(1, Math.ceil(ctx.measureText(text).width));
463
- const fontSize = parseInt(font) || 16;
464
- const height = Math.max(1, Math.ceil(fontSize * 1.5));
465
- canvas.width = width;
466
- canvas.height = height;
467
- ctx.font = font;
468
513
  let fillCol = "#ffffff";
469
514
  if (typeof color === "string") {
470
515
  fillCol = color;
@@ -474,11 +519,36 @@ var ThreeRenderer = class {
474
519
  fillCol = grad.colorStops[0].color;
475
520
  }
476
521
  }
477
- ctx.fillStyle = fillCol;
478
- ctx.textBaseline = "alphabetic";
479
- ctx.fillText(text, 0, fontSize);
480
- const texture = new THREE.CanvasTexture(canvas);
481
- texture.minFilter = THREE.LinearFilter;
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;
482
552
  const material = new THREE.MeshBasicMaterial({
483
553
  map: texture,
484
554
  transparent: true,
@@ -494,10 +564,12 @@ var ThreeRenderer = class {
494
564
  }
495
565
  fillCircle(cx, cy, radius, color, alpha = 1) {
496
566
  const geometry = new THREE.CircleGeometry(radius, 32);
567
+ const parsed = parseThreeColor(color);
568
+ const opacity = this.globalAlpha * alpha * parsed.alpha;
497
569
  const material = new THREE.MeshBasicMaterial({
498
- color: new THREE.Color(color),
499
- transparent: this.globalAlpha * alpha < 1,
500
- opacity: this.globalAlpha * alpha,
570
+ color: parsed.color,
571
+ transparent: opacity < 1,
572
+ opacity,
501
573
  depthWrite: false
502
574
  });
503
575
  const mesh = new THREE.Mesh(geometry, material);
@@ -506,12 +578,51 @@ var ThreeRenderer = class {
506
578
  this.scene.add(mesh);
507
579
  this.activeObjects.push(mesh);
508
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
+ */
509
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;
510
605
  this.renderer.render(this.scene, this.camera);
511
606
  }
512
607
  createLinearGradient(x0, y0, x1, y1, colorStops) {
513
608
  return new WebGLGradient(x0, y0, x1, y1, colorStops);
514
609
  }
610
+ disposed = false;
611
+ dispose() {
612
+ if (this.disposed) return;
613
+ this.disposed = true;
614
+ this.disposeActiveObjects();
615
+ for (const entry of this.textTextureCache.values()) {
616
+ entry.texture.dispose();
617
+ }
618
+ this.textTextureCache.clear();
619
+ this.currentPath = null;
620
+ this.stack.length = 0;
621
+ this.alphaStack.length = 0;
622
+ this.scissorStack.length = 0;
623
+ this.renderer.setScissorTest(false);
624
+ this.renderer.dispose();
625
+ }
515
626
  };
516
627
 
517
628
  // src/ThreeAdapter.ts
@@ -528,8 +639,20 @@ var ThreeAdapter = class {
528
639
  mesh;
529
640
  /** Track hover states independently per pointerId for WebXR / Multi-Touch. */
530
641
  activePointers = /* @__PURE__ */ new Map();
642
+ /**
643
+ * Holds the original `vectoScene.render` reference so {@link dispose} can
644
+ * restore it. Without restoring, a later render call would write
645
+ * `needsUpdate` on a disposed (deleted) `CanvasTexture`, which Three.js
646
+ * flags with `"THREE.Texture: trying to use deleted texture"`.
647
+ */
648
+ _originalRender = null;
649
+ /** Tracks whether this adapter owns the canvas it is rendering to. */
650
+ _ownsCanvas;
651
+ _disposed = false;
531
652
  constructor(options) {
532
- this.canvas = options.canvas || (typeof document !== "undefined" ? document.createElement("canvas") : { width: options.width, height: options.height });
653
+ const optCanvas = options.canvas;
654
+ this._ownsCanvas = !optCanvas;
655
+ this.canvas = optCanvas || (typeof document !== "undefined" ? document.createElement("canvas") : { width: options.width, height: options.height });
533
656
  this.canvas.width = options.width;
534
657
  this.canvas.height = options.height;
535
658
  const sceneOptions = {
@@ -541,7 +664,8 @@ var ThreeAdapter = class {
541
664
  this.texture = new THREE2.CanvasTexture(this.canvas);
542
665
  this.texture.minFilter = THREE2.LinearFilter;
543
666
  this.texture.magFilter = THREE2.LinearFilter;
544
- const originalRender = this.vectoScene.render;
667
+ this._originalRender = this.vectoScene.render;
668
+ const originalRender = this._originalRender;
545
669
  this.vectoScene.render = (renderer, dt, time) => {
546
670
  originalRender.call(this.vectoScene, renderer, dt, time);
547
671
  this.texture.needsUpdate = true;
@@ -637,34 +761,51 @@ var ThreeAdapter = class {
637
761
  a11yEl.focus();
638
762
  }
639
763
  } else {
640
- const e = originalEvent instanceof MouseEvent ? originalEvent : void 0;
641
- const vectoEvent = new import_core2.VectoJSEvent(type, entity, e, type !== "pointerleave");
764
+ const vectoEvent = new import_core2.VectoJSEvent(type, entity, originalEvent, type !== "pointerleave", {
765
+ x,
766
+ y
767
+ });
642
768
  entity.dispatchEvent(vectoEvent);
643
769
  }
644
770
  }
645
771
  createDOMEvent(type, x, y, pointerId, originalEvent) {
772
+ let event;
646
773
  if (type === "wheel") {
647
774
  const wheelE = originalEvent instanceof WheelEvent ? originalEvent : void 0;
648
- return new WheelEvent("wheel", {
775
+ event = new WheelEvent("wheel", {
649
776
  clientX: x,
650
777
  clientY: y,
651
778
  deltaX: wheelE ? wheelE.deltaX : 0,
652
779
  deltaY: wheelE ? wheelE.deltaY : 0,
653
780
  deltaZ: wheelE ? wheelE.deltaZ : 0,
654
781
  deltaMode: wheelE ? wheelE.deltaMode : 0,
782
+ shiftKey: wheelE ? wheelE.shiftKey : false,
783
+ ctrlKey: wheelE ? wheelE.ctrlKey : false,
784
+ altKey: wheelE ? wheelE.altKey : false,
785
+ metaKey: wheelE ? wheelE.metaKey : false,
786
+ bubbles: true,
787
+ cancelable: true
788
+ });
789
+ } else {
790
+ event = new PointerEvent(type, {
791
+ clientX: x,
792
+ clientY: y,
793
+ button: originalEvent instanceof MouseEvent ? originalEvent.button : 0,
794
+ buttons: originalEvent instanceof MouseEvent ? originalEvent.buttons : 0,
795
+ pointerId,
796
+ shiftKey: originalEvent instanceof MouseEvent ? originalEvent.shiftKey : false,
797
+ ctrlKey: originalEvent instanceof MouseEvent ? originalEvent.ctrlKey : false,
798
+ altKey: originalEvent instanceof MouseEvent ? originalEvent.altKey : false,
799
+ metaKey: originalEvent instanceof MouseEvent ? originalEvent.metaKey : false,
655
800
  bubbles: true,
656
801
  cancelable: true
657
802
  });
658
803
  }
659
- return new PointerEvent(type, {
660
- clientX: x,
661
- clientY: y,
662
- button: originalEvent instanceof MouseEvent ? originalEvent.button : 0,
663
- buttons: originalEvent instanceof MouseEvent ? originalEvent.buttons : 0,
664
- pointerId,
665
- bubbles: true,
666
- cancelable: true
804
+ Object.defineProperties(event, {
805
+ vectoSceneX: { value: x },
806
+ vectoSceneY: { value: y }
667
807
  });
808
+ return event;
668
809
  }
669
810
  findEntityById(root, id) {
670
811
  if (root.id === id) return root;
@@ -687,6 +828,12 @@ var ThreeAdapter = class {
687
828
  * Disposes of Three.js textures, geometries, and VectoJS scenes to prevent memory leaks.
688
829
  */
689
830
  dispose() {
831
+ if (this._disposed) return;
832
+ this._disposed = true;
833
+ if (this._originalRender) {
834
+ this.vectoScene.render = this._originalRender;
835
+ this._originalRender = null;
836
+ }
690
837
  this.texture.dispose();
691
838
  this.mesh.geometry.dispose();
692
839
  if (Array.isArray(this.mesh.material)) {
@@ -699,8 +846,10 @@ var ThreeAdapter = class {
699
846
  }
700
847
  this.vectoScene.destroy();
701
848
  this.activePointers.clear();
702
- this.canvas.width = 0;
703
- this.canvas.height = 0;
849
+ if (this._ownsCanvas) {
850
+ this.canvas.width = 0;
851
+ this.canvas.height = 0;
852
+ }
704
853
  }
705
854
  };
706
855
  // Annotate the CommonJS export names for ESM import in node: