@expofp/renderer 1.4.2 → 2.0.0

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +112 -32
  2. package/dist/index.js +1578 -1202
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
  var _a;
5
- import { DataTexture, FloatType, UnsignedIntType, IntType, RGBAFormat, RGBAIntegerFormat, RGFormat, RGIntegerFormat, RedFormat, RedIntegerFormat, BatchedMesh as BatchedMesh$1, BufferAttribute, StreamDrawUsage, Color, Matrix4, Vector3, Vector4, DoubleSide, MeshBasicMaterial, Texture, Quaternion, Group, PlaneGeometry, SRGBColorSpace, Vector2, BufferGeometry, LinearSRGBColorSpace, Mesh, Plane, Raycaster, Sphere, Box3, Spherical, PerspectiveCamera, Scene, Camera, MathUtils, Clock, WebGLRenderer } from "three";
5
+ import { Color, Matrix4, Vector3, DataTexture, RGBAFormat, FloatType, RedFormat, UnsignedIntType, IntType, RGBAIntegerFormat, RGFormat, RGIntegerFormat, RedIntegerFormat, BatchedMesh as BatchedMesh$1, BufferAttribute, StreamDrawUsage, Vector4, AlwaysDepth, DoubleSide, MeshBasicMaterial, Texture, Group, PlaneGeometry, SRGBColorSpace, Vector2, Mesh, LessEqualDepth, Quaternion, BufferGeometry, LinearSRGBColorSpace, Plane, Raycaster, Sphere, Box3, Spherical, PerspectiveCamera, Camera, Scene, MathUtils, Clock, WebGLRenderer } from "three";
6
6
  import { traverseAncestorsGenerator } from "three/examples/jsm/utils/SceneUtils.js";
7
7
  import { BatchedText as BatchedText$1, Text as Text$1 } from "troika-three-text";
8
8
  import { LineMaterial, LineSegmentsGeometry } from "three/examples/jsm/Addons.js";
@@ -10,351 +10,703 @@ import { MaxRectsPacker, Rectangle } from "maxrects-packer";
10
10
  import { converter, parse } from "culori";
11
11
  import { RAD2DEG, DEG2RAD as DEG2RAD$1 } from "three/src/math/MathUtils.js";
12
12
  import { EventManager, Rotate, Pan } from "mjolnir.js";
13
- function isObject(item) {
14
- return !!item && typeof item === "object" && !Array.isArray(item);
15
- }
16
- function deepMerge(target, ...sources) {
17
- if (!sources.length) return target;
18
- const source = sources.shift();
19
- if (source === void 0) {
20
- return target;
21
- }
22
- if (isObject(target) && isObject(source)) {
23
- for (const key in source) {
24
- if (isObject(source[key])) {
25
- if (!target[key]) {
26
- Object.assign(target, { [key]: {} });
27
- }
28
- deepMerge(target[key], source[key]);
29
- } else {
30
- Object.assign(target, { [key]: source[key] });
31
- }
32
- }
13
+ const floatsPerMember = 32;
14
+ const tempColor = new Color();
15
+ const defaultStrokeColor = 8421504;
16
+ const tempMat4 = new Matrix4();
17
+ const tempVec3a = new Vector3();
18
+ const tempVec3b = new Vector3();
19
+ const origin = new Vector3();
20
+ const defaultOrient = "+x+y";
21
+ class BatchedText extends BatchedText$1 {
22
+ // eslint-disable-next-line jsdoc/require-jsdoc
23
+ constructor() {
24
+ super();
25
+ __publicField(this, "mapInstanceIdToText", /* @__PURE__ */ new Map());
26
+ __publicField(this, "textArray", []);
27
+ __publicField(this, "textureNeedsUpdate", false);
33
28
  }
34
- return deepMerge(target, ...sources);
35
- }
36
- function getSquareTextureSize(capacity, pixelsPerInstance) {
37
- return Math.max(pixelsPerInstance, Math.ceil(Math.sqrt(capacity / pixelsPerInstance)) * pixelsPerInstance);
38
- }
39
- function getSquareTextureInfo(arrayType, channels, pixelsPerInstance, capacity) {
40
- if (channels === 3) {
41
- console.warn('"channels" cannot be 3. Set to 4. More info: https://github.com/mrdoob/three.js/pull/23228');
42
- channels = 4;
29
+ /** Number of texts in the batch */
30
+ get size() {
31
+ return this._members.size;
43
32
  }
44
- const size = getSquareTextureSize(capacity, pixelsPerInstance);
45
- const array = new arrayType(size * size * channels);
46
- const isFloat = arrayType.name.includes("Float");
47
- const isUnsignedInt = arrayType.name.includes("Uint");
48
- const type = isFloat ? FloatType : isUnsignedInt ? UnsignedIntType : IntType;
49
- let format;
50
- switch (channels) {
51
- case 1:
52
- format = isFloat ? RedFormat : RedIntegerFormat;
53
- break;
54
- case 2:
55
- format = isFloat ? RGFormat : RGIntegerFormat;
56
- break;
57
- case 4:
58
- format = isFloat ? RGBAFormat : RGBAIntegerFormat;
59
- break;
33
+ /** Base material before patching */
34
+ get baseMaterial() {
35
+ return this._baseMaterial;
60
36
  }
61
- return { array, size, type, format };
62
- }
63
- class SquareDataTexture extends DataTexture {
64
37
  /**
65
- * @param arrayType The constructor for the TypedArray.
66
- * @param channels The number of channels in the texture.
67
- * @param pixelsPerInstance The number of pixels required for each instance.
68
- * @param capacity The total number of instances.
69
- * @param uniformMap Optional map for handling uniform values.
70
- * @param fetchInFragmentShader Optional flag that determines if uniform values should be fetched in the fragment shader instead of the vertex shader.
38
+ * Get the {@link Text} object by instance id
39
+ * @param instanceId Instance id
40
+ * @returns Text object
71
41
  */
72
- constructor(arrayType, channels, pixelsPerInstance, capacity, uniformMap, fetchInFragmentShader) {
73
- if (channels === 3) channels = 4;
74
- const { array, format, size, type } = getSquareTextureInfo(arrayType, channels, pixelsPerInstance, capacity);
75
- super(array, size, size, format, type);
76
- __publicField(this, "data");
77
- __publicField(this, "channels");
78
- __publicField(this, "pixelsPerInstance");
79
- __publicField(this, "stride");
80
- __publicField(this, "uniformMap");
81
- __publicField(this, "fetchUniformsInFragmentShader");
82
- __publicField(this, "uniformPrefix", "batch_");
83
- this.data = array;
84
- this.channels = channels;
85
- this.pixelsPerInstance = pixelsPerInstance;
86
- this.stride = pixelsPerInstance * channels;
87
- this.uniformMap = uniformMap;
88
- this.fetchUniformsInFragmentShader = fetchInFragmentShader;
89
- this.needsUpdate = true;
42
+ getText(instanceId) {
43
+ return this.mapInstanceIdToText.get(instanceId);
90
44
  }
91
45
  /**
92
- * Sets a uniform value at the specified instance ID in the texture.
93
- * @param id The instance ID to set the uniform for.
94
- * @param name The name of the uniform.
95
- * @param value The value to set for the uniform.
46
+ * Set the visibility of the {@link Text} object by instance id.
47
+ * This is for interface compatibility with {@link BatchedMesh}.
48
+ * @param instanceId Instance id
49
+ * @param visible Visibility flag
96
50
  */
97
- setUniformAt(id, name, value) {
98
- const schema = this.uniformMap.get(name);
99
- if (!schema) {
100
- console.warn(`SquareDataTexture.setUniformAt: uniform ${name} not found`);
101
- return;
102
- }
103
- const { offset, size } = schema;
104
- const stride = this.stride;
105
- if (size === 1) {
106
- this.data[id * stride + offset] = value;
107
- } else {
108
- value.toArray(this.data, id * stride + offset);
109
- }
51
+ setVisibleAt(instanceId, visible) {
52
+ const text = this.getText(instanceId);
53
+ text.visible = visible;
110
54
  }
111
- /**
112
- * Retrieves a uniform value at the specified instance ID from the texture.
113
- * @param id The instance ID to retrieve the uniform from.
114
- * @param name The name of the uniform.
115
- * @param target Optional target object to store the uniform value.
116
- * @returns The uniform value for the specified instance.
117
- */
118
- getUniformAt(id, name, target) {
119
- const schema = this.uniformMap.get(name);
120
- if (!schema) {
121
- console.warn(`SquareDataTexture.getUniformAt: uniform ${name} not found`);
122
- return 0;
123
- }
124
- const { offset, size } = schema;
125
- const stride = this.stride;
126
- if (size === 1) {
127
- return this.data[id * stride + offset];
55
+ addText(text, instanceId) {
56
+ super.addText(text);
57
+ if (instanceId !== void 0) {
58
+ this.mapInstanceIdToText.set(instanceId, text);
128
59
  }
129
- return target.fromArray(this.data, id * stride + offset);
130
- }
131
- /** Mark the texture as needing an update. */
132
- update() {
133
- this.needsUpdate = true;
134
- }
135
- /**
136
- * Generates the GLSL code for accessing the uniform data stored in the texture.
137
- * @param textureName The name of the texture in the GLSL shader.
138
- * @param indexName The name of the index in the GLSL shader.
139
- * @param indexType The type of the index in the GLSL shader.
140
- * @returns An object containing the GLSL code for the vertex and fragment shaders.
141
- */
142
- getUniformsGLSL(textureName, indexName, indexType) {
143
- const vertex = this.getUniformsVertexGLSL(textureName, indexName, indexType);
144
- const fragment = this.getUniformsFragmentGLSL(textureName, indexName, indexType);
145
- return { vertex, fragment };
60
+ this.textArray.push(text);
146
61
  }
147
- getUniformsVertexGLSL(textureName, indexName, indexType) {
148
- if (this.fetchUniformsInFragmentShader) {
149
- return (
150
- /*glsl*/
151
- `
152
- flat varying ${indexType} ${this.uniformPrefix}${indexName};
153
- void main() {
154
- ${this.uniformPrefix}${indexName} = ${indexName};
155
- `
156
- );
157
- }
158
- const texelsFetch = this.texelsFetchGLSL(textureName, indexName);
159
- const getFromTexels = this.getFromTexelsGLSL();
160
- const { assignVarying, declareVarying } = this.getVarying();
161
- return (
162
- /*glsl*/
163
- `
164
- uniform highp sampler2D ${textureName};
165
- ${declareVarying}
166
- void main() {
167
- #ifdef USE_BATCHING
168
- ${indexType} ${indexName} = ${indexType}(getIndirectIndex(gl_DrawID));
169
- #endif
170
- ${texelsFetch}
171
- ${getFromTexels}
172
- ${assignVarying}`
173
- );
62
+ dispose() {
63
+ super.dispose();
64
+ this.dispatchEvent({ type: "dispose" });
174
65
  }
175
- getUniformsFragmentGLSL(textureName, indexName, indexType) {
176
- if (!this.fetchUniformsInFragmentShader) {
177
- const { declareVarying, getVarying } = this.getVarying();
178
- return (
179
- /*glsl*/
180
- `
181
- ${declareVarying}
182
- void main() {
183
- ${getVarying}`
66
+ // TODO: Check performance
67
+ _prepareForRender(material) {
68
+ var _a2;
69
+ const isOutline = material.isTextOutlineMaterial;
70
+ material.uniforms.uTroikaIsOutline.value = isOutline;
71
+ let texture = this._dataTextures[isOutline ? "outline" : "main"];
72
+ const dataLength = Math.pow(2, Math.ceil(Math.log2(this._members.size * floatsPerMember)));
73
+ if (!texture || dataLength !== texture.image.data.length) {
74
+ if (texture) texture.dispose();
75
+ const width = Math.min(dataLength / 4, 1024);
76
+ texture = this._dataTextures[isOutline ? "outline" : "main"] = new DataTexture(
77
+ new Float32Array(dataLength),
78
+ width,
79
+ dataLength / 4 / width,
80
+ RGBAFormat,
81
+ FloatType
184
82
  );
185
83
  }
186
- const texelsFetch = this.texelsFetchGLSL(textureName, `${this.uniformPrefix}${indexName}`);
187
- const getFromTexels = this.getFromTexelsGLSL();
188
- return (
189
- /*glsl*/
190
- `
191
- uniform highp sampler2D ${textureName};
192
- flat varying ${indexType} ${this.uniformPrefix}${indexName};
193
- void main() {
194
- ${texelsFetch}
195
- ${getFromTexels}`
196
- );
197
- }
198
- texelsFetchGLSL(textureName, indexName) {
199
- const pixelsPerInstance = this.pixelsPerInstance;
200
- let texelsFetch = (
201
- /*glsl*/
202
- `
203
- int size = textureSize(${textureName}, 0).x;
204
- int j = int(${indexName}) * ${pixelsPerInstance};
205
- int x = j % size;
206
- int y = j / size;
207
- `
208
- );
209
- for (let i = 0; i < pixelsPerInstance; i++) {
210
- texelsFetch += /*glsl*/
211
- `vec4 ${this.uniformPrefix}texel${i} = texelFetch(${textureName}, ivec2(x + ${i}, y), 0);
212
- `;
213
- }
214
- return texelsFetch;
215
- }
216
- getFromTexelsGLSL() {
217
- const uniforms = this.uniformMap;
218
- let getFromTexels = "";
219
- for (const [name, { type, offset, size }] of uniforms) {
220
- const tId = Math.floor(offset / this.channels);
221
- if (type === "mat3") {
222
- getFromTexels += /*glsl*/
223
- `mat3 ${name} = mat3(${this.uniformPrefix}texel${tId}.rgb, vec3(${this.uniformPrefix}texel${tId}.a, ${this.uniformPrefix}texel${tId + 1}.rg), vec3(${this.uniformPrefix}texel${tId + 1}.ba, ${this.uniformPrefix}texel${tId + 2}.r));
224
- `;
225
- } else if (type === "mat4") {
226
- getFromTexels += /*glsl*/
227
- `mat4 ${name} = mat4(${this.uniformPrefix}texel${tId}, ${this.uniformPrefix}texel${tId + 1}, ${this.uniformPrefix}texel${tId + 2}, ${this.uniformPrefix}texel${tId + 3});
228
- `;
84
+ const texData = texture.image.data;
85
+ this.textureNeedsUpdate = false;
86
+ for (const text of this.textArray) {
87
+ const index = ((_a2 = this._members.get(text)) == null ? void 0 : _a2.index) ?? -1;
88
+ const textRenderInfo = text.textRenderInfo;
89
+ if (index < 0 || !textRenderInfo) continue;
90
+ const startIndex = index * floatsPerMember;
91
+ if (!text.visible) {
92
+ for (let i = 0; i < 16; i++) {
93
+ this.setTexData(startIndex + i, 0, texData);
94
+ }
95
+ continue;
96
+ }
97
+ const matrix = text.matrix.elements;
98
+ for (let i = 0; i < 16; i++) {
99
+ this.setTexData(startIndex + i, matrix[i], texData);
100
+ }
101
+ text._prepareForRender(material);
102
+ const {
103
+ uTroikaTotalBounds,
104
+ uTroikaClipRect,
105
+ uTroikaPositionOffset,
106
+ uTroikaEdgeOffset,
107
+ uTroikaBlurRadius,
108
+ uTroikaStrokeWidth,
109
+ uTroikaStrokeColor,
110
+ uTroikaStrokeOpacity,
111
+ uTroikaFillOpacity,
112
+ uTroikaCurveRadius
113
+ } = material.uniforms;
114
+ for (let i = 0; i < 4; i++) {
115
+ this.setTexData(startIndex + 16 + i, uTroikaTotalBounds.value.getComponent(i), texData);
116
+ }
117
+ for (let i = 0; i < 4; i++) {
118
+ this.setTexData(startIndex + 20 + i, uTroikaClipRect.value.getComponent(i), texData);
119
+ }
120
+ let color = isOutline ? text.outlineColor || 0 : text.color;
121
+ color ?? (color = this.color);
122
+ color ?? (color = this.material.color);
123
+ color ?? (color = 16777215);
124
+ this.setTexData(startIndex + 24, tempColor.set(color).getHex(), texData);
125
+ this.setTexData(startIndex + 25, uTroikaFillOpacity.value, texData);
126
+ this.setTexData(startIndex + 26, uTroikaCurveRadius.value, texData);
127
+ if (isOutline) {
128
+ this.setTexData(startIndex + 28, uTroikaPositionOffset.value.x, texData);
129
+ this.setTexData(startIndex + 29, uTroikaPositionOffset.value.y, texData);
130
+ this.setTexData(startIndex + 30, uTroikaEdgeOffset.value, texData);
131
+ this.setTexData(startIndex + 31, uTroikaBlurRadius.value, texData);
229
132
  } else {
230
- const components = this.getUniformComponents(offset, size);
231
- getFromTexels += /*glsl*/
232
- `${type} ${name} = ${this.uniformPrefix}texel${tId}.${components};
233
- `;
133
+ this.setTexData(startIndex + 28, uTroikaStrokeWidth.value, texData);
134
+ this.setTexData(startIndex + 29, tempColor.set(uTroikaStrokeColor.value).getHex(), texData);
135
+ this.setTexData(startIndex + 30, uTroikaStrokeOpacity.value, texData);
234
136
  }
235
137
  }
236
- return getFromTexels;
237
- }
238
- getVarying() {
239
- const uniforms = this.uniformMap;
240
- let declareVarying = "";
241
- let assignVarying = "";
242
- let getVarying = "";
243
- for (const [name, { type }] of uniforms) {
244
- declareVarying += /*glsl*/
245
- `flat varying ${type} ${this.uniformPrefix}${name};
246
- `;
247
- assignVarying += /*glsl*/
248
- `${this.uniformPrefix}${name} = ${name};
249
- `;
250
- getVarying += /*glsl*/
251
- `${type} ${name} = ${this.uniformPrefix}${name};
252
- `;
253
- }
254
- return { declareVarying, assignVarying, getVarying };
138
+ texture.needsUpdate = this.textureNeedsUpdate;
139
+ material.setMatrixTexture(texture);
255
140
  }
256
- getUniformComponents(offset, size) {
257
- const startIndex = offset % this.channels;
258
- let components = "";
259
- for (let i = 0; i < size; i++) {
260
- components += componentsArray[startIndex + i];
141
+ setTexData(index, value, texData) {
142
+ if (value !== texData[index]) {
143
+ texData[index] = value;
144
+ this.textureNeedsUpdate = true;
261
145
  }
262
- return components;
263
146
  }
264
147
  }
265
- const componentsArray = ["r", "g", "b", "a"];
266
- const batchIdName = "batchId";
267
- class BatchedMesh extends BatchedMesh$1 {
268
- /**
269
- * @param instanceCount the max number of individual geometries planned to be added.
270
- * @param vertexCount the max number of vertices to be used by all geometries.
271
- * @param indexCount the max number of indices to be used by all geometries.
272
- * @param material an instance of {@link Material}. Default is a new {@link MeshBasicMaterial}.
273
- */
274
- constructor(instanceCount, vertexCount, indexCount, material) {
275
- super(instanceCount, vertexCount, indexCount, material);
276
- __publicField(this, "uniformsTexture");
277
- __publicField(this, "isMaterialPatched", false);
278
- __publicField(this, "uniformSchema", {});
279
- __publicField(this, "boundsNeedsUpdate", false);
280
- // Multi_draw with index causes excessive memory consumption on renderer process
281
- // TODO: Create issue in three.js repo
282
- __publicField(this, "useMultiDraw", false);
283
- __publicField(this, "useIndex", false);
284
- __publicField(this, "batchCount", 0);
285
- __publicField(this, "indexBuffer");
286
- __publicField(this, "geometryById", /* @__PURE__ */ new Map());
287
- __publicField(this, "mapGeometryToInstanceId", /* @__PURE__ */ new Map());
288
- material.forceSinglePass = true;
289
- addDim(this);
290
- this.addEventListener("added", () => {
291
- if (!this.useIndex && this.geometry.index !== null) {
292
- this.geometry = this.geometry.toNonIndexed();
293
- this.resizeToFitGeometry(this.geometry);
148
+ class Text extends Text$1 {
149
+ _prepareForRender(material) {
150
+ const isOutline = material.isTextOutlineMaterial;
151
+ const uniforms = material.uniforms;
152
+ const textInfo = this.textRenderInfo;
153
+ if (textInfo) {
154
+ const { sdfTexture, blockBounds } = textInfo;
155
+ const { width, height } = sdfTexture.image;
156
+ uniforms.uTroikaSDFTexture.value = sdfTexture;
157
+ uniforms.uTroikaSDFTextureSize.value.set(width, height);
158
+ uniforms.uTroikaSDFGlyphSize.value = textInfo.sdfGlyphSize;
159
+ uniforms.uTroikaSDFExponent.value = textInfo.sdfExponent;
160
+ uniforms.uTroikaTotalBounds.value.fromArray(blockBounds);
161
+ uniforms.uTroikaUseGlyphColors.value = !isOutline && !!textInfo.glyphColors;
162
+ let distanceOffset = 0;
163
+ let blurRadius = 0;
164
+ let strokeWidth = 0;
165
+ let fillOpacity;
166
+ let strokeOpacity = 1;
167
+ let strokeColor;
168
+ let offsetX = 0;
169
+ let offsetY = 0;
170
+ if (isOutline) {
171
+ const { outlineWidth, outlineOffsetX, outlineOffsetY, outlineBlur, outlineOpacity } = this;
172
+ distanceOffset = this._parsePercent(outlineWidth) || 0;
173
+ blurRadius = Math.max(0, this._parsePercent(outlineBlur) || 0);
174
+ fillOpacity = outlineOpacity;
175
+ offsetX = this._parsePercent(outlineOffsetX) || 0;
176
+ offsetY = this._parsePercent(outlineOffsetY) || 0;
177
+ } else {
178
+ strokeWidth = Math.max(0, this._parsePercent(this.strokeWidth) || 0);
179
+ if (strokeWidth) {
180
+ strokeColor = this.strokeColor;
181
+ uniforms.uTroikaStrokeColor.value.set(strokeColor ?? defaultStrokeColor);
182
+ strokeOpacity = this.strokeOpacity;
183
+ strokeOpacity ?? (strokeOpacity = 1);
184
+ }
185
+ fillOpacity = this.fillOpacity;
294
186
  }
295
- });
296
- }
297
- /**
298
- * Appends uniform definitions to the current schema, and creates a new {@link SquareDataTexture} if needed.
299
- * @param schema description of per-instance uniforms by shader stage (vertex/fragment)
300
- */
301
- addPerInstanceUniforms(schema) {
302
- this.uniformSchema = deepMerge(this.uniformSchema, schema);
303
- const parsedSchema = this.parseUniformSchema(this.uniformSchema);
304
- if (this.uniformsTexture) this.uniformsTexture.dispose();
305
- this.uniformsTexture = new SquareDataTexture(
306
- Float32Array,
307
- parsedSchema.channels,
308
- parsedSchema.texelsPerInstance,
309
- this.maxInstanceCount,
310
- parsedSchema.uniformMap,
311
- parsedSchema.fetchInFragmentShader
312
- );
313
- }
314
- addGeometry(geometry, reservedVertexRange, reservedIndexRange) {
315
- if (this.useMultiDraw) return super.addGeometry(geometry, reservedVertexRange, reservedIndexRange);
316
- this.addBatchIdBuffer(geometry, this.instanceCount);
317
- const geometryId = super.addGeometry(geometry, reservedVertexRange, reservedIndexRange);
318
- this.geometryById.set(geometryId, geometry);
319
- return geometryId;
320
- }
321
- addInstance(geometryId) {
322
- if (this.useMultiDraw) return super.addInstance(geometryId);
323
- if (this.mapGeometryToInstanceId.has(geometryId)) {
324
- const geometry = this.geometryById.get(geometryId);
325
- this.resizeToFitGeometry(geometry);
326
- geometryId = this.addGeometry(geometry);
187
+ uniforms.uTroikaEdgeOffset.value = distanceOffset;
188
+ uniforms.uTroikaPositionOffset.value.set(offsetX, offsetY);
189
+ uniforms.uTroikaBlurRadius.value = blurRadius;
190
+ uniforms.uTroikaStrokeWidth.value = strokeWidth;
191
+ uniforms.uTroikaStrokeOpacity.value = strokeOpacity;
192
+ uniforms.uTroikaFillOpacity.value = fillOpacity ?? 1;
193
+ uniforms.uTroikaCurveRadius.value = this.curveRadius || 0;
194
+ const clipRect = this.clipRect;
195
+ if (clipRect && Array.isArray(clipRect) && clipRect.length === 4) {
196
+ uniforms.uTroikaClipRect.value.fromArray(clipRect);
197
+ } else {
198
+ const pad = (this.fontSize || 0.1) * 100;
199
+ uniforms.uTroikaClipRect.value.set(
200
+ blockBounds[0] - pad,
201
+ blockBounds[1] - pad,
202
+ blockBounds[2] + pad,
203
+ blockBounds[3] + pad
204
+ );
205
+ }
206
+ this.geometry.applyClipRect(uniforms.uTroikaClipRect.value);
327
207
  }
328
- const instanceId = super.addInstance(geometryId);
329
- this.mapGeometryToInstanceId.set(geometryId, instanceId);
330
- return instanceId;
331
- }
332
- onBeforeRender(renderer, scene, camera, geometry, material, group) {
333
- var _a2;
334
- if (!this.isMaterialPatched) this.patchMaterial(material);
335
- (_a2 = this.uniformsTexture) == null ? void 0 : _a2.update();
336
- if (this.useMultiDraw) return super.onBeforeRender(renderer, scene, camera, geometry, material, group);
337
- if (!this.indexBuffer) {
338
- const vertexCount = geometry.getAttribute("position").count;
339
- this.indexBuffer = new BufferAttribute(new Uint32Array(vertexCount), 1).setUsage(StreamDrawUsage);
208
+ uniforms.uTroikaSDFDebug.value = !!this.debugSDF;
209
+ material.polygonOffset = !!this.depthOffset;
210
+ material.polygonOffsetFactor = material.polygonOffsetUnits = this.depthOffset || 0;
211
+ const color = isOutline ? this.outlineColor || 0 : this.color;
212
+ if (color == null) {
213
+ delete material.color;
214
+ } else {
215
+ const colorObj = material.hasOwnProperty("color") ? material.color : material.color = new Color();
216
+ if (color !== colorObj._input || typeof color === "object") {
217
+ colorObj.set(colorObj._input = color);
218
+ }
219
+ }
220
+ let orient = this.orientation || defaultOrient;
221
+ if (orient !== material._orientation) {
222
+ const rotMat = uniforms.uTroikaOrient.value;
223
+ orient = orient.replace(/[^-+xyz]/g, "");
224
+ const match = orient !== defaultOrient && /^([-+])([xyz])([-+])([xyz])$/.exec(orient);
225
+ if (match) {
226
+ const [, hSign, hAxis, vSign, vAxis] = match;
227
+ tempVec3a.set(0, 0, 0)[hAxis] = hSign === "-" ? 1 : -1;
228
+ tempVec3b.set(0, 0, 0)[vAxis] = vSign === "-" ? -1 : 1;
229
+ tempMat4.lookAt(origin, tempVec3a.cross(tempVec3b), tempVec3b);
230
+ rotMat.setFromMatrix4(tempMat4);
231
+ } else {
232
+ rotMat.identity();
233
+ }
234
+ material._orientation = orient;
340
235
  }
341
- super.onBeforeRender(renderer, scene, camera, geometry, material, group);
342
- this.batchCount = this.updateIndexBuffer(geometry);
343
- this._multiDrawCount = 0;
344
236
  }
345
- onAfterRender(renderer, scene, camera, geometry) {
346
- var _a2;
347
- if (this.useMultiDraw) return;
348
- const batchCount = this.batchCount;
349
- const gl = renderer.getContext();
350
- if (geometry.index == null) return console.warn("No index buffer", (_a2 = this.parent) == null ? void 0 : _a2.name);
351
- const type = this.getIndexType(gl, geometry.index);
352
- gl.drawElements(gl.TRIANGLES, batchCount, type, 0);
353
- renderer.info.update(batchCount, gl.TRIANGLES, 1);
354
- geometry.setIndex(null);
237
+ }
238
+ function setDimming(root, dim) {
239
+ root.userData["uDim"] = dim === void 0 ? void 0 : +dim;
240
+ }
241
+ function toggleInstanceDim(object, instanceId, dim) {
242
+ const value = dim === void 0 ? 0 : (+dim - 0.5) * 2;
243
+ if (object instanceof BatchedMesh) {
244
+ object.setUniformAt(instanceId, "skipDimInstance", value);
245
+ return;
355
246
  }
356
- /**
357
- * Retrieves the value of a uniform at the specified instance ID.
247
+ const skipDimTexture = object.userData["skipDimTexture"];
248
+ if (skipDimTexture) {
249
+ const skipDimData = skipDimTexture.image.data;
250
+ skipDimData[instanceId] = value;
251
+ skipDimTexture.needsUpdate = true;
252
+ }
253
+ }
254
+ function addDimToMaterial(material) {
255
+ if (material.userData.hasDimShader) return;
256
+ const onBeforeCompile = material.onBeforeCompile.bind(material);
257
+ const onBeforeRender = material.onBeforeRender.bind(material);
258
+ material.onBeforeCompile = (shader, renderer) => {
259
+ onBeforeCompile(shader, renderer);
260
+ shader.uniforms["uDim"] = { value: material.userData.uDim ?? 0 };
261
+ shader.uniforms["skipDimTexture"] = { value: material.userData.skipDimTexture ?? null };
262
+ shader.vertexShader = shader.vertexShader.replace("void main() {", `${dimColorVertexDefs}
263
+ void main() {`).replace(
264
+ "#include <fog_vertex>",
265
+ /*glsl*/
266
+ `
267
+ #include <fog_vertex>
268
+ setDimAmount();
269
+ `
270
+ ).concat(dimColorVertexImpl);
271
+ shader.fragmentShader = /*glsl*/
272
+ `
273
+ ${dimColorFrag}
274
+ ${shader.fragmentShader}
275
+ `.replace(
276
+ "#include <colorspace_fragment>",
277
+ /*glsl*/
278
+ `
279
+ gl_FragColor = dimColor(gl_FragColor);
280
+ #include <colorspace_fragment>
281
+ `
282
+ );
283
+ material.userData.shader = shader;
284
+ };
285
+ material.onBeforeRender = (renderer, scene, camera, geometry, object, group) => {
286
+ onBeforeRender(renderer, scene, camera, geometry, object, group);
287
+ const skipDimTexture = object.userData["skipDimTexture"];
288
+ let uDim = object.userData["uDim"];
289
+ if (uDim === void 0) {
290
+ for (const ancestor of traverseAncestorsGenerator(object)) {
291
+ if (ancestor.userData["uDim"] !== void 0) {
292
+ uDim = ancestor.userData["uDim"];
293
+ break;
294
+ }
295
+ }
296
+ }
297
+ const shader = material.userData.shader;
298
+ if (!shader) {
299
+ material.userData.uDim = uDim;
300
+ material.userData.skipDimTexture = object.userData["skipDimTexture"];
301
+ return;
302
+ }
303
+ shader.uniforms["uDim"].value = uDim ?? 0;
304
+ shader.uniforms["skipDimTexture"].value = skipDimTexture ?? null;
305
+ };
306
+ material.userData.hasDimShader = true;
307
+ }
308
+ function addDim(mesh) {
309
+ if (mesh instanceof BatchedMesh) mesh.addPerInstanceUniforms({ vertex: { skipDimInstance: "float" } });
310
+ if (mesh instanceof BatchedText) addSkipDimTexture(mesh);
311
+ }
312
+ function addSkipDimTexture(text) {
313
+ const count = text.size;
314
+ const size = Math.ceil(Math.sqrt(count));
315
+ const array = new Float32Array(size * size);
316
+ array.fill(0);
317
+ const texture = new DataTexture(array, size, size, RedFormat, FloatType);
318
+ texture.needsUpdate = true;
319
+ text.userData["skipDimTexture"] = texture;
320
+ text.addEventListener("dispose", () => texture.dispose());
321
+ return texture;
322
+ }
323
+ const dimColorVertexDefs = (
324
+ /*glsl*/
325
+ `
326
+ uniform float uDim;
327
+ out float dimAmount;
328
+ #ifdef TROIKA_DERIVED_MATERIAL_1
329
+ uniform sampler2D skipDimTexture;
330
+ #endif
331
+ void setDimAmount();
332
+ `
333
+ );
334
+ const dimColorVertexImpl = (
335
+ /*glsl*/
336
+ `
337
+ void setDimAmount() {
338
+ float instanceDim = 0.;
339
+ #ifdef USE_BATCH_UNIFORMS
340
+ instanceDim = batch_skipDimInstance;
341
+ #endif
342
+ #ifdef TROIKA_DERIVED_MATERIAL_1
343
+ float indirectIndex = aTroikaTextBatchMemberIndex;
344
+ int size = textureSize(skipDimTexture, 0).x;
345
+ int i = int(indirectIndex);
346
+ int x = i % size;
347
+ int y = i / size;
348
+ instanceDim = texelFetch(skipDimTexture, ivec2(x, y), 0).r;
349
+ #endif
350
+ dimAmount = instanceDim == 0. ? uDim : instanceDim / 2. + 0.5;
351
+ }
352
+ `
353
+ );
354
+ const dimColorFrag = (
355
+ /*glsl*/
356
+ `
357
+ in float dimAmount;
358
+
359
+ const vec3 grayWeights = vec3(0.299, 0.587, 0.114);
360
+ const float darkenFactor = pow(2., 2.2); // Gamma corrected
361
+
362
+ vec4 dimColor(vec4 col) {
363
+ vec3 color = col.rgb / col.a;
364
+ vec3 gray = vec3(dot(grayWeights, color));
365
+ vec3 m = mix(color, gray / darkenFactor, dimAmount);
366
+ return vec4(m * col.a, col.a);
367
+ }`
368
+ );
369
+ function isObject(item) {
370
+ return !!item && typeof item === "object" && !Array.isArray(item);
371
+ }
372
+ function deepMerge(target, ...sources) {
373
+ if (!sources.length) return target;
374
+ const source = sources.shift();
375
+ if (source === void 0) {
376
+ return target;
377
+ }
378
+ if (isObject(target) && isObject(source)) {
379
+ for (const key in source) {
380
+ if (isObject(source[key])) {
381
+ if (!target[key]) {
382
+ Object.assign(target, { [key]: {} });
383
+ }
384
+ deepMerge(target[key], source[key]);
385
+ } else {
386
+ Object.assign(target, { [key]: source[key] });
387
+ }
388
+ }
389
+ }
390
+ return deepMerge(target, ...sources);
391
+ }
392
+ function getSquareTextureSize(capacity, pixelsPerInstance) {
393
+ return Math.max(pixelsPerInstance, Math.ceil(Math.sqrt(capacity / pixelsPerInstance)) * pixelsPerInstance);
394
+ }
395
+ function getSquareTextureInfo(arrayType, channels, pixelsPerInstance, capacity) {
396
+ if (channels === 3) {
397
+ console.warn('"channels" cannot be 3. Set to 4. More info: https://github.com/mrdoob/three.js/pull/23228');
398
+ channels = 4;
399
+ }
400
+ const size = getSquareTextureSize(capacity, pixelsPerInstance);
401
+ const array = new arrayType(size * size * channels);
402
+ const isFloat = arrayType.name.includes("Float");
403
+ const isUnsignedInt = arrayType.name.includes("Uint");
404
+ const type = isFloat ? FloatType : isUnsignedInt ? UnsignedIntType : IntType;
405
+ let format;
406
+ switch (channels) {
407
+ case 1:
408
+ format = isFloat ? RedFormat : RedIntegerFormat;
409
+ break;
410
+ case 2:
411
+ format = isFloat ? RGFormat : RGIntegerFormat;
412
+ break;
413
+ case 4:
414
+ format = isFloat ? RGBAFormat : RGBAIntegerFormat;
415
+ break;
416
+ }
417
+ return { array, size, type, format };
418
+ }
419
+ class SquareDataTexture extends DataTexture {
420
+ /**
421
+ * @param arrayType The constructor for the TypedArray.
422
+ * @param channels The number of channels in the texture.
423
+ * @param pixelsPerInstance The number of pixels required for each instance.
424
+ * @param capacity The total number of instances.
425
+ * @param uniformMap Optional map for handling uniform values.
426
+ * @param fetchInFragmentShader Optional flag that determines if uniform values should be fetched in the fragment shader instead of the vertex shader.
427
+ */
428
+ constructor(arrayType, channels, pixelsPerInstance, capacity, uniformMap, fetchInFragmentShader) {
429
+ if (channels === 3) channels = 4;
430
+ const { array, format, size, type } = getSquareTextureInfo(arrayType, channels, pixelsPerInstance, capacity);
431
+ super(array, size, size, format, type);
432
+ __publicField(this, "data");
433
+ __publicField(this, "channels");
434
+ __publicField(this, "pixelsPerInstance");
435
+ __publicField(this, "stride");
436
+ __publicField(this, "uniformMap");
437
+ __publicField(this, "fetchUniformsInFragmentShader");
438
+ __publicField(this, "uniformPrefix", "batch_");
439
+ this.data = array;
440
+ this.channels = channels;
441
+ this.pixelsPerInstance = pixelsPerInstance;
442
+ this.stride = pixelsPerInstance * channels;
443
+ this.uniformMap = uniformMap;
444
+ this.fetchUniformsInFragmentShader = fetchInFragmentShader;
445
+ this.needsUpdate = true;
446
+ }
447
+ /**
448
+ * Sets a uniform value at the specified instance ID in the texture.
449
+ * @param id The instance ID to set the uniform for.
450
+ * @param name The name of the uniform.
451
+ * @param value The value to set for the uniform.
452
+ */
453
+ setUniformAt(id, name, value) {
454
+ const schema = this.uniformMap.get(name);
455
+ if (!schema) {
456
+ console.warn(`SquareDataTexture.setUniformAt: uniform ${name} not found`);
457
+ return;
458
+ }
459
+ const { offset, size } = schema;
460
+ const stride = this.stride;
461
+ if (size === 1) {
462
+ this.data[id * stride + offset] = value;
463
+ } else {
464
+ value.toArray(this.data, id * stride + offset);
465
+ }
466
+ }
467
+ /**
468
+ * Retrieves a uniform value at the specified instance ID from the texture.
469
+ * @param id The instance ID to retrieve the uniform from.
470
+ * @param name The name of the uniform.
471
+ * @param target Optional target object to store the uniform value.
472
+ * @returns The uniform value for the specified instance.
473
+ */
474
+ getUniformAt(id, name, target) {
475
+ const schema = this.uniformMap.get(name);
476
+ if (!schema) {
477
+ console.warn(`SquareDataTexture.getUniformAt: uniform ${name} not found`);
478
+ return 0;
479
+ }
480
+ const { offset, size } = schema;
481
+ const stride = this.stride;
482
+ if (size === 1) {
483
+ return this.data[id * stride + offset];
484
+ }
485
+ return target.fromArray(this.data, id * stride + offset);
486
+ }
487
+ /** Mark the texture as needing an update. */
488
+ update() {
489
+ this.needsUpdate = true;
490
+ }
491
+ /**
492
+ * Generates the GLSL code for accessing the uniform data stored in the texture.
493
+ * @param textureName The name of the texture in the GLSL shader.
494
+ * @param indexName The name of the index in the GLSL shader.
495
+ * @param indexType The type of the index in the GLSL shader.
496
+ * @returns An object containing the GLSL code for the vertex and fragment shaders.
497
+ */
498
+ getUniformsGLSL(textureName, indexName, indexType) {
499
+ const vertex = this.getUniformsVertexGLSL(textureName, indexName, indexType);
500
+ const fragment = this.getUniformsFragmentGLSL(textureName, indexName, indexType);
501
+ return { vertex, fragment };
502
+ }
503
+ getUniformsVertexGLSL(textureName, indexName, indexType) {
504
+ if (this.fetchUniformsInFragmentShader) {
505
+ return (
506
+ /*glsl*/
507
+ `
508
+ varying ${indexType} ${this.uniformPrefix}${indexName};
509
+ void main() {
510
+ ${this.uniformPrefix}${indexName} = ${indexName};
511
+ `
512
+ );
513
+ }
514
+ const texelsFetch = this.texelsFetchGLSL(textureName, indexName);
515
+ const getFromTexels = this.getFromTexelsGLSL();
516
+ const { assignVarying, declareVarying } = this.getVarying();
517
+ return (
518
+ /*glsl*/
519
+ `
520
+ uniform highp sampler2D ${textureName};
521
+ ${declareVarying}
522
+ void main() {
523
+ #ifdef USE_BATCHING
524
+ ${indexType} ${indexName} = ${indexType}(getIndirectIndex(gl_DrawID));
525
+ #endif
526
+ ${texelsFetch}
527
+ ${getFromTexels}
528
+ ${assignVarying}`
529
+ );
530
+ }
531
+ getUniformsFragmentGLSL(textureName, indexName, indexType) {
532
+ if (!this.fetchUniformsInFragmentShader) {
533
+ const { declareVarying, getVarying } = this.getVarying();
534
+ return (
535
+ /*glsl*/
536
+ `
537
+ ${declareVarying}
538
+ void main() {
539
+ ${getVarying}`
540
+ );
541
+ }
542
+ const texelsFetch = this.texelsFetchGLSL(textureName, `${this.uniformPrefix}${indexName}`);
543
+ const getFromTexels = this.getFromTexelsGLSL();
544
+ return (
545
+ /*glsl*/
546
+ `
547
+ uniform highp sampler2D ${textureName};
548
+ varying ${indexType} ${this.uniformPrefix}${indexName};
549
+ void main() {
550
+ ${texelsFetch}
551
+ ${getFromTexels}`
552
+ );
553
+ }
554
+ texelsFetchGLSL(textureName, indexName) {
555
+ const pixelsPerInstance = this.pixelsPerInstance;
556
+ let texelsFetch = (
557
+ /*glsl*/
558
+ `
559
+ int size = textureSize(${textureName}, 0).x;
560
+ int j = int(${indexName}) * ${pixelsPerInstance};
561
+ int x = j % size;
562
+ int y = j / size;
563
+ `
564
+ );
565
+ for (let i = 0; i < pixelsPerInstance; i++) {
566
+ texelsFetch += /*glsl*/
567
+ `vec4 ${this.uniformPrefix}texel${i} = texelFetch(${textureName}, ivec2(x + ${i}, y), 0);
568
+ `;
569
+ }
570
+ return texelsFetch;
571
+ }
572
+ getFromTexelsGLSL() {
573
+ const uniforms = this.uniformMap;
574
+ let getFromTexels = "";
575
+ for (const [name, { type, offset, size }] of uniforms) {
576
+ const tId = Math.floor(offset / this.channels);
577
+ if (type === "mat3") {
578
+ getFromTexels += /*glsl*/
579
+ `mat3 ${name} = mat3(${this.uniformPrefix}texel${tId}.rgb, vec3(${this.uniformPrefix}texel${tId}.a, ${this.uniformPrefix}texel${tId + 1}.rg), vec3(${this.uniformPrefix}texel${tId + 1}.ba, ${this.uniformPrefix}texel${tId + 2}.r));
580
+ `;
581
+ } else if (type === "mat4") {
582
+ getFromTexels += /*glsl*/
583
+ `mat4 ${name} = mat4(${this.uniformPrefix}texel${tId}, ${this.uniformPrefix}texel${tId + 1}, ${this.uniformPrefix}texel${tId + 2}, ${this.uniformPrefix}texel${tId + 3});
584
+ `;
585
+ } else {
586
+ const components = this.getUniformComponents(offset, size);
587
+ getFromTexels += /*glsl*/
588
+ `${type} ${name} = ${this.uniformPrefix}texel${tId}.${components};
589
+ `;
590
+ }
591
+ }
592
+ return getFromTexels;
593
+ }
594
+ getVarying() {
595
+ const uniforms = this.uniformMap;
596
+ let declareVarying = "";
597
+ let assignVarying = "";
598
+ let getVarying = "";
599
+ for (const [name, { type }] of uniforms) {
600
+ declareVarying += /*glsl*/
601
+ `varying ${type} ${this.uniformPrefix}${name};
602
+ `;
603
+ assignVarying += /*glsl*/
604
+ `${this.uniformPrefix}${name} = ${name};
605
+ `;
606
+ getVarying += /*glsl*/
607
+ `${type} ${name} = ${this.uniformPrefix}${name};
608
+ `;
609
+ }
610
+ return { declareVarying, assignVarying, getVarying };
611
+ }
612
+ getUniformComponents(offset, size) {
613
+ const startIndex = offset % this.channels;
614
+ let components = "";
615
+ for (let i = 0; i < size; i++) {
616
+ components += componentsArray[startIndex + i];
617
+ }
618
+ return components;
619
+ }
620
+ }
621
+ const componentsArray = ["r", "g", "b", "a"];
622
+ const batchIdName = "batchId";
623
+ const _BatchedMesh = class _BatchedMesh extends BatchedMesh$1 {
624
+ /**
625
+ * @param instanceCount the max number of individual geometries planned to be added.
626
+ * @param vertexCount the max number of vertices to be used by all geometries.
627
+ * @param indexCount the max number of indices to be used by all geometries.
628
+ * @param material an instance of {@link Material}. Default is a new {@link MeshBasicMaterial}.
629
+ */
630
+ constructor(instanceCount, vertexCount, indexCount, material) {
631
+ super(instanceCount, vertexCount, indexCount, material);
632
+ __publicField(this, "uniformsTexture");
633
+ __publicField(this, "uniformSchema", {});
634
+ __publicField(this, "isMaterialPatched", false);
635
+ __publicField(this, "boundsNeedsUpdate", false);
636
+ __publicField(this, "batchCount", 0);
637
+ __publicField(this, "indexBuffer");
638
+ __publicField(this, "geometryById", /* @__PURE__ */ new Map());
639
+ __publicField(this, "mapGeometryToInstanceId", /* @__PURE__ */ new Map());
640
+ material.forceSinglePass = true;
641
+ addDim(this);
642
+ this.addEventListener("added", () => {
643
+ if (!_BatchedMesh.useMultiDraw && this.geometry.index !== null) {
644
+ this.geometry = this.geometry.toNonIndexed();
645
+ this.resizeToFitGeometry(this.geometry);
646
+ }
647
+ });
648
+ }
649
+ /**
650
+ * Appends uniform definitions to the current schema, and creates a new {@link SquareDataTexture} if needed.
651
+ * @param schema description of per-instance uniforms by shader stage (vertex/fragment)
652
+ */
653
+ addPerInstanceUniforms(schema) {
654
+ this.uniformSchema = deepMerge(this.uniformSchema, schema);
655
+ const parsedSchema = this.parseUniformSchema(this.uniformSchema);
656
+ if (this.uniformsTexture) this.uniformsTexture.dispose();
657
+ this.uniformsTexture = new SquareDataTexture(
658
+ Float32Array,
659
+ parsedSchema.channels,
660
+ parsedSchema.texelsPerInstance,
661
+ this.maxInstanceCount,
662
+ parsedSchema.uniformMap,
663
+ parsedSchema.fetchInFragmentShader
664
+ );
665
+ if (!this.isMaterialPatched) this.patchMaterial(this.material);
666
+ }
667
+ addGeometry(geometry, reservedVertexRange, reservedIndexRange) {
668
+ if (_BatchedMesh.useMultiDraw) return super.addGeometry(geometry, reservedVertexRange, reservedIndexRange);
669
+ this.addBatchIdBuffer(geometry, this.instanceCount);
670
+ const geometryId = super.addGeometry(geometry, reservedVertexRange, reservedIndexRange);
671
+ this.geometryById.set(geometryId, geometry);
672
+ return geometryId;
673
+ }
674
+ addInstance(geometryId) {
675
+ if (_BatchedMesh.useMultiDraw) return super.addInstance(geometryId);
676
+ if (this.mapGeometryToInstanceId.has(geometryId)) {
677
+ const geometry = this.geometryById.get(geometryId);
678
+ this.resizeToFitGeometry(geometry);
679
+ geometryId = this.addGeometry(geometry);
680
+ }
681
+ const instanceId = super.addInstance(geometryId);
682
+ this.mapGeometryToInstanceId.set(geometryId, instanceId);
683
+ return instanceId;
684
+ }
685
+ onBeforeRender(renderer, scene, camera, geometry, material, group) {
686
+ var _a2;
687
+ (_a2 = this.uniformsTexture) == null ? void 0 : _a2.update();
688
+ if (_BatchedMesh.useMultiDraw) return super.onBeforeRender(renderer, scene, camera, geometry, material, group);
689
+ if (!this.indexBuffer) {
690
+ const vertexCount = geometry.getAttribute("position").count;
691
+ this.indexBuffer = new BufferAttribute(new Uint32Array(vertexCount), 1).setUsage(StreamDrawUsage);
692
+ }
693
+ super.onBeforeRender(renderer, scene, camera, geometry, material, group);
694
+ this.batchCount = this.updateIndexBuffer(geometry);
695
+ this._multiDrawCount = 0;
696
+ }
697
+ onAfterRender(renderer, scene, camera, geometry) {
698
+ var _a2;
699
+ if (_BatchedMesh.useMultiDraw) return;
700
+ const batchCount = this.batchCount;
701
+ const gl = renderer.getContext();
702
+ if (geometry.index == null) return console.warn("No index buffer", (_a2 = this.parent) == null ? void 0 : _a2.name);
703
+ const type = this.getIndexType(gl, geometry.index);
704
+ gl.drawElements(gl.TRIANGLES, batchCount, type, 0);
705
+ renderer.info.update(batchCount, gl.TRIANGLES, 1);
706
+ geometry.setIndex(null);
707
+ }
708
+ /**
709
+ * Retrieves the value of a uniform at the specified instance ID.
358
710
  * @param id instance ID
359
711
  * @param name uniform name
360
712
  * @param target Optional target object to store the uniform value
@@ -388,530 +740,179 @@ class BatchedMesh extends BatchedMesh$1 {
388
740
  this.boundsNeedsUpdate = false;
389
741
  }
390
742
  }
391
- setMatrixAt(instanceId, matrix) {
392
- super.setMatrixAt(instanceId, matrix);
393
- this.boundsNeedsUpdate = true;
394
- return this;
395
- }
396
- dispose() {
397
- var _a2;
398
- this.geometry.setIndex(this.indexBuffer ?? null);
399
- super.dispose();
400
- this.uniformSchema = {};
401
- (_a2 = this.uniformsTexture) == null ? void 0 : _a2.dispose();
402
- this.uniformsTexture = void 0;
403
- this.indexBuffer = void 0;
404
- this.geometryById.forEach((geometry) => geometry.dispose());
405
- this.geometryById.clear();
406
- this.mapGeometryToInstanceId.clear();
407
- return this;
408
- }
409
- resizeToFitGeometry(geometry) {
410
- var _a2;
411
- const vertexCount = geometry.attributes["position"].count;
412
- const indexCount = ((_a2 = geometry.index) == null ? void 0 : _a2.count) ?? 0;
413
- this._maxVertexCount += vertexCount;
414
- this._maxIndexCount += indexCount;
415
- this.setGeometrySize(this._maxVertexCount, this._maxIndexCount);
416
- }
417
- updateIndexBuffer(geometry) {
418
- const { _multiDrawStarts, _multiDrawCounts, _multiDrawCount } = this;
419
- const indexBuffer = this.indexBuffer;
420
- const indexArray = indexBuffer.array;
421
- const batchIdBuffer = geometry.getAttribute(batchIdName);
422
- const batchIdArray = batchIdBuffer.array;
423
- const indirectArray = this._indirectTexture.image.data;
424
- let indexCount = 0;
425
- for (let i = 0; i < _multiDrawCount; i++) {
426
- const start = _multiDrawStarts[i];
427
- const count = _multiDrawCounts[i];
428
- const batchId = batchIdArray[start];
429
- indirectArray[batchId] = batchId;
430
- for (let j = start; j < start + count; j++) {
431
- indexArray[indexCount++] = j;
432
- }
433
- }
434
- indexBuffer.needsUpdate = true;
435
- geometry.setIndex(indexBuffer);
436
- return indexCount;
437
- }
438
- addBatchIdBuffer(geometry, geometryId) {
439
- const hasAttribute = geometry.hasAttribute(batchIdName);
440
- const vertexCount = geometry.getAttribute("position").count;
441
- const batchIdArray = hasAttribute ? geometry.getAttribute(batchIdName).array : new Int32Array(vertexCount);
442
- for (let i = 0; i < vertexCount; i++) {
443
- batchIdArray[i] = geometryId;
444
- }
445
- if (!hasAttribute) {
446
- const batchIdBuffer = new BufferAttribute(batchIdArray, 1).setUsage(StreamDrawUsage);
447
- geometry.setAttribute(batchIdName, batchIdBuffer);
448
- }
449
- }
450
- patchMaterial(material) {
451
- const onBeforeCompile = material.onBeforeCompile.bind(material);
452
- const customProgramCacheKey = material.customProgramCacheKey.bind(material);
453
- material.onBeforeCompile = (shader, renderer) => {
454
- var _a2;
455
- onBeforeCompile(shader, renderer);
456
- shader.defines ?? (shader.defines = {});
457
- shader.defines["USE_BATCH_UNIFORMS"] = "";
458
- shader.uniforms["uniformsTexture"] = { value: this.uniformsTexture };
459
- const { vertex, fragment } = ((_a2 = this.uniformsTexture) == null ? void 0 : _a2.getUniformsGLSL("uniformsTexture", "instanceId", "int")) ?? {
460
- vertex: "",
461
- fragment: ""
462
- };
463
- const patch = (
464
- /*glsl*/
465
- `
466
- #ifdef gl_DrawID
467
- #define _gl_DrawID ${batchIdName}
468
- #else
469
- #define gl_DrawID ${batchIdName}
470
- #endif
471
- in int ${batchIdName};
472
- `
473
- );
474
- const main = "void main() {";
475
- if (!this.useMultiDraw) shader.vertexShader = shader.vertexShader.replace(main, `${patch}${main}`);
476
- if (vertex) shader.vertexShader = shader.vertexShader.replace(main, vertex);
477
- if (fragment) shader.fragmentShader = shader.fragmentShader.replace(main, fragment);
478
- };
479
- material.customProgramCacheKey = () => {
480
- return `batch_${this.id}_${!!this.uniformsTexture}_${customProgramCacheKey()}`;
481
- };
482
- this.isMaterialPatched = true;
483
- }
484
- // Taken from https://github.com/agargaro/instanced-mesh/blob/master/src/core/feature/Uniforms.ts
485
- parseUniformSchema(schema) {
486
- let totalSize = 0;
487
- const uniformMap = /* @__PURE__ */ new Map();
488
- const uniforms = [];
489
- const vertexSchema = schema.vertex ?? {};
490
- const fragmentSchema = schema.fragment ?? {};
491
- let fetchInFragmentShader = true;
492
- for (const name in vertexSchema) {
493
- const type = vertexSchema[name];
494
- const size = this.getUniformSize(type);
495
- totalSize += size;
496
- uniforms.push({ name, type, size });
497
- fetchInFragmentShader = false;
498
- }
499
- for (const name in fragmentSchema) {
500
- if (!vertexSchema[name]) {
501
- const type = fragmentSchema[name];
502
- const size = this.getUniformSize(type);
503
- totalSize += size;
504
- uniforms.push({ name, type, size });
505
- }
506
- }
507
- uniforms.sort((a, b) => b.size - a.size);
508
- const tempOffset = [];
509
- for (const { name, size, type } of uniforms) {
510
- const offset = this.getUniformOffset(size, tempOffset);
511
- uniformMap.set(name, { offset, size, type });
512
- }
513
- const pixelsPerInstance = Math.ceil(totalSize / 4);
514
- const channels = Math.min(totalSize, 4);
515
- return { channels, texelsPerInstance: pixelsPerInstance, uniformMap, fetchInFragmentShader };
516
- }
517
- getUniformOffset(size, tempOffset) {
518
- if (size < 4) {
519
- for (let i = 0; i < tempOffset.length; i++) {
520
- if (tempOffset[i] + size <= 4) {
521
- const offset2 = i * 4 + tempOffset[i];
522
- tempOffset[i] += size;
523
- return offset2;
524
- }
525
- }
526
- }
527
- const offset = tempOffset.length * 4;
528
- for (; size > 0; size -= 4) {
529
- tempOffset.push(size);
530
- }
531
- return offset;
532
- }
533
- getUniformSize(type) {
534
- switch (type) {
535
- case "float":
536
- return 1;
537
- case "vec2":
538
- return 2;
539
- case "vec3":
540
- return 3;
541
- case "vec4":
542
- return 4;
543
- case "mat3":
544
- return 9;
545
- case "mat4":
546
- return 16;
547
- }
548
- }
549
- getIndexType(gl, index) {
550
- const array = index.array;
551
- if (array instanceof Uint16Array) return gl.UNSIGNED_SHORT;
552
- if (array instanceof Uint32Array) return gl.UNSIGNED_INT;
553
- return gl.UNSIGNED_BYTE;
554
- }
555
- }
556
- const floatsPerMember = 32;
557
- const tempColor = new Color();
558
- const defaultStrokeColor = 8421504;
559
- const tempMat4 = new Matrix4();
560
- const tempVec3a = new Vector3();
561
- const tempVec3b = new Vector3();
562
- const origin = new Vector3();
563
- const defaultOrient = "+x+y";
564
- class BatchedText extends BatchedText$1 {
565
- // eslint-disable-next-line jsdoc/require-jsdoc
566
- constructor() {
567
- super();
568
- __publicField(this, "mapInstanceIdToText", /* @__PURE__ */ new Map());
569
- __publicField(this, "textArray", []);
570
- __publicField(this, "textureNeedsUpdate", false);
571
- }
572
- /** Number of texts in the batch */
573
- get size() {
574
- return this._members.size;
575
- }
576
- /** Base material before patching */
577
- get baseMaterial() {
578
- return this._baseMaterial;
579
- }
580
- /**
581
- * Get the {@link Text} object by instance id
582
- * @param instanceId Instance id
583
- * @returns Text object
584
- */
585
- getText(instanceId) {
586
- return this.mapInstanceIdToText.get(instanceId);
587
- }
588
- /**
589
- * Set the visibility of the {@link Text} object by instance id.
590
- * This is for interface compatibility with {@link BatchedMesh}.
591
- * @param instanceId Instance id
592
- * @param visible Visibility flag
593
- */
594
- setVisibleAt(instanceId, visible) {
595
- const text = this.getText(instanceId);
596
- text.visible = visible;
597
- }
598
- addText(text, instanceId) {
599
- super.addText(text);
600
- if (instanceId !== void 0) {
601
- this.mapInstanceIdToText.set(instanceId, text);
602
- }
603
- this.textArray.push(text);
743
+ setMatrixAt(instanceId, matrix) {
744
+ super.setMatrixAt(instanceId, matrix);
745
+ this.boundsNeedsUpdate = true;
746
+ return this;
604
747
  }
605
748
  dispose() {
749
+ var _a2;
750
+ if (this.indexBuffer) this.geometry.setIndex(this.indexBuffer);
606
751
  super.dispose();
607
- this.dispatchEvent({ type: "dispose" });
752
+ this.uniformSchema = {};
753
+ (_a2 = this.uniformsTexture) == null ? void 0 : _a2.dispose();
754
+ this.uniformsTexture = void 0;
755
+ this.indexBuffer = void 0;
756
+ this.geometryById.forEach((geometry) => geometry.dispose());
757
+ this.geometryById.clear();
758
+ this.mapGeometryToInstanceId.clear();
759
+ return this;
608
760
  }
609
- _prepareForRender(material) {
761
+ resizeToFitGeometry(geometry) {
610
762
  var _a2;
611
- const isOutline = material.isTextOutlineMaterial;
612
- material.uniforms.uTroikaIsOutline.value = isOutline;
613
- let texture = this._dataTextures[isOutline ? "outline" : "main"];
614
- const dataLength = Math.pow(2, Math.ceil(Math.log2(this._members.size * floatsPerMember)));
615
- if (!texture || dataLength !== texture.image.data.length) {
616
- if (texture) texture.dispose();
617
- const width = Math.min(dataLength / 4, 1024);
618
- texture = this._dataTextures[isOutline ? "outline" : "main"] = new DataTexture(
619
- new Float32Array(dataLength),
620
- width,
621
- dataLength / 4 / width,
622
- RGBAFormat,
623
- FloatType
624
- );
625
- }
626
- const texData = texture.image.data;
627
- this.textureNeedsUpdate = false;
628
- for (const text of this.textArray) {
629
- const index = ((_a2 = this._members.get(text)) == null ? void 0 : _a2.index) ?? -1;
630
- const textRenderInfo = text.textRenderInfo;
631
- if (index < 0 || !textRenderInfo) continue;
632
- const startIndex = index * floatsPerMember;
633
- if (!text.visible) {
634
- for (let i = 0; i < 16; i++) {
635
- this.setTexData(startIndex + i, 0, texData);
636
- }
637
- continue;
638
- }
639
- const matrix = text.matrix.elements;
640
- for (let i = 0; i < 16; i++) {
641
- this.setTexData(startIndex + i, matrix[i], texData);
642
- }
643
- text._prepareForRender(material);
644
- const {
645
- uTroikaTotalBounds,
646
- uTroikaClipRect,
647
- uTroikaPositionOffset,
648
- uTroikaEdgeOffset,
649
- uTroikaBlurRadius,
650
- uTroikaStrokeWidth,
651
- uTroikaStrokeColor,
652
- uTroikaStrokeOpacity,
653
- uTroikaFillOpacity,
654
- uTroikaCurveRadius
655
- } = material.uniforms;
656
- for (let i = 0; i < 4; i++) {
657
- this.setTexData(startIndex + 16 + i, uTroikaTotalBounds.value.getComponent(i), texData);
658
- }
659
- for (let i = 0; i < 4; i++) {
660
- this.setTexData(startIndex + 20 + i, uTroikaClipRect.value.getComponent(i), texData);
661
- }
662
- let color = isOutline ? text.outlineColor || 0 : text.color;
663
- color ?? (color = this.color);
664
- color ?? (color = this.material.color);
665
- color ?? (color = 16777215);
666
- this.setTexData(startIndex + 24, tempColor.set(color).getHex(), texData);
667
- this.setTexData(startIndex + 25, uTroikaFillOpacity.value, texData);
668
- this.setTexData(startIndex + 26, uTroikaCurveRadius.value, texData);
669
- if (isOutline) {
670
- this.setTexData(startIndex + 28, uTroikaPositionOffset.value.x, texData);
671
- this.setTexData(startIndex + 29, uTroikaPositionOffset.value.y, texData);
672
- this.setTexData(startIndex + 30, uTroikaEdgeOffset.value, texData);
673
- this.setTexData(startIndex + 31, uTroikaBlurRadius.value, texData);
674
- } else {
675
- this.setTexData(startIndex + 28, uTroikaStrokeWidth.value, texData);
676
- this.setTexData(startIndex + 29, tempColor.set(uTroikaStrokeColor.value).getHex(), texData);
677
- this.setTexData(startIndex + 30, uTroikaStrokeOpacity.value, texData);
763
+ const vertexCount = geometry.attributes["position"].count;
764
+ const indexCount = ((_a2 = geometry.index) == null ? void 0 : _a2.count) ?? 0;
765
+ this._maxVertexCount += vertexCount;
766
+ this._maxIndexCount += indexCount;
767
+ this.setGeometrySize(this._maxVertexCount, this._maxIndexCount);
768
+ }
769
+ updateIndexBuffer(geometry) {
770
+ const { _multiDrawStarts, _multiDrawCounts, _multiDrawCount } = this;
771
+ const indexBuffer = this.indexBuffer;
772
+ const indexArray = indexBuffer.array;
773
+ const batchIdBuffer = geometry.getAttribute(batchIdName);
774
+ const batchIdArray = batchIdBuffer.array;
775
+ const indirectArray = this._indirectTexture.image.data;
776
+ let indexCount = 0;
777
+ for (let i = 0; i < _multiDrawCount; i++) {
778
+ const start = _multiDrawStarts[i];
779
+ const count = _multiDrawCounts[i];
780
+ const batchId = batchIdArray[start];
781
+ indirectArray[batchId] = batchId;
782
+ for (let j = start; j < start + count; j++) {
783
+ indexArray[indexCount++] = j;
678
784
  }
679
785
  }
680
- texture.needsUpdate = this.textureNeedsUpdate;
681
- material.setMatrixTexture(texture);
786
+ indexBuffer.needsUpdate = true;
787
+ geometry.setIndex(indexBuffer);
788
+ return indexCount;
682
789
  }
683
- setTexData(index, value, texData) {
684
- if (value !== texData[index]) {
685
- texData[index] = value;
686
- this.textureNeedsUpdate = true;
790
+ addBatchIdBuffer(geometry, geometryId) {
791
+ const hasAttribute = geometry.hasAttribute(batchIdName);
792
+ const vertexCount = geometry.getAttribute("position").count;
793
+ const batchIdArray = hasAttribute ? geometry.getAttribute(batchIdName).array : new Int32Array(vertexCount);
794
+ for (let i = 0; i < vertexCount; i++) {
795
+ batchIdArray[i] = geometryId;
687
796
  }
688
- }
689
- }
690
- class Text extends Text$1 {
691
- _prepareForRender(material) {
692
- const isOutline = material.isTextOutlineMaterial;
693
- const uniforms = material.uniforms;
694
- const textInfo = this.textRenderInfo;
695
- if (textInfo) {
696
- const { sdfTexture, blockBounds } = textInfo;
697
- const { width, height } = sdfTexture.image;
698
- uniforms.uTroikaSDFTexture.value = sdfTexture;
699
- uniforms.uTroikaSDFTextureSize.value.set(width, height);
700
- uniforms.uTroikaSDFGlyphSize.value = textInfo.sdfGlyphSize;
701
- uniforms.uTroikaSDFExponent.value = textInfo.sdfExponent;
702
- uniforms.uTroikaTotalBounds.value.fromArray(blockBounds);
703
- uniforms.uTroikaUseGlyphColors.value = !isOutline && !!textInfo.glyphColors;
704
- let distanceOffset = 0;
705
- let blurRadius = 0;
706
- let strokeWidth = 0;
707
- let fillOpacity;
708
- let strokeOpacity = 1;
709
- let strokeColor;
710
- let offsetX = 0;
711
- let offsetY = 0;
712
- if (isOutline) {
713
- const { outlineWidth, outlineOffsetX, outlineOffsetY, outlineBlur, outlineOpacity } = this;
714
- distanceOffset = this._parsePercent(outlineWidth) || 0;
715
- blurRadius = Math.max(0, this._parsePercent(outlineBlur) || 0);
716
- fillOpacity = outlineOpacity;
717
- offsetX = this._parsePercent(outlineOffsetX) || 0;
718
- offsetY = this._parsePercent(outlineOffsetY) || 0;
719
- } else {
720
- strokeWidth = Math.max(0, this._parsePercent(this.strokeWidth) || 0);
721
- if (strokeWidth) {
722
- strokeColor = this.strokeColor;
723
- uniforms.uTroikaStrokeColor.value.set(strokeColor ?? defaultStrokeColor);
724
- strokeOpacity = this.strokeOpacity;
725
- strokeOpacity ?? (strokeOpacity = 1);
726
- }
727
- fillOpacity = this.fillOpacity;
728
- }
729
- uniforms.uTroikaEdgeOffset.value = distanceOffset;
730
- uniforms.uTroikaPositionOffset.value.set(offsetX, offsetY);
731
- uniforms.uTroikaBlurRadius.value = blurRadius;
732
- uniforms.uTroikaStrokeWidth.value = strokeWidth;
733
- uniforms.uTroikaStrokeOpacity.value = strokeOpacity;
734
- uniforms.uTroikaFillOpacity.value = fillOpacity ?? 1;
735
- uniforms.uTroikaCurveRadius.value = this.curveRadius || 0;
736
- const clipRect = this.clipRect;
737
- if (clipRect && Array.isArray(clipRect) && clipRect.length === 4) {
738
- uniforms.uTroikaClipRect.value.fromArray(clipRect);
739
- } else {
740
- const pad = (this.fontSize || 0.1) * 100;
741
- uniforms.uTroikaClipRect.value.set(
742
- blockBounds[0] - pad,
743
- blockBounds[1] - pad,
744
- blockBounds[2] + pad,
745
- blockBounds[3] + pad
746
- );
747
- }
748
- this.geometry.applyClipRect(uniforms.uTroikaClipRect.value);
797
+ if (!hasAttribute) {
798
+ const batchIdBuffer = new BufferAttribute(batchIdArray, 1).setUsage(StreamDrawUsage);
799
+ geometry.setAttribute(batchIdName, batchIdBuffer);
749
800
  }
750
- uniforms.uTroikaSDFDebug.value = !!this.debugSDF;
751
- material.polygonOffset = !!this.depthOffset;
752
- material.polygonOffsetFactor = material.polygonOffsetUnits = this.depthOffset || 0;
753
- const color = isOutline ? this.outlineColor || 0 : this.color;
754
- if (color == null) {
755
- delete material.color;
756
- } else {
757
- const colorObj = material.hasOwnProperty("color") ? material.color : material.color = new Color();
758
- if (color !== colorObj._input || typeof color === "object") {
759
- colorObj.set(colorObj._input = color);
760
- }
801
+ }
802
+ patchMaterial(material) {
803
+ const onBeforeCompile = material.onBeforeCompile.bind(material);
804
+ const customProgramCacheKey = material.customProgramCacheKey.bind(material);
805
+ material.onBeforeCompile = (shader, renderer) => {
806
+ var _a2;
807
+ onBeforeCompile(shader, renderer);
808
+ shader.defines ?? (shader.defines = {});
809
+ shader.defines["USE_BATCH_UNIFORMS"] = "";
810
+ shader.uniforms["uniformsTexture"] = { value: this.uniformsTexture };
811
+ const { vertex, fragment } = ((_a2 = this.uniformsTexture) == null ? void 0 : _a2.getUniformsGLSL("uniformsTexture", "instanceId", "int")) ?? {
812
+ vertex: "",
813
+ fragment: ""
814
+ };
815
+ const patch = (
816
+ /*glsl*/
817
+ `
818
+ #ifdef gl_DrawID
819
+ #define _gl_DrawID ${batchIdName}
820
+ #else
821
+ #define gl_DrawID ${batchIdName}
822
+ #endif
823
+ in int ${batchIdName};
824
+ `
825
+ );
826
+ const main = "void main() {";
827
+ if (!_BatchedMesh.useMultiDraw) shader.vertexShader = shader.vertexShader.replace(main, `${patch}${main}`);
828
+ if (vertex) shader.vertexShader = shader.vertexShader.replace(main, vertex);
829
+ if (fragment) shader.fragmentShader = shader.fragmentShader.replace(main, fragment);
830
+ };
831
+ material.customProgramCacheKey = () => {
832
+ return `batch_${this.id}_${!!this.uniformsTexture}_${customProgramCacheKey()}`;
833
+ };
834
+ this.isMaterialPatched = true;
835
+ }
836
+ // Taken from https://github.com/agargaro/instanced-mesh/blob/master/src/core/feature/Uniforms.ts
837
+ parseUniformSchema(schema) {
838
+ let totalSize = 0;
839
+ const uniformMap = /* @__PURE__ */ new Map();
840
+ const uniforms = [];
841
+ const vertexSchema = schema.vertex ?? {};
842
+ const fragmentSchema = schema.fragment ?? {};
843
+ let fetchInFragmentShader = true;
844
+ for (const name in vertexSchema) {
845
+ const type = vertexSchema[name];
846
+ const size = this.getUniformSize(type);
847
+ totalSize += size;
848
+ uniforms.push({ name, type, size });
849
+ fetchInFragmentShader = false;
761
850
  }
762
- let orient = this.orientation || defaultOrient;
763
- if (orient !== material._orientation) {
764
- const rotMat = uniforms.uTroikaOrient.value;
765
- orient = orient.replace(/[^-+xyz]/g, "");
766
- const match = orient !== defaultOrient && /^([-+])([xyz])([-+])([xyz])$/.exec(orient);
767
- if (match) {
768
- const [, hSign, hAxis, vSign, vAxis] = match;
769
- tempVec3a.set(0, 0, 0)[hAxis] = hSign === "-" ? 1 : -1;
770
- tempVec3b.set(0, 0, 0)[vAxis] = vSign === "-" ? -1 : 1;
771
- tempMat4.lookAt(origin, tempVec3a.cross(tempVec3b), tempVec3b);
772
- rotMat.setFromMatrix4(tempMat4);
773
- } else {
774
- rotMat.identity();
851
+ for (const name in fragmentSchema) {
852
+ if (!vertexSchema[name]) {
853
+ const type = fragmentSchema[name];
854
+ const size = this.getUniformSize(type);
855
+ totalSize += size;
856
+ uniforms.push({ name, type, size });
775
857
  }
776
- material._orientation = orient;
777
858
  }
859
+ uniforms.sort((a, b) => b.size - a.size);
860
+ const tempOffset = [];
861
+ for (const { name, size, type } of uniforms) {
862
+ const offset = this.getUniformOffset(size, tempOffset);
863
+ uniformMap.set(name, { offset, size, type });
864
+ }
865
+ const pixelsPerInstance = Math.ceil(totalSize / 4);
866
+ const channels = Math.min(totalSize, 4);
867
+ return { channels, texelsPerInstance: pixelsPerInstance, uniformMap, fetchInFragmentShader };
778
868
  }
779
- }
780
- function setDimming(root, dim) {
781
- root.userData["uDim"] = dim === void 0 ? void 0 : +dim;
782
- }
783
- function toggleInstanceDim(object, instanceId, dim) {
784
- const value = dim === void 0 ? 0 : (+dim - 0.5) * 2;
785
- if (object instanceof BatchedMesh) {
786
- object.setUniformAt(instanceId, "skipDimInstance", value);
787
- return;
788
- }
789
- const skipDimTexture = object.userData["skipDimTexture"];
790
- if (skipDimTexture) {
791
- const skipDimData = skipDimTexture.image.data;
792
- skipDimData[instanceId] = value;
793
- skipDimTexture.needsUpdate = true;
794
- }
795
- }
796
- function addDimToMaterial(material) {
797
- if (material.userData.hasDimShader) return;
798
- const onBeforeCompile = material.onBeforeCompile.bind(material);
799
- const onBeforeRender = material.onBeforeRender.bind(material);
800
- material.onBeforeCompile = (shader, renderer) => {
801
- onBeforeCompile(shader, renderer);
802
- shader.uniforms["uDim"] = { value: material.userData.uDim ?? 0 };
803
- shader.uniforms["skipDimTexture"] = { value: material.userData.skipDimTexture ?? null };
804
- shader.vertexShader = shader.vertexShader.replace("void main() {", `${dimColorVertexDefs}
805
- void main() {`).replace(
806
- "#include <fog_vertex>",
807
- /*glsl*/
808
- `
809
- #include <fog_vertex>
810
- setDimAmount();
811
- `
812
- ).concat(dimColorVertexImpl);
813
- shader.fragmentShader = /*glsl*/
814
- `
815
- ${dimColorFrag}
816
- ${shader.fragmentShader}
817
- `.replace(
818
- "#include <colorspace_fragment>",
819
- /*glsl*/
820
- `
821
- gl_FragColor = dimColor(gl_FragColor);
822
- #include <colorspace_fragment>
823
- `
824
- );
825
- material.userData.shader = shader;
826
- };
827
- material.onBeforeRender = (renderer, scene, camera, geometry, object, group) => {
828
- onBeforeRender(renderer, scene, camera, geometry, object, group);
829
- const skipDimTexture = object.userData["skipDimTexture"];
830
- let uDim = object.userData["uDim"];
831
- if (uDim === void 0) {
832
- for (const ancestor of traverseAncestorsGenerator(object)) {
833
- if (ancestor.userData["uDim"] !== void 0) {
834
- uDim = ancestor.userData["uDim"];
835
- break;
869
+ getUniformOffset(size, tempOffset) {
870
+ if (size < 4) {
871
+ for (let i = 0; i < tempOffset.length; i++) {
872
+ if (tempOffset[i] + size <= 4) {
873
+ const offset2 = i * 4 + tempOffset[i];
874
+ tempOffset[i] += size;
875
+ return offset2;
836
876
  }
837
877
  }
838
878
  }
839
- const shader = material.userData.shader;
840
- if (!shader) {
841
- material.userData.uDim = uDim;
842
- material.userData.skipDimTexture = object.userData["skipDimTexture"];
843
- return;
879
+ const offset = tempOffset.length * 4;
880
+ for (; size > 0; size -= 4) {
881
+ tempOffset.push(size);
844
882
  }
845
- shader.uniforms["uDim"].value = uDim ?? 0;
846
- shader.uniforms["skipDimTexture"].value = skipDimTexture ?? null;
847
- };
848
- material.userData.hasDimShader = true;
849
- }
850
- function addDim(mesh) {
851
- if (mesh instanceof BatchedMesh) mesh.addPerInstanceUniforms({ vertex: { skipDimInstance: "float" } });
852
- if (mesh instanceof BatchedText) addSkipDimTexture(mesh);
853
- }
854
- function addSkipDimTexture(text) {
855
- const count = text.size;
856
- const size = Math.ceil(Math.sqrt(count));
857
- const array = new Float32Array(size * size);
858
- array.fill(0);
859
- const texture = new DataTexture(array, size, size, RedFormat, FloatType);
860
- texture.needsUpdate = true;
861
- text.userData["skipDimTexture"] = texture;
862
- text.addEventListener("dispose", () => texture.dispose());
863
- return texture;
864
- }
865
- const dimColorVertexDefs = (
866
- /*glsl*/
867
- `
868
- uniform float uDim;
869
- out float dimAmount;
870
- #ifdef TROIKA_DERIVED_MATERIAL_1
871
- uniform sampler2D skipDimTexture;
872
- #endif
873
- void setDimAmount();
874
- `
875
- );
876
- const dimColorVertexImpl = (
877
- /*glsl*/
878
- `
879
- void setDimAmount() {
880
- float instanceDim = 0.;
881
- #ifdef USE_BATCHING
882
- instanceDim = batch_skipDimInstance;
883
- #endif
884
- #ifdef TROIKA_DERIVED_MATERIAL_1
885
- float indirectIndex = aTroikaTextBatchMemberIndex;
886
- int size = textureSize(skipDimTexture, 0).x;
887
- int i = int(indirectIndex);
888
- int x = i % size;
889
- int y = i / size;
890
- instanceDim = texelFetch(skipDimTexture, ivec2(x, y), 0).r;
891
- #endif
892
- dimAmount = instanceDim == 0. ? uDim : instanceDim / 2. + 0.5;
883
+ return offset;
893
884
  }
894
- `
895
- );
896
- const dimColorFrag = (
897
- /*glsl*/
898
- `
899
- in float dimAmount;
900
-
901
- const vec3 grayWeights = vec3(0.299, 0.587, 0.114);
902
- const float darkenFactor = pow(2., 2.2); // Gamma corrected
903
-
904
- vec4 dimColor(vec4 col) {
905
- vec3 color = col.rgb / col.a;
906
- vec3 gray = vec3(dot(grayWeights, color));
907
- vec3 m = mix(color, gray / darkenFactor, dimAmount);
908
- return vec4(m * col.a, col.a);
909
- }`
910
- );
885
+ getUniformSize(type) {
886
+ switch (type) {
887
+ case "float":
888
+ return 1;
889
+ case "vec2":
890
+ return 2;
891
+ case "vec3":
892
+ return 3;
893
+ case "vec4":
894
+ return 4;
895
+ case "mat3":
896
+ return 9;
897
+ case "mat4":
898
+ return 16;
899
+ }
900
+ }
901
+ getIndexType(gl, index) {
902
+ const array = index.array;
903
+ if (array instanceof Uint16Array) return gl.UNSIGNED_SHORT;
904
+ if (array instanceof Uint32Array) return gl.UNSIGNED_INT;
905
+ return gl.UNSIGNED_BYTE;
906
+ }
907
+ };
908
+ /** Whether to use WebGL_multi_draw extension or less performant fallback (Firefox only) */
909
+ __publicField(_BatchedMesh, "useMultiDraw", true);
910
+ let BatchedMesh = _BatchedMesh;
911
911
  const sharedParameters = {
912
912
  side: DoubleSide,
913
913
  transparent: true,
914
- depthTest: false
914
+ forceSinglePass: true,
915
+ depthFunc: AlwaysDepth
915
916
  };
916
917
  class MaterialSystem {
917
918
  constructor() {
@@ -950,7 +951,6 @@ class MaterialSystem {
950
951
  uniforms["resolution"].value.set(this.viewport.z, this.viewport.w);
951
952
  }
952
953
  };
953
- this.addPolygonOffset(material);
954
954
  addDimToMaterial(material);
955
955
  return material;
956
956
  }
@@ -969,7 +969,6 @@ class MaterialSystem {
969
969
  color: params.color,
970
970
  opacity: params.opacity
971
971
  });
972
- this.addPolygonOffset(material);
973
972
  addDimToMaterial(material);
974
973
  return material;
975
974
  }
@@ -993,7 +992,6 @@ class MaterialSystem {
993
992
  );
994
993
  };
995
994
  }
996
- this.addPolygonOffset(material);
997
995
  addDimToMaterial(material);
998
996
  return material;
999
997
  }
@@ -1014,14 +1012,10 @@ class MaterialSystem {
1014
1012
  `
1015
1013
  );
1016
1014
  };
1017
- this.addPolygonOffset(this.backgroundMaterial);
1018
1015
  addDimToMaterial(this.backgroundMaterial);
1019
1016
  }
1020
1017
  return this.backgroundMaterial;
1021
1018
  }
1022
- addPolygonOffset(material) {
1023
- return;
1024
- }
1025
1019
  }
1026
1020
  function isShapeDef(def) {
1027
1021
  return def.shape !== void 0;
@@ -1053,6 +1047,31 @@ function isLineLayer(layer) {
1053
1047
  function isLayerLayer(layer) {
1054
1048
  return layer.children[0] && isLayerDef(layer.children[0]);
1055
1049
  }
1050
+ function groupBy(list, keyGetter) {
1051
+ const map = /* @__PURE__ */ new Map();
1052
+ list.forEach((item) => {
1053
+ const key = keyGetter(item);
1054
+ const collection = map.get(key);
1055
+ if (!collection) {
1056
+ map.set(key, [item]);
1057
+ } else {
1058
+ collection.push(item);
1059
+ }
1060
+ });
1061
+ return map;
1062
+ }
1063
+ function partition(list, pred) {
1064
+ const truthy = [];
1065
+ const falsy = [];
1066
+ for (const item of list) {
1067
+ if (pred(item)) {
1068
+ truthy.push(item);
1069
+ } else {
1070
+ falsy.push(item);
1071
+ }
1072
+ }
1073
+ return [truthy, falsy];
1074
+ }
1056
1075
  const INTERACTIVE_LAYER = 1;
1057
1076
  function setInteractive(object, isInteractive) {
1058
1077
  if (isInteractive) object.layers.enable(INTERACTIVE_LAYER);
@@ -1063,19 +1082,6 @@ function isVisible(object) {
1063
1082
  if (!object.visible) return false;
1064
1083
  return [...traverseAncestorsGenerator(object)].every((obj) => obj.visible);
1065
1084
  }
1066
- function printTree(object, fullName = false) {
1067
- object.traverse((obj) => {
1068
- let s = "";
1069
- let obj2 = obj;
1070
- while (obj2 !== object) {
1071
- s = "|___ " + s;
1072
- obj2 = (obj2 == null ? void 0 : obj2.parent) ?? null;
1073
- }
1074
- const renderOrder = obj.isGroup ? "" : `, RO: ${obj.renderOrder}`;
1075
- const name = fullName ? obj.name : obj.name.split(":").at(-1);
1076
- console.log(`${s}${name}<${obj.type}>${renderOrder}`);
1077
- });
1078
- }
1079
1085
  class RenderableSystem {
1080
1086
  /**
1081
1087
  * @param type readable name of the system's type for debugging
@@ -1126,6 +1132,15 @@ class RenderableSystem {
1126
1132
  const shapeDef = this.getDefsByObject(mesh)[batchId];
1127
1133
  return shapeDef;
1128
1134
  }
1135
+ /**
1136
+ * Dispose all objects and clear the mappings
1137
+ */
1138
+ dispose() {
1139
+ for (const object of this.getAllObjects()) {
1140
+ this.disposeObject(object);
1141
+ }
1142
+ this.clearMappings();
1143
+ }
1129
1144
  /**
1130
1145
  * Protected implementation method that subclasses can override.
1131
1146
  * This ensures logging always happens even when the method is overridden.
@@ -1298,10 +1313,10 @@ class ImageSystem extends RenderableSystem {
1298
1313
  /** Textures memory limit in megabytes */
1299
1314
  __publicField(this, "memoryLimitMb");
1300
1315
  __publicField(this, "packer");
1301
- __publicField(this, "position", new Vector3());
1302
- __publicField(this, "rotation", new Quaternion());
1303
- __publicField(this, "scale", new Vector3());
1304
- __publicField(this, "matrix", new Matrix4());
1316
+ __publicField(this, "globalTranslationMatrix", new Matrix4());
1317
+ __publicField(this, "originTranslationMatrix", new Matrix4());
1318
+ __publicField(this, "rotationMatrix", new Matrix4());
1319
+ __publicField(this, "scaleMatrix", new Matrix4());
1305
1320
  this.materialSystem = materialSystem;
1306
1321
  const atlasTextureSize = renderer.context.capabilities.maxTextureSize;
1307
1322
  console.log(`Max texture size: ${atlasTextureSize}`);
@@ -1401,17 +1416,13 @@ class ImageSystem extends RenderableSystem {
1401
1416
  }
1402
1417
  updateDefImpl(imageDef, mesh, instanceIds) {
1403
1418
  const bounds = imageDef.bounds;
1404
- this.position.set(bounds.center.x, bounds.center.y, 0);
1405
- if (imageDef.origin) {
1406
- const xFactor = 0.5 - imageDef.origin[0];
1407
- const yFactor = 0.5 - imageDef.origin[1];
1408
- this.position.x += bounds.size.width * xFactor;
1409
- this.position.y += bounds.size.height * yFactor;
1410
- }
1411
- this.scale.set(bounds.size.width, bounds.size.height, 1);
1412
- this.rotation.setFromAxisAngle(new Vector3(0, 0, 1), bounds.rotation);
1413
- this.matrix.compose(this.position, this.rotation, this.scale);
1414
- mesh.setMatrixAt(instanceIds[0], this.matrix);
1419
+ const origin2 = imageDef.origin ?? [0.5, 0.5];
1420
+ this.originTranslationMatrix.makeTranslation(0.5 - origin2[0], 0.5 - origin2[1], 0);
1421
+ this.globalTranslationMatrix.makeTranslation(bounds.center.x, bounds.center.y, 0);
1422
+ this.scaleMatrix.makeScale(bounds.size.width, bounds.size.height, 1);
1423
+ this.rotationMatrix.makeRotationZ(bounds.rotation);
1424
+ const matrix = this.originTranslationMatrix.premultiply(this.scaleMatrix).premultiply(this.rotationMatrix).premultiply(this.globalTranslationMatrix);
1425
+ mesh.setMatrixAt(instanceIds[0], matrix);
1415
1426
  }
1416
1427
  packImages(images) {
1417
1428
  this.packer.reset();
@@ -1545,7 +1556,15 @@ class LineSystem extends RenderableSystem {
1545
1556
  }
1546
1557
  }
1547
1558
  function createVector2(vector2) {
1548
- return vector2 instanceof Vector2 ? vector2 : Array.isArray(vector2) ? new Vector2(vector2[0], vector2[1]) : new Vector2(vector2.x, vector2.y);
1559
+ if (vector2 instanceof Vector2) return vector2;
1560
+ if (Array.isArray(vector2)) return new Vector2(vector2[0], vector2[1]);
1561
+ return new Vector2(vector2.x, vector2.y);
1562
+ }
1563
+ function createVector3(vector3) {
1564
+ if (vector3 instanceof Vector2) return new Vector3(vector3.x, vector3.y, 0);
1565
+ if (vector3 instanceof Vector3) return vector3;
1566
+ if (Array.isArray(vector3)) return new Vector3(vector3[0], vector3[1], vector3[2] ?? 0);
1567
+ return new Vector3(vector3.x, vector3.y, "z" in vector3 ? vector3.z : 0);
1549
1568
  }
1550
1569
  class Rect {
1551
1570
  /**
@@ -1613,7 +1632,7 @@ class Polygon {
1613
1632
  __publicField(this, "vertices");
1614
1633
  /** Array of polygon indices. Each index is a triplet of vertex indices forming a triangle. */
1615
1634
  __publicField(this, "indices");
1616
- this.vertices = vertices.map(createVector2);
1635
+ this.vertices = vertices.map(createVector3);
1617
1636
  this.indices = indices;
1618
1637
  }
1619
1638
  /**
@@ -1652,41 +1671,19 @@ class Polygon {
1652
1671
  }
1653
1672
  /**
1654
1673
  * Rotates all vertices of the polygon around the given center.
1655
- * @param rotation Rotation angle in radians. Positive values rotate clockwise.
1656
- * @param center Center of the rotation.
1657
- * @returns this {@link Polygon} instance
1658
- */
1659
- rotate(rotation, center) {
1660
- this.vertices.forEach((vertex) => {
1661
- vertex.rotateAround(center, rotation);
1662
- });
1663
- return this;
1664
- }
1665
- }
1666
- function groupBy(list, keyGetter) {
1667
- const map = /* @__PURE__ */ new Map();
1668
- list.forEach((item) => {
1669
- const key = keyGetter(item);
1670
- const collection = map.get(key);
1671
- if (!collection) {
1672
- map.set(key, [item]);
1673
- } else {
1674
- collection.push(item);
1675
- }
1676
- });
1677
- return map;
1678
- }
1679
- function partition(list, pred) {
1680
- const truthy = [];
1681
- const falsy = [];
1682
- for (const item of list) {
1683
- if (pred(item)) {
1684
- truthy.push(item);
1685
- } else {
1686
- falsy.push(item);
1687
- }
1674
+ * @param rotation Rotation angle in radians. Positive values rotate clockwise.
1675
+ * @param center Center of the rotation.
1676
+ * @returns this {@link Polygon} instance
1677
+ */
1678
+ rotate(rotation, center) {
1679
+ const tempVec2 = new Vector2();
1680
+ this.vertices.forEach((vertex) => {
1681
+ tempVec2.set(vertex.x, vertex.y);
1682
+ tempVec2.rotateAround(center, rotation);
1683
+ vertex.set(tempVec2.x, tempVec2.y, vertex.z);
1684
+ });
1685
+ return this;
1688
1686
  }
1689
- return [truthy, falsy];
1690
1687
  }
1691
1688
  class MeshSystem extends RenderableSystem {
1692
1689
  /**
@@ -1725,6 +1722,12 @@ class MeshSystem extends RenderableSystem {
1725
1722
  opaqueMesh.name = "opaque";
1726
1723
  group.add(opaqueMesh);
1727
1724
  }
1725
+ if (layer.mode === "3d") {
1726
+ group.children.filter((child) => child instanceof Mesh).forEach((mesh) => {
1727
+ const material = mesh.material;
1728
+ material.depthFunc = LessEqualDepth;
1729
+ });
1730
+ }
1728
1731
  return group;
1729
1732
  }
1730
1733
  updateDefImpl(shapeDef, mesh, instanceIds) {
@@ -1741,12 +1744,10 @@ class MeshSystem extends RenderableSystem {
1741
1744
  let rectGeometry = void 0;
1742
1745
  const shapeDefToGeometry = /* @__PURE__ */ new Map();
1743
1746
  for (const shapeDef of shapes) {
1744
- if (shapeDef.shape instanceof Rect) {
1745
- if (!rectGeometry) {
1746
- rectGeometry = new PlaneGeometry(1, 1);
1747
- rectGeometry.deleteAttribute("normal");
1748
- rectGeometry.deleteAttribute("uv");
1749
- }
1747
+ if (shapeDef.shape instanceof Rect && !rectGeometry) {
1748
+ rectGeometry = new PlaneGeometry(1, 1);
1749
+ rectGeometry.deleteAttribute("normal");
1750
+ rectGeometry.deleteAttribute("uv");
1750
1751
  vertexCount += rectGeometry.getAttribute("position").count;
1751
1752
  indexCount += ((_a2 = rectGeometry.index) == null ? void 0 : _a2.count) ?? 0;
1752
1753
  } else if (shapeDef.shape instanceof Polygon) {
@@ -1758,6 +1759,7 @@ class MeshSystem extends RenderableSystem {
1758
1759
  }
1759
1760
  const material = this.materialSystem.createColorMaterial({ opacity });
1760
1761
  const batchedMesh = new BatchedMesh(shapes.length, vertexCount, indexCount, material);
1762
+ const rectGeometryId = rectGeometry ? batchedMesh.addGeometry(rectGeometry) : void 0;
1761
1763
  batchedMesh.setCustomSort((list) => this.sortInstances(batchedMesh, list));
1762
1764
  const position = new Vector3();
1763
1765
  const rotation = new Quaternion();
@@ -1765,8 +1767,7 @@ class MeshSystem extends RenderableSystem {
1765
1767
  const matrix = new Matrix4();
1766
1768
  for (const shapeDef of shapes) {
1767
1769
  let instanceId = void 0;
1768
- if (shapeDef.shape instanceof Rect) {
1769
- const rectGeometryId = rectGeometry ? batchedMesh.addGeometry(rectGeometry) : void 0;
1770
+ if (shapeDef.shape instanceof Rect && rectGeometryId !== void 0) {
1770
1771
  instanceId = batchedMesh.addInstance(rectGeometryId);
1771
1772
  position.set(shapeDef.shape.center.x, shapeDef.shape.center.y, 0);
1772
1773
  rotation.setFromAxisAngle(new Vector3(0, 0, 1), shapeDef.shape.rotation ?? 0);
@@ -1811,6 +1812,7 @@ class TextSystem extends RenderableSystem {
1811
1812
  __publicField(this, "initialTextScale", new Vector2(1, -1));
1812
1813
  __publicField(this, "textColor", new Color());
1813
1814
  __publicField(this, "pendingUpdates", /* @__PURE__ */ new Map());
1815
+ __publicField(this, "sdfAtlases", /* @__PURE__ */ new Set());
1814
1816
  __publicField(this, "alignmentOffset", new Vector2());
1815
1817
  __publicField(this, "alignmentDirection", new Vector2());
1816
1818
  __publicField(this, "localPosition", new Vector2());
@@ -1820,6 +1822,10 @@ class TextSystem extends RenderableSystem {
1820
1822
  __publicField(this, "localToMax", new Vector2());
1821
1823
  this.materialSystem = materialSystem;
1822
1824
  }
1825
+ dispose() {
1826
+ super.dispose();
1827
+ this.sdfAtlases.forEach((texture) => texture.dispose());
1828
+ }
1823
1829
  buildLayer(layer) {
1824
1830
  const group = new Group();
1825
1831
  const batchedText = this.buildBatchedText(layer);
@@ -1897,7 +1903,12 @@ class TextSystem extends RenderableSystem {
1897
1903
  for (const { textDef, instanceIds } of mappingData) {
1898
1904
  this.registerDefObject(textDef, batchedText, instanceIds);
1899
1905
  }
1900
- batchedText.addEventListener("synccomplete", () => this.renderer.update());
1906
+ batchedText.addEventListener("synccomplete", () => {
1907
+ var _a2;
1908
+ const sdfTexture = (_a2 = batchedText.textRenderInfo) == null ? void 0 : _a2.sdfTexture;
1909
+ if (sdfTexture) this.sdfAtlases.add(sdfTexture);
1910
+ this.renderer.update();
1911
+ });
1901
1912
  return batchedText;
1902
1913
  }
1903
1914
  // TODO: Simplify
@@ -1990,6 +2001,7 @@ class LayerSystem {
1990
2001
  * @param renderer {@link Renderer}
1991
2002
  */
1992
2003
  constructor(renderer) {
2004
+ __publicField(this, "backgroundMesh");
1993
2005
  __publicField(this, "materialSystem");
1994
2006
  __publicField(this, "meshSystem");
1995
2007
  __publicField(this, "imageSystem");
@@ -2029,6 +2041,7 @@ class LayerSystem {
2029
2041
  }
2030
2042
  /**
2031
2043
  * Update the given defs immediately, or queue them for update if update buffering is enabled.
2044
+ * NOTE: Currently update buffering is disabled, as observed performance gains are negligible. Need to revisit this.
2032
2045
  * @param defs {@link RenderableDef} array
2033
2046
  */
2034
2047
  updateDefs(defs) {
@@ -2089,6 +2102,18 @@ class LayerSystem {
2089
2102
  }
2090
2103
  return rootGroup;
2091
2104
  }
2105
+ /**
2106
+ * Dispose all objects and clear the mappings
2107
+ */
2108
+ disposeScene() {
2109
+ var _a2, _b, _c;
2110
+ for (const system of this.systems) {
2111
+ system.dispose();
2112
+ }
2113
+ (_a2 = this.backgroundMesh) == null ? void 0 : _a2.geometry.dispose();
2114
+ (_c = (_b = this.backgroundMesh) == null ? void 0 : _b.material) == null ? void 0 : _c.dispose();
2115
+ this.backgroundMesh = void 0;
2116
+ }
2092
2117
  updateDef(def) {
2093
2118
  if (isShapeDef(def)) this.meshSystem.updateDef(def);
2094
2119
  else if (isImageDef(def)) this.imageSystem.updateDef(def);
@@ -2153,6 +2178,7 @@ class LayerSystem {
2153
2178
  backgroundMesh.frustumCulled = false;
2154
2179
  backgroundMesh.renderOrder = 0;
2155
2180
  backgroundMesh.name = "background";
2181
+ this.backgroundMesh = backgroundMesh;
2156
2182
  return backgroundMesh;
2157
2183
  }
2158
2184
  initRenderOrder(rootLayer) {
@@ -2170,6 +2196,8 @@ class LayerSystem {
2170
2196
  this.layerDefRenderOrder.push(layer);
2171
2197
  }
2172
2198
  }
2199
+ const [threeDLayers, twoDLayers] = partition(this.layerDefRenderOrder, (layer) => layer.mode === "3d");
2200
+ this.layerDefRenderOrder = [...twoDLayers, ...threeDLayers];
2173
2201
  }
2174
2202
  getFullLayerName(layerDef) {
2175
2203
  let fullName = layerDef.name;
@@ -2181,6 +2209,19 @@ class LayerSystem {
2181
2209
  return fullName;
2182
2210
  }
2183
2211
  }
2212
+ function printTree(object, fullName = false) {
2213
+ object.traverse((obj) => {
2214
+ let s = "";
2215
+ let obj2 = obj;
2216
+ while (obj2 !== object) {
2217
+ s = "|___ " + s;
2218
+ obj2 = (obj2 == null ? void 0 : obj2.parent) ?? null;
2219
+ }
2220
+ const renderOrder = obj.isGroup ? "" : `, RO: ${obj.renderOrder}`;
2221
+ const name = fullName ? obj.name : obj.name.split(":").at(-1);
2222
+ console.log(`${s}${name}<${obj.type}>${renderOrder}`);
2223
+ });
2224
+ }
2184
2225
  /*!
2185
2226
  * camera-controls
2186
2227
  * https://github.com/yomotsu/camera-controls
@@ -4717,6 +4758,20 @@ class CameraController extends CameraControls {
4717
4758
  constructor(camera, renderer) {
4718
4759
  super(camera);
4719
4760
  this.renderer = renderer;
4761
+ this.dollyToCursor = true;
4762
+ this.draggingSmoothTime = 0;
4763
+ void this.rotatePolarTo(0, false);
4764
+ this.mouseButtons = {
4765
+ left: CameraController.ACTION.NONE,
4766
+ middle: CameraController.ACTION.NONE,
4767
+ right: CameraController.ACTION.NONE,
4768
+ wheel: CameraController.ACTION.NONE
4769
+ };
4770
+ this.touches = {
4771
+ one: CameraController.ACTION.NONE,
4772
+ two: CameraController.ACTION.NONE,
4773
+ three: CameraController.ACTION.NONE
4774
+ };
4720
4775
  }
4721
4776
  update(delta) {
4722
4777
  var _a2;
@@ -4737,31 +4792,30 @@ class CameraSystem {
4737
4792
  * @param renderer {@link Renderer} instance
4738
4793
  */
4739
4794
  constructor(renderer) {
4740
- /** External camera instance. Used to render the scene in external mode (e.g. Mapbox GL JS). */
4741
- __publicField(this, "externalCamera");
4795
+ /** {@link PerspectiveCamera} instance. Used to render the scene in internal mode. */
4796
+ __publicField(this, "camera");
4742
4797
  /** {@link CameraController} instance. Used to smoothly animate the camera. */
4743
4798
  __publicField(this, "controller");
4744
- __publicField(this, "camera");
4745
- __publicField(this, "zoomIdentityDistance");
4799
+ /**
4800
+ * Cached previous viewport height used to preserve zoom across resizes.
4801
+ * Note: we intentionally keep this separate from the derived identity distance.
4802
+ */
4803
+ __publicField(this, "prevViewportHeightPx");
4804
+ /** [min, max] zoom factors */
4746
4805
  __publicField(this, "zoomBounds");
4806
+ /** Default FOV for the camera. Taken from Mapbox GL JS. */
4807
+ __publicField(this, "defaultFov", 36.87);
4747
4808
  this.renderer = renderer;
4748
- const [w, h] = renderer.size;
4749
- this.camera = new PerspectiveCamera(90, w / (h || 1));
4809
+ const h = renderer.size[1];
4810
+ this.prevViewportHeightPx = h;
4811
+ this.camera = new PerspectiveCamera(this.defaultFov);
4750
4812
  this.camera.up.set(0, 0, -1);
4751
- this.zoomIdentityDistance = h / 2;
4752
- this.camera.position.z = this.zoomIdentityDistance;
4753
- this.controller = new CameraController(this.camera, renderer);
4754
- this.controller.polarAngle = 0;
4813
+ this.controller = new CameraController(this.camera, this.renderer);
4755
4814
  this.controller.distance = this.zoomIdentityDistance;
4756
4815
  }
4757
- /** Current camera instance. */
4758
- get currentCamera() {
4759
- return this.externalCamera ?? this.camera;
4760
- }
4761
4816
  /** Current camera zoom factor. */
4762
4817
  get zoomFactor() {
4763
- const distance = this.controller.distance;
4764
- return distance ? this.zoomIdentityDistance / distance : 1;
4818
+ return this.zoomFactorForHeight(this.renderer.size[1]);
4765
4819
  }
4766
4820
  /**
4767
4821
  * Calculates the camera distance from the scene's plane for a given zoom factor.
@@ -4769,7 +4823,7 @@ class CameraSystem {
4769
4823
  * @returns Corresponding camera distance on the Z axis
4770
4824
  */
4771
4825
  zoomFactorToDistance(zoomFactor) {
4772
- return zoomFactor > 0 ? this.zoomIdentityDistance / zoomFactor : this.zoomIdentityDistance;
4826
+ return this.zoomIdentityDistance / zoomFactor;
4773
4827
  }
4774
4828
  /**
4775
4829
  * Initializes the camera with the given zoom bounds.
@@ -4783,41 +4837,188 @@ class CameraSystem {
4783
4837
  updateCamera() {
4784
4838
  if (!this.zoomBounds) return;
4785
4839
  const [w, h] = this.renderer.size;
4786
- const zoomFactor = this.zoomFactor;
4787
- this.zoomIdentityDistance = h / 2;
4788
- const maxDistance = Math.abs(this.zoomIdentityDistance / this.zoomBounds[0]);
4789
- const minDistance = Math.abs(this.zoomIdentityDistance / this.zoomBounds[1]);
4840
+ if (w <= 0 || h <= 0) return;
4841
+ const zoomFactor = this.zoomFactorForHeight(this.prevViewportHeightPx);
4842
+ const newZoomIdentity = this.zoomIdentityDistanceForHeight(h);
4843
+ const maxDistance = Math.abs(newZoomIdentity / this.zoomBounds[0]);
4844
+ const minDistance = Math.abs(newZoomIdentity / this.zoomBounds[1]);
4790
4845
  this.camera.aspect = w / (h || 1);
4791
- this.computeCameraClipPlanes(minDistance, maxDistance);
4846
+ this.camera.near = 0.01;
4847
+ this.camera.far = Math.max(maxDistance, this.camera.near) * 2;
4792
4848
  this.camera.updateProjectionMatrix();
4793
- this.syncController(minDistance, maxDistance, zoomFactor);
4794
- }
4795
- computeCameraClipPlanes(minDistance, maxDistance, nearSafetyFactor = 0.5, farSafetyFactor = 1.5) {
4796
- const fov = this.camera.fov * DEG2RAD$1;
4797
- const aspect = this.camera.aspect;
4798
- const maxPolarAngle = 85 * DEG2RAD$1;
4799
- const halfFovY = fov / 2;
4800
- const halfFovX = Math.atan(Math.tan(halfFovY) * aspect);
4801
- const diagonalFov = 2 * Math.atan(Math.sqrt(Math.tan(halfFovX) ** 2 + Math.tan(halfFovY) ** 2));
4802
- const minHeight = minDistance * Math.cos(maxPolarAngle);
4803
- const near = Math.max(0.1, minHeight * nearSafetyFactor);
4804
- const criticalHeight = minHeight;
4805
- const horizontalDistToOrbit = minDistance * Math.sin(maxPolarAngle);
4806
- const distToOrbit = minDistance;
4807
- const visibleRadiusAtOrbit = distToOrbit * Math.tan(diagonalFov / 2);
4808
- const planeExtent = Math.max(maxDistance, visibleRadiusAtOrbit);
4809
- const horizontalDistToFarEdge = horizontalDistToOrbit + planeExtent;
4810
- const maxViewDistance = Math.sqrt(criticalHeight ** 2 + horizontalDistToFarEdge ** 2);
4811
- const far = maxViewDistance * farSafetyFactor;
4812
- this.camera.near = near;
4813
- this.camera.far = far;
4814
- if (this.renderer.debugLog) console.log("camera clip planes", near, far);
4815
- }
4816
- syncController(minDistance, maxDistance, zoomFactor) {
4817
- if (this.renderer.debugLog) console.log("syncController", minDistance, maxDistance, zoomFactor);
4818
4849
  this.controller.minDistance = minDistance;
4819
4850
  this.controller.maxDistance = maxDistance;
4820
4851
  void this.controller.dollyTo(this.zoomFactorToDistance(zoomFactor), false);
4852
+ this.prevViewportHeightPx = h;
4853
+ }
4854
+ /**
4855
+ * Distance from the scene plane corresponding to zoomFactor = 1 for the current viewport height.
4856
+ * Derived from camera FOV and renderer height (in pixels).
4857
+ */
4858
+ get zoomIdentityDistance() {
4859
+ return this.zoomIdentityDistanceForHeight(this.renderer.size[1]);
4860
+ }
4861
+ /**
4862
+ * Calculates the zoom identity distance for a given viewport height.
4863
+ * @param viewportHeightPx Renderer height in pixels
4864
+ * @returns Zoom identity distance
4865
+ */
4866
+ zoomIdentityDistanceForHeight(viewportHeightPx) {
4867
+ if (viewportHeightPx <= 0) return 0;
4868
+ return viewportHeightPx * 0.5 / Math.tan(this.camera.fov * DEG2RAD$1 / 2);
4869
+ }
4870
+ /**
4871
+ * Calculates the zoom factor for a given viewport height.
4872
+ * @param viewportHeightPx Renderer height in pixels
4873
+ * @returns Zoom factor
4874
+ */
4875
+ zoomFactorForHeight(viewportHeightPx) {
4876
+ const zid = this.zoomIdentityDistanceForHeight(viewportHeightPx);
4877
+ if (zid === 0) return 1;
4878
+ return zid / (this.controller.distance || zid);
4879
+ }
4880
+ }
4881
+ class ExternalSystem {
4882
+ /**
4883
+ * @param pickingSystem {@link PickingSystem} instance
4884
+ */
4885
+ constructor(pickingSystem) {
4886
+ /** External camera instance */
4887
+ __publicField(this, "camera", new Camera());
4888
+ __publicField(this, "staticTransformMatrix", new Matrix4());
4889
+ __publicField(this, "intersectionPoint", new Vector3());
4890
+ /**
4891
+ * Scratch NDC coordinate used by external ptScale. Kept as a field to avoid allocating a Vector2 each frame.
4892
+ * (This is always screen center: NDC (0,0))
4893
+ */
4894
+ __publicField(this, "ndcCenter", new Vector2(0, 0));
4895
+ /**
4896
+ * Scratch clip-space vector used when projecting SVG points through the external camera matrix.
4897
+ * Kept as a field to avoid allocating a Vector4 each frame.
4898
+ */
4899
+ __publicField(this, "clipPoint", new Vector4());
4900
+ /**
4901
+ * Scratch pixel points used by external ptScale estimation.
4902
+ * p0 is the screen center in drawing-buffer pixels; p1/p2 are projected offsets from the anchor point.
4903
+ */
4904
+ __publicField(this, "px0", new Vector2());
4905
+ __publicField(this, "px1", new Vector2());
4906
+ __publicField(this, "px2", new Vector2());
4907
+ this.pickingSystem = pickingSystem;
4908
+ }
4909
+ /**
4910
+ * Set static part of an svg -> px transform matrix
4911
+ * @param staticTransformMatrix static transform matrix to apply to the scene
4912
+ */
4913
+ setStaticTransform(staticTransformMatrix) {
4914
+ if (!this.validateMatrix(staticTransformMatrix, "setStaticTransform")) return;
4915
+ this.staticTransformMatrix.fromArray(staticTransformMatrix);
4916
+ }
4917
+ /**
4918
+ * Set dynamic part of an svg -> px transform matrix. Should be called every frame.
4919
+ * @param dynamicTransformMatrix dynamic transform matrix (changes every frame)
4920
+ */
4921
+ setDynamicTransform(dynamicTransformMatrix) {
4922
+ if (!this.validateMatrix(dynamicTransformMatrix, "setDynamicTransform")) return;
4923
+ this.camera.projectionMatrix.fromArray(dynamicTransformMatrix).multiply(this.staticTransformMatrix);
4924
+ this.camera.projectionMatrixInverse.copy(this.camera.projectionMatrix).invert();
4925
+ }
4926
+ /**
4927
+ * Estimates pixel→SVG scale for external context rendering (e.g. Mapbox).
4928
+ *
4929
+ * In external mode we don't own a camera rig/controller, so we can't derive scale from "camera distance".
4930
+ * Instead, we:
4931
+ * 1) Find the point on the SVG plane that is currently under the screen center.
4932
+ * 2) Measure how much the screen position changes when moving 1 SVG unit in X/Y around that point.
4933
+ * 3) Convert that local plane→screen mapping into a single "zoom-like" scalar that is stable under tilt+rotate.
4934
+ *
4935
+ * This matches internal mode semantics better because the internal orbit target is also kept under screen center.
4936
+ * @param viewportSize Size of the viewport in drawing-buffer pixels
4937
+ * @returns Pixel-to-SVG scale factor (px → svg units)
4938
+ */
4939
+ pxToSvgScale(viewportSize) {
4940
+ const M = this.camera.projectionMatrix;
4941
+ const [viewportW, viewportH] = viewportSize;
4942
+ if (viewportW <= 0 || viewportH <= 0) return;
4943
+ const intersectionPoint = this.pickingSystem.intersectPlane(this.ndcCenter, this.camera, this.intersectionPoint);
4944
+ if (!intersectionPoint) return;
4945
+ const anchorX = intersectionPoint.x;
4946
+ const anchorY = intersectionPoint.y;
4947
+ const clip = this.clipPoint;
4948
+ const p0 = this.px0.set(viewportW * 0.5, viewportH * 0.5);
4949
+ const p1 = this.px1;
4950
+ const p2 = this.px2;
4951
+ const svgToPixels = (x, y, out) => {
4952
+ clip.set(x, y, 0, 1).applyMatrix4(M);
4953
+ if (clip.w === 0) return false;
4954
+ const ndcX = clip.x / clip.w;
4955
+ const ndcY = clip.y / clip.w;
4956
+ out.set((ndcX + 1) * 0.5 * viewportW, (1 - ndcY) * 0.5 * viewportH);
4957
+ return true;
4958
+ };
4959
+ const svgStep = 1;
4960
+ if (!svgToPixels(anchorX + svgStep, anchorY, p1)) return;
4961
+ if (!svgToPixels(anchorX, anchorY + svgStep, p2)) return;
4962
+ const pxDeltaPerSvgX = p1.sub(p0).divideScalar(svgStep);
4963
+ const pxDeltaPerSvgY = p2.sub(p0).divideScalar(svgStep);
4964
+ const pixelsSqPerSvgX = pxDeltaPerSvgX.dot(pxDeltaPerSvgX);
4965
+ const pixelsSqPerSvgY = pxDeltaPerSvgY.dot(pxDeltaPerSvgY);
4966
+ const pixelsSqCross = pxDeltaPerSvgX.dot(pxDeltaPerSvgY);
4967
+ const sumPixelsSq = pixelsSqPerSvgX + pixelsSqPerSvgY;
4968
+ const areaPixelsSq = pixelsSqPerSvgX * pixelsSqPerSvgY - pixelsSqCross * pixelsSqCross;
4969
+ const maxStretchDiscriminant = Math.max(0, sumPixelsSq * sumPixelsSq - 4 * areaPixelsSq);
4970
+ const maxPixelsSqPerSvg = 0.5 * (sumPixelsSq + Math.sqrt(maxStretchDiscriminant));
4971
+ const pxPerSvg = Math.sqrt(maxPixelsSqPerSvg);
4972
+ if (!Number.isFinite(pxPerSvg) || pxPerSvg <= 0) return;
4973
+ return 1 / pxPerSvg;
4974
+ }
4975
+ validateMatrix(matrix, name) {
4976
+ if (matrix.length !== 16) {
4977
+ console.warn(`[ViewportSystem.${name}]: Matrix must be 16 elements long`);
4978
+ return false;
4979
+ }
4980
+ return true;
4981
+ }
4982
+ }
4983
+ class PickingSystem {
4984
+ /** */
4985
+ constructor() {
4986
+ __publicField(this, "raycaster", new Raycaster());
4987
+ __publicField(this, "ndcPoint", new Vector2());
4988
+ __publicField(this, "viewboxPlane", new Plane(new Vector3(0, 0, 1), 0));
4989
+ this.raycaster.layers.set(INTERACTIVE_LAYER);
4990
+ }
4991
+ /**
4992
+ * Gets the objects intersected by the raycaster.
4993
+ * @param ndcCoords raycast point in NDC (normalized device coordinates)
4994
+ * @param scene {@link Scene} instance
4995
+ * @param camera {@link Camera} instance
4996
+ * @returns Array of {@link Intersection} instances
4997
+ */
4998
+ getIntersectedObjects(ndcCoords, scene, camera) {
4999
+ this.setRaycasterFromCamera(ndcCoords, camera);
5000
+ const intersections = this.raycaster.intersectObject(scene, true);
5001
+ return intersections.filter((i) => isVisible(i.object));
5002
+ }
5003
+ /**
5004
+ * Intersects the xy-plane with the raycaster.
5005
+ * @param ndcCoords raycast point in NDC (normalized device coordinates
5006
+ * @param camera {@link Camera} instance
5007
+ * @param out Output vector
5008
+ * @returns Intersection point in world space or null if no intersection.
5009
+ */
5010
+ intersectPlane(ndcCoords, camera, out) {
5011
+ this.setRaycasterFromCamera(ndcCoords, camera);
5012
+ return this.raycaster.ray.intersectPlane(this.viewboxPlane, out) ?? void 0;
5013
+ }
5014
+ setRaycasterFromCamera(ndcCoords, camera) {
5015
+ if (camera.isPerspectiveCamera || camera.isOrthographicCamera) {
5016
+ this.ndcPoint.set(ndcCoords.x, ndcCoords.y);
5017
+ this.raycaster.setFromCamera(this.ndcPoint, camera);
5018
+ } else {
5019
+ this.raycaster.ray.origin.set(0, 0, 0).unproject(camera);
5020
+ this.raycaster.ray.direction.set(ndcCoords.x, ndcCoords.y, 1).unproject(camera).sub(this.raycaster.ray.origin).normalize();
5021
+ }
4821
5022
  }
4822
5023
  }
4823
5024
  class SceneSystem {
@@ -4827,29 +5028,22 @@ class SceneSystem {
4827
5028
  constructor(renderer) {
4828
5029
  /** {@link Scene} instance */
4829
5030
  __publicField(this, "scene");
4830
- /** World matrix - SVGWorld transform */
5031
+ /** World matrix - modelworld transform */
4831
5032
  __publicField(this, "worldMatrix", new Matrix4());
4832
- /** Inverse world matrix - WorldSVG transform */
5033
+ /** Inverse world matrix - worldmodel transform */
4833
5034
  __publicField(this, "inverseWorldMatrix", new Matrix4());
5035
+ __publicField(this, "tempVector3", new Vector3());
4834
5036
  __publicField(this, "translationMatrix", new Matrix4());
4835
5037
  __publicField(this, "scaleMatrix", new Matrix4());
4836
- __publicField(this, "scaleVector", new Vector3());
4837
5038
  __publicField(this, "visibleRectOffsetMatrix", new Matrix4());
4838
5039
  __publicField(this, "viewbox");
4839
5040
  this.renderer = renderer;
4840
5041
  this.scene = new Scene();
4841
5042
  this.scene.matrixAutoUpdate = false;
4842
5043
  }
4843
- /** Scene scale factor (SVG to pixel) */
5044
+ /** Scene scale factor (model space to world space) */
4844
5045
  get scaleFactor() {
4845
- this.scaleVector.setFromMatrixScale(this.scene.matrix);
4846
- if (this.scaleVector.z === 1) {
4847
- return this.scaleVector.x;
4848
- } else {
4849
- const perspectiveW = this.scene.matrix.elements[15];
4850
- const halfViewportWidth = this.renderer.size[0] / 2;
4851
- return halfViewportWidth * this.scaleVector.x / perspectiveW;
4852
- }
5046
+ return this.scene.matrix.elements[0];
4853
5047
  }
4854
5048
  /**
4855
5049
  * Initializes the scene with the given SVG viewbox.
@@ -4859,28 +5053,48 @@ class SceneSystem {
4859
5053
  this.viewbox = viewbox;
4860
5054
  this.updateScene();
4861
5055
  }
4862
- /**
4863
- * Updates the scene transform when the renderer size changes.
4864
- */
5056
+ /** Updates the scene transform from the current viewbox and renderer size. */
4865
5057
  updateScene() {
4866
5058
  if (!this.viewbox) return;
5059
+ this.composeMatrices(this.viewbox);
5060
+ }
5061
+ /**
5062
+ * Converts a point from model coordinates to world coordinates.
5063
+ * @param modelCoords Point in model coordinates
5064
+ * @param out Output vector
5065
+ * @returns Point in world coordinates
5066
+ */
5067
+ modelToWorld(modelCoords, out) {
5068
+ const worldPoint = this.tempVector3.set(modelCoords.x, modelCoords.y, 0).applyMatrix4(this.worldMatrix);
5069
+ out.set(worldPoint.x, worldPoint.y, 0);
5070
+ return out;
5071
+ }
5072
+ /**
5073
+ * Converts a point from world coordinates to model coordinates. Z axis is ignored.
5074
+ * @param worldCoords Point in world coordinates
5075
+ * @param out Output vector
5076
+ * @returns Point in SVG coordinates
5077
+ */
5078
+ worldToModel(worldCoords, out) {
5079
+ const modelPoint = this.tempVector3.copy(worldCoords).applyMatrix4(this.inverseWorldMatrix);
5080
+ out.set(modelPoint.x, modelPoint.y);
5081
+ return out;
5082
+ }
5083
+ composeMatrices(viewbox) {
4867
5084
  const dpr = this.renderer.context.getPixelRatio();
4868
5085
  const visibleRect = this.renderer.visibleRect;
4869
- const [viewBoxWidth, viewBoxHeight] = this.viewbox.size;
5086
+ const [viewBoxWidth, viewBoxHeight] = viewbox.size;
4870
5087
  const [visibleRectWidth, visibleRectHeight] = (visibleRect == null ? void 0 : visibleRect.size.clone().multiplyScalar(dpr)) ?? this.renderer.size;
4871
5088
  const scaleFactor = Math.min(visibleRectWidth / viewBoxWidth, visibleRectHeight / viewBoxHeight);
4872
- const [centerX, centerY] = this.viewbox.center;
5089
+ const [centerX, centerY] = viewbox.center;
4873
5090
  this.translationMatrix.makeTranslation(-centerX, -centerY, 0);
4874
5091
  this.scaleMatrix.makeScale(scaleFactor, scaleFactor, 1);
4875
5092
  if (visibleRect) {
4876
5093
  const visibleRectCenter = visibleRect.center.clone().multiplyScalar(dpr);
4877
- const canvasCenter = new Vector2(...this.renderer.size).multiplyScalar(0.5);
5094
+ const canvasCenter = { x: this.renderer.size[0] / 2, y: this.renderer.size[1] / 2 };
4878
5095
  const offset = visibleRectCenter.sub(canvasCenter);
4879
5096
  this.visibleRectOffsetMatrix.makeTranslation(offset.x, offset.y, 0);
4880
5097
  }
4881
- this.composeMatrices();
4882
- }
4883
- composeMatrices() {
4884
5098
  this.worldMatrix.copy(this.translationMatrix).premultiply(this.scaleMatrix);
4885
5099
  if (this.renderer.visibleRect) this.worldMatrix.premultiply(this.visibleRectOffsetMatrix);
4886
5100
  this.scene.matrix.copy(this.worldMatrix);
@@ -4894,19 +5108,18 @@ class ViewportSystem {
4894
5108
  * @param eventSystem {@link EventSystem} instance
4895
5109
  */
4896
5110
  constructor(renderer, eventSystem) {
5111
+ __publicField(this, "pickingSystem");
5112
+ __publicField(this, "externalSystem");
4897
5113
  __publicField(this, "sceneSystem");
4898
5114
  __publicField(this, "cameraSystem");
4899
- __publicField(this, "raycaster", new Raycaster());
4900
- __publicField(this, "intersectionPoint", new Vector3());
4901
- __publicField(this, "viewboxPlane", new Plane(new Vector3(0, 0, 1), 0));
4902
- __publicField(this, "pxToSvgScaleThreshold", 1e-3);
5115
+ __publicField(this, "pxToSvgScaleThreshold", 1e-4);
4903
5116
  __publicField(this, "prevPxToSvgScale");
4904
- __publicField(this, "externalStaticTransformMatrix", new Matrix4());
4905
5117
  this.renderer = renderer;
4906
5118
  this.eventSystem = eventSystem;
5119
+ this.pickingSystem = new PickingSystem();
4907
5120
  this.sceneSystem = new SceneSystem(renderer);
4908
5121
  this.cameraSystem = new CameraSystem(renderer);
4909
- this.raycaster.layers.set(INTERACTIVE_LAYER);
5122
+ this.externalSystem = new ExternalSystem(this.pickingSystem);
4910
5123
  }
4911
5124
  /** {@link Scene} instance */
4912
5125
  get scene() {
@@ -4914,10 +5127,10 @@ class ViewportSystem {
4914
5127
  }
4915
5128
  /** Current {@link Camera} instance */
4916
5129
  get camera() {
4917
- return this.cameraSystem.currentCamera;
5130
+ return this.renderer.isExternalMode ? this.externalSystem.camera : this.cameraSystem.camera;
4918
5131
  }
4919
5132
  /** {@link CameraController} instance */
4920
- get cameraController() {
5133
+ get controller() {
4921
5134
  return this.cameraSystem.controller;
4922
5135
  }
4923
5136
  /** Current camera zoom factor. */
@@ -4930,14 +5143,22 @@ class ViewportSystem {
4930
5143
  }
4931
5144
  /** Pixel to SVG scale factor */
4932
5145
  get pxToSvgScale() {
4933
- return 1 / (this.scaleFactor * this.zoomFactor);
5146
+ return this.renderer.isExternalMode ? this.externalSystem.pxToSvgScale(this.renderer.size) ?? this.prevPxToSvgScale ?? 1 : 1 / (this.scaleFactor * this.zoomFactor);
5147
+ }
5148
+ /**
5149
+ * Get bearing angle between current camera orientation and true north (in radians).
5150
+ * Angle is in range [0, 2π), going clockwise from north.
5151
+ */
5152
+ get bearing() {
5153
+ const tau = Math.PI * 2;
5154
+ return MathUtils.euclideanModulo(-this.controller.azimuthAngle, tau);
4934
5155
  }
4935
5156
  /**
4936
5157
  * Initializes the viewport and zoom bounds with the given scene definition.
4937
5158
  * @param sceneDef {@link SceneDef} scene definition
4938
5159
  */
4939
5160
  initViewport(sceneDef) {
4940
- this.sceneSystem.initScene(sceneDef.viewbox);
5161
+ if (!this.renderer.isExternalMode) this.sceneSystem.initScene(sceneDef.viewbox);
4941
5162
  this.cameraSystem.initCamera([0.1, sceneDef.viewbox.size.width > 1e5 ? 100 : 35]);
4942
5163
  }
4943
5164
  /** Updates the viewport when the renderer size changes. */
@@ -4957,71 +5178,99 @@ class ViewportSystem {
4957
5178
  }
4958
5179
  /**
4959
5180
  * Gets the objects intersected by the raycaster.
4960
- * @param normalizedCoords raycast point in NDC (normalized device coordinates
5181
+ * @param ndcCoords raycast point in NDC (normalized device coordinates)
4961
5182
  * @returns Array of {@link Intersection} instances
4962
5183
  */
4963
- getIntersectedObjects(normalizedCoords) {
4964
- const { scene, camera } = this;
4965
- this.raycaster.setFromCamera(normalizedCoords, camera);
4966
- const intersections = this.raycaster.intersectObject(scene, true).filter((i) => isVisible(i.object));
4967
- return intersections;
5184
+ getIntersectedObjects(ndcCoords) {
5185
+ return this.pickingSystem.getIntersectedObjects(ndcCoords, this.scene, this.camera);
4968
5186
  }
4969
5187
  /**
4970
- * Converts a point from SVG coordinates to world coordinates.
4971
- * @param svgCoords Point in SVG coordinates
5188
+ * Converts a point from model coordinates to world coordinates.
5189
+ * @param modelCoords Point in model coordinates
5190
+ * @param out Optional output vector
4972
5191
  * @returns Point in world coordinates
4973
5192
  */
4974
- svgToWorld(svgCoords) {
4975
- const svg3D = new Vector3(...svgCoords, 0);
4976
- svg3D.applyMatrix4(this.sceneSystem.worldMatrix);
4977
- return new Vector2(svg3D.x, svg3D.y);
5193
+ modelToWorld(modelCoords, out = new Vector3()) {
5194
+ return this.sceneSystem.modelToWorld(modelCoords, out);
4978
5195
  }
4979
5196
  /**
4980
- * Converts a point from world coordinates to SVG coordinates. Z axis is ignored.
5197
+ * Converts a point from world coordinates to model coordinates. Z axis is ignored.
4981
5198
  * @param worldCoords Point in world coordinates
4982
- * @returns Point in SVG coordinates
5199
+ * @param out Optional output vector
5200
+ * @returns Point in model coordinates
4983
5201
  */
4984
- worldToSvg(worldCoords) {
4985
- const svgCoords = worldCoords.clone().applyMatrix4(this.sceneSystem.inverseWorldMatrix);
4986
- return new Vector2(svgCoords.x, svgCoords.y);
5202
+ worldToModel(worldCoords, out = new Vector2()) {
5203
+ return this.sceneSystem.worldToModel(worldCoords, out);
4987
5204
  }
4988
5205
  /**
4989
- * Converts a point from screen coordinates to the given coordinate space.
4990
- * @param space Space to convert to (either "svg" or "world")
4991
- * @param normalizedCoords Point in NDC (normalized device coordinates)
4992
- * @returns Point in the given space
5206
+ * Converts a point from screen coordinates to world space.
5207
+ * @param ndcCoords Point in NDC (normalized device coordinates)
5208
+ * @param out Optional output vector
5209
+ * @returns Point in world space
4993
5210
  */
4994
- screenTo(space, normalizedCoords) {
4995
- this.raycaster.setFromCamera(normalizedCoords, this.camera);
4996
- this.raycaster.ray.intersectPlane(this.viewboxPlane, this.intersectionPoint);
4997
- if (space === "svg") this.intersectionPoint.applyMatrix4(this.sceneSystem.inverseWorldMatrix);
4998
- return { x: this.intersectionPoint.x, y: this.intersectionPoint.y };
5211
+ ndcToWorld(ndcCoords, out = new Vector3()) {
5212
+ return this.pickingSystem.intersectPlane(ndcCoords, this.camera, out);
4999
5213
  }
5000
5214
  /**
5001
- * Calculates the camera distance from the scene's plane for a given zoom factor.
5002
- * @param zoomFactor Zoom factor
5003
- * @returns Corresponding camera distance on the Z axis
5215
+ * Convert canvas coordinates (relative to the canvas's top left corner)
5216
+ * to NDC (normalized device coordinates).
5217
+ * @param point object defining the coordinates relative to the canvas's top left corner
5218
+ * @param out Optional output vector
5219
+ * @returns Point in NDC space
5004
5220
  */
5005
- zoomFactorToDistance(zoomFactor) {
5006
- return this.cameraSystem.zoomFactorToDistance(zoomFactor);
5221
+ canvasToNDC(point, out = new Vector2()) {
5222
+ const dpr = this.renderer.context.getPixelRatio();
5223
+ const [width, height] = [this.renderer.size[0] / dpr, this.renderer.size[1] / dpr];
5224
+ const uv = [point.x / width, point.y / height];
5225
+ const ndc = [uv[0] * 2 - 1, -uv[1] * 2 + 1];
5226
+ return out.set(MathUtils.clamp(ndc[0], -1, 1), MathUtils.clamp(ndc[1], -1, 1));
5007
5227
  }
5008
5228
  /**
5009
- * Sets the external transform matrix.
5229
+ * Convert canvas coordinates (CSS pixels, relative to canvas top-left) to SVG coordinates.
5230
+ * @param point point in canvas space (CSS pixels)
5231
+ * @returns point in SVG coordinates or undefined if point is outside the SVG plane
5232
+ */
5233
+ canvasToSvg(point) {
5234
+ const vec2 = new Vector2();
5235
+ const vec3 = new Vector3();
5236
+ const ndcPoint = this.canvasToNDC(point, vec2);
5237
+ const worldPoint = this.ndcToWorld(ndcPoint, vec3);
5238
+ if (!worldPoint) return;
5239
+ return this.worldToModel(worldPoint, vec2);
5240
+ }
5241
+ /**
5242
+ * Set static part of an svg -> px transform matrix
5010
5243
  * @param staticTransformMatrix static transform matrix to apply to the scene
5011
5244
  */
5012
- setExternalTransform(staticTransformMatrix) {
5013
- this.cameraSystem.externalCamera = new Camera();
5014
- this.externalStaticTransformMatrix.fromArray(staticTransformMatrix);
5245
+ setStaticTransform(staticTransformMatrix) {
5246
+ this.externalSystem.setStaticTransform(staticTransformMatrix);
5015
5247
  }
5016
5248
  /**
5017
- * Updates the external camera.
5018
- * @param dynamicTransformMatrix dynamic transform matrix to apply to the scene
5249
+ * Set dynamic part of an svg -> px transform matrix. Should be called every frame.
5250
+ * @param dynamicTransformMatrix dynamic transform matrix (changes every frame)
5019
5251
  */
5020
- updateExternalCamera(dynamicTransformMatrix) {
5021
- this.scene.matrix.fromArray(dynamicTransformMatrix).multiply(this.externalStaticTransformMatrix);
5022
- this.scene.matrixWorldNeedsUpdate = true;
5252
+ setDynamicTransform(dynamicTransformMatrix) {
5253
+ this.externalSystem.setDynamicTransform(dynamicTransformMatrix);
5254
+ }
5255
+ /**
5256
+ * Calculates the camera distance from the scene's plane for a given zoom factor.
5257
+ * @param zoomFactor Zoom factor
5258
+ * @returns Corresponding camera distance on the Z axis
5259
+ */
5260
+ zoomFactorToDistance(zoomFactor) {
5261
+ return this.cameraSystem.zoomFactorToDistance(zoomFactor);
5023
5262
  }
5024
5263
  }
5264
+ function asViewportAPI(viewportSystem) {
5265
+ return {
5266
+ canvasToSvg: viewportSystem.canvasToSvg.bind(viewportSystem),
5267
+ setStaticTransform: viewportSystem.setStaticTransform.bind(viewportSystem),
5268
+ setDynamicTransform: viewportSystem.setDynamicTransform.bind(viewportSystem)
5269
+ };
5270
+ }
5271
+ function eventToCanvas(event) {
5272
+ return { x: event.offsetX, y: event.offsetY };
5273
+ }
5025
5274
  class ControlsSystem {
5026
5275
  /**
5027
5276
  * @param renderer {@link Renderer} instance
@@ -5033,7 +5282,7 @@ class ControlsSystem {
5033
5282
  this.renderer = renderer;
5034
5283
  this.viewportSystem = viewportSystem;
5035
5284
  this.interactionsSystem = interactionsSystem;
5036
- this.controller = viewportSystem.cameraController;
5285
+ this.controller = viewportSystem.controller;
5037
5286
  }
5038
5287
  /** Gesture handlers for camera controls. */
5039
5288
  get handlers() {
@@ -5062,7 +5311,9 @@ class ControlsSystem {
5062
5311
  const dpr = this.renderer.context.getPixelRatio();
5063
5312
  const visibleRect = this.renderer.visibleRect;
5064
5313
  const bearingAngle = -this.controller.azimuthAngle;
5065
- const worldRect = new Rect(this.viewportSystem.svgToWorld(rect.min), this.viewportSystem.svgToWorld(rect.max));
5314
+ const worldMin = this.viewportSystem.modelToWorld(rect.min);
5315
+ const worldMax = this.viewportSystem.modelToWorld(rect.max);
5316
+ const worldRect = new Rect(worldMin, worldMax);
5066
5317
  const worldPolygon = Polygon.fromRect(worldRect).rotate(bearingAngle, worldRect.center);
5067
5318
  const xValues = worldPolygon.vertices.map((p) => p.x);
5068
5319
  const yValues = worldPolygon.vertices.map((p) => p.y);
@@ -5070,7 +5321,7 @@ class ControlsSystem {
5070
5321
  [Math.min(...xValues), Math.min(...yValues)],
5071
5322
  [Math.max(...xValues), Math.max(...yValues)]
5072
5323
  );
5073
- const targetRect = visibleRect ? new Rect(visibleRect.min.clone().multiplyScalar(dpr), visibleRect.max.clone().multiplyScalar(dpr)) : new Rect([0, 0], this.renderer.size);
5324
+ const targetRect = visibleRect ? new Rect(visibleRect.min.clone().multiplyScalar(dpr), visibleRect.max.clone().multiplyScalar(dpr)) : new Rect([0, 0], [...this.renderer.size]);
5074
5325
  if (paddingPercent) targetRect.addPadding(targetRect.size.x * paddingPercent, targetRect.size.y * paddingPercent);
5075
5326
  const zoomByWidth = targetRect.size.x / sourceRect.size.x;
5076
5327
  const zoomByHeight = targetRect.size.y / sourceRect.size.y;
@@ -5095,8 +5346,8 @@ class ControlsSystem {
5095
5346
  * @returns Promise that resolves when the pan animation completes
5096
5347
  */
5097
5348
  panBy(x, y, immediate) {
5098
- const svgOrigin = this.viewportSystem.svgToWorld(new Vector2(0, 0));
5099
- const svgOffset = this.viewportSystem.svgToWorld(new Vector2(x, y));
5349
+ const svgOrigin = this.viewportSystem.modelToWorld({ x: 0, y: 0 });
5350
+ const svgOffset = this.viewportSystem.modelToWorld({ x, y });
5100
5351
  const worldOffset = new Vector3(svgOffset.x - svgOrigin.x, svgOffset.y - svgOrigin.y, 0);
5101
5352
  const currentTarget = this.controller.getTarget(new Vector3());
5102
5353
  const newTarget = currentTarget.add(worldOffset);
@@ -5110,7 +5361,7 @@ class ControlsSystem {
5110
5361
  * @returns Promise that resolves when the pan animation completes
5111
5362
  */
5112
5363
  panTo(x, y, immediate) {
5113
- const worldCoords = this.viewportSystem.svgToWorld(new Vector2(x, y));
5364
+ const worldCoords = this.viewportSystem.modelToWorld({ x, y });
5114
5365
  return this.controller.moveTo(worldCoords.x, worldCoords.y, 0, !immediate);
5115
5366
  }
5116
5367
  /**
@@ -5208,8 +5459,8 @@ class ControlsSystem {
5208
5459
  this.controller.setBoundary(void 0);
5209
5460
  return;
5210
5461
  }
5211
- const worldMin = this.viewportSystem.svgToWorld(rect.min);
5212
- const worldMax = this.viewportSystem.svgToWorld(rect.max);
5462
+ const worldMin = this.viewportSystem.modelToWorld(rect.min);
5463
+ const worldMax = this.viewportSystem.modelToWorld(rect.max);
5213
5464
  const boundary = new Box3(new Vector3(worldMin.x, worldMin.y, 0), new Vector3(worldMax.x, worldMax.y, 0));
5214
5465
  this.controller.setBoundary(boundary);
5215
5466
  }
@@ -5219,9 +5470,9 @@ class ControlsSystem {
5219
5470
  */
5220
5471
  getCameraState() {
5221
5472
  const target = this.controller.getTarget(new Vector3());
5222
- const center = this.viewportSystem.worldToSvg(target);
5473
+ const center = this.viewportSystem.worldToModel(target);
5223
5474
  const zoom = this.viewportSystem.zoomFactor;
5224
- const roll = this.interactionsSystem.bearing * RAD2DEG;
5475
+ const roll = this.viewportSystem.bearing * RAD2DEG;
5225
5476
  const pitch = this.controller.polarAngle * RAD2DEG;
5226
5477
  const ptScale = this.viewportSystem.pxToSvgScale;
5227
5478
  return { center, zoom, roll, pitch, ptScale };
@@ -5318,21 +5569,6 @@ function asEventAPI(system) {
5318
5569
  clear: system.clear.bind(system)
5319
5570
  };
5320
5571
  }
5321
- function clientToCanvas(event, domElement, target) {
5322
- target = target ?? new Vector2();
5323
- const { left, top } = domElement.getBoundingClientRect();
5324
- const clientX = "clientX" in event ? event.clientX : event.x;
5325
- const clientY = "clientY" in event ? event.clientY : event.y;
5326
- target.set(clientX - left, clientY - top);
5327
- return target;
5328
- }
5329
- function canvasToNDC(coordinates, canvas, target) {
5330
- target = target ?? new Vector2();
5331
- const { width, height } = canvas.getBoundingClientRect();
5332
- const uv = [coordinates.x / width, coordinates.y / height];
5333
- target.set(uv[0] * 2 - 1, -uv[1] * 2 + 1);
5334
- return target;
5335
- }
5336
5572
  class Handler {
5337
5573
  /**
5338
5574
  * @param viewportSystem The viewport system instance
@@ -5347,7 +5583,7 @@ class Handler {
5347
5583
  this.viewportSystem = viewportSystem;
5348
5584
  this.domElement = domElement;
5349
5585
  this.eventManager = eventManager;
5350
- this.controller = viewportSystem.cameraController;
5586
+ this.controller = viewportSystem.controller;
5351
5587
  }
5352
5588
  /**
5353
5589
  * Per-frame update for this handler.
@@ -5558,13 +5794,15 @@ class PitchHandler extends Handler {
5558
5794
  __publicField(this, "isValid");
5559
5795
  __publicField(this, "firstMove");
5560
5796
  __publicField(this, "lastPoints");
5797
+ __publicField(this, "p0", new Vector2());
5798
+ __publicField(this, "p1", new Vector2());
5561
5799
  __publicField(this, "prevTwoFingerAction");
5562
5800
  __publicField(this, "onPitchStart", (e) => {
5563
5801
  const pointers = e.pointers.sort((a, b) => a.pointerId - b.pointerId);
5564
- const p0 = clientToCanvas(pointers[0], this.domElement);
5565
- const p1 = clientToCanvas(pointers[1], this.domElement);
5566
- this.lastPoints = [p0, p1];
5567
- if (this.isVertical(p0.clone().sub(p1))) {
5802
+ this.p0.copy(eventToCanvas(pointers[0]));
5803
+ this.p1.copy(eventToCanvas(pointers[1]));
5804
+ this.lastPoints = [new Vector2().copy(this.p0), new Vector2().copy(this.p1)];
5805
+ if (this.isVertical(this.p0.sub(this.p1))) {
5568
5806
  this.isValid = false;
5569
5807
  }
5570
5808
  });
@@ -5579,18 +5817,19 @@ class PitchHandler extends Handler {
5579
5817
  const lastPoints = this.lastPoints;
5580
5818
  if (!lastPoints) return;
5581
5819
  const pointers = e.pointers.sort((a, b) => a.pointerId - b.pointerId);
5582
- const p0 = clientToCanvas(pointers[0], this.domElement);
5583
- const p1 = clientToCanvas(pointers[1], this.domElement);
5584
- const vectorA = p0.clone().sub(lastPoints[0]);
5585
- const vectorB = p1.clone().sub(lastPoints[1]);
5586
- this.isValid = this.gestureBeginsVertically(vectorA, vectorB, e.timeStamp);
5820
+ this.p0.copy(eventToCanvas(pointers[0]));
5821
+ this.p1.copy(eventToCanvas(pointers[1]));
5822
+ this.p0.sub(lastPoints[0]);
5823
+ this.p1.sub(lastPoints[1]);
5824
+ this.isValid = this.gestureBeginsVertically(this.p0, this.p1, e.timeStamp);
5587
5825
  if (!this.isValid) return;
5588
5826
  if (this.prevTwoFingerAction === void 0) {
5589
5827
  this.prevTwoFingerAction = this.controller.touches.two;
5590
5828
  this.controller.touches.two = CameraController.ACTION.NONE;
5591
5829
  }
5592
- this.lastPoints = [p0, p1];
5593
- const yDeltaAverage = (vectorA.y + vectorB.y) / 2;
5830
+ lastPoints[0].add(this.p0);
5831
+ lastPoints[1].add(this.p1);
5832
+ const yDeltaAverage = (this.p0.y + this.p1.y) / 2;
5594
5833
  const degreesPerPixelMoved = -0.5;
5595
5834
  const deltaAngle = yDeltaAverage * degreesPerPixelMoved * DEG2RAD$1;
5596
5835
  void this.controller.rotatePolarTo(this.controller.polarAngle + deltaAngle, false);
@@ -5635,12 +5874,10 @@ class PitchHandler extends Handler {
5635
5874
  updatePolarAngles() {
5636
5875
  this.controller.minPolarAngle = this.minPitch * DEG2RAD$1;
5637
5876
  this.controller.maxPolarAngle = this.maxPitch * DEG2RAD$1;
5638
- if (this.controller.polarAngle < this.controller.minPolarAngle) {
5877
+ if (this.controller.polarAngle < this.controller.minPolarAngle)
5639
5878
  void this.controller.rotatePolarTo(this.controller.minPolarAngle, false);
5640
- }
5641
- if (this.controller.polarAngle > this.controller.maxPolarAngle) {
5879
+ if (this.controller.polarAngle > this.controller.maxPolarAngle)
5642
5880
  void this.controller.rotatePolarTo(this.controller.maxPolarAngle, false);
5643
- }
5644
5881
  }
5645
5882
  gestureBeginsVertically(vectorA, vectorB, timeStamp) {
5646
5883
  if (this.isValid !== void 0) return this.isValid;
@@ -5672,35 +5909,36 @@ class RollHandler extends Handler {
5672
5909
  __publicField(this, "rotationThreshold", 25);
5673
5910
  // Threshold tracking (Mapbox-style)
5674
5911
  __publicField(this, "startVector");
5675
- __publicField(this, "vector");
5912
+ __publicField(this, "vector", new Vector2());
5913
+ __publicField(this, "p0", new Vector2());
5914
+ __publicField(this, "p1", new Vector2());
5676
5915
  __publicField(this, "minDiameter", 0);
5677
5916
  __publicField(this, "prevAngle", 0);
5678
5917
  // Camera and pivot vectors
5918
+ __publicField(this, "pivotNDC", new Vector2());
5679
5919
  __publicField(this, "pivotWorld", new Vector3());
5680
5920
  __publicField(this, "targetWorld", new Vector3());
5681
5921
  __publicField(this, "cameraPosition", new Vector3());
5682
5922
  __publicField(this, "cameraForward", new Vector3());
5683
5923
  __publicField(this, "rotationMatrix", new Matrix4());
5684
5924
  __publicField(this, "onRotateStart", (e) => {
5685
- console.log("onRotateStart");
5686
5925
  const pointers = e.pointers;
5687
- const p0 = clientToCanvas(pointers[0], this.domElement);
5688
- const p1 = clientToCanvas(pointers[1], this.domElement);
5689
- this.startVector = p0.sub(p1);
5926
+ this.p0.copy(eventToCanvas(pointers[0]));
5927
+ this.p1.copy(eventToCanvas(pointers[1]));
5928
+ this.startVector = new Vector2().copy(this.p0).sub(this.p1);
5690
5929
  this.minDiameter = this.startVector.length();
5691
5930
  });
5692
5931
  __publicField(this, "onRotateEnd", () => {
5693
5932
  this.isRolling = false;
5694
5933
  this.startVector = void 0;
5695
- this.vector = void 0;
5696
5934
  this.minDiameter = 0;
5697
5935
  });
5698
5936
  __publicField(this, "onRotate", (e) => {
5699
5937
  const pointers = e.pointers;
5700
5938
  if (!this.isRolling) {
5701
- const p0 = clientToCanvas(pointers[0], this.domElement);
5702
- const p1 = clientToCanvas(pointers[1], this.domElement);
5703
- this.vector = p0.sub(p1);
5939
+ this.p0.copy(eventToCanvas(pointers[0]));
5940
+ this.p1.copy(eventToCanvas(pointers[1]));
5941
+ this.vector.copy(this.p0).sub(this.p1);
5704
5942
  if (this.isBelowThreshold(this.vector)) return;
5705
5943
  this.isRolling = true;
5706
5944
  this.prevAngle = e.rotation;
@@ -5708,7 +5946,7 @@ class RollHandler extends Handler {
5708
5946
  const deltaAngle = (e.rotation - this.prevAngle) * -DEG2RAD$1;
5709
5947
  this.prevAngle = e.rotation;
5710
5948
  if (Math.abs(deltaAngle) < 1e-3) return;
5711
- this.setPivot(e);
5949
+ if (!this.setPivot(e)) return;
5712
5950
  this.rotationMatrix.makeRotationZ(deltaAngle);
5713
5951
  this.cameraPosition.sub(this.pivotWorld).applyMatrix4(this.rotationMatrix).add(this.pivotWorld);
5714
5952
  this.cameraForward.applyMatrix4(this.rotationMatrix);
@@ -5756,13 +5994,12 @@ class RollHandler extends Handler {
5756
5994
  this.eventManager.off("rotateend", this.onRotateEnd);
5757
5995
  }
5758
5996
  setPivot(e) {
5759
- const pivotScreen = e.center;
5760
- const pivotNDC = canvasToNDC(pivotScreen, this.domElement);
5761
- const pivotWorld2D = this.viewportSystem.screenTo("world", pivotNDC);
5762
- this.pivotWorld.set(pivotWorld2D.x, pivotWorld2D.y, 0);
5997
+ this.viewportSystem.canvasToNDC(e.offsetCenter, this.pivotNDC);
5998
+ if (!this.viewportSystem.ndcToWorld(this.pivotNDC, this.pivotWorld)) return false;
5763
5999
  this.controller.getPosition(this.cameraPosition);
5764
6000
  this.controller.getTarget(this.targetWorld);
5765
6001
  this.cameraForward.copy(this.targetWorld).sub(this.cameraPosition);
6002
+ return true;
5766
6003
  }
5767
6004
  /**
5768
6005
  * Check if rotation is below threshold (Mapbox-style).
@@ -5782,7 +6019,6 @@ class RollHandler extends Handler {
5782
6019
  const startVector = this.startVector;
5783
6020
  if (!startVector) return false;
5784
6021
  const bearingDeltaSinceStart = this.getBearingDelta(vector, startVector);
5785
- console.log("bearingDeltaSinceStart", vector, startVector);
5786
6022
  return Math.abs(bearingDeltaSinceStart) < threshold;
5787
6023
  }
5788
6024
  /**
@@ -5794,16 +6030,6 @@ class RollHandler extends Handler {
5794
6030
  getBearingDelta(a, b) {
5795
6031
  return a.angleTo(b) * RAD2DEG;
5796
6032
  }
5797
- /**
5798
- * Normalize angle to be between -π and π
5799
- * @param angle Angle in radians
5800
- * @returns Normalized angle in radians
5801
- */
5802
- normalizeAngle(angle) {
5803
- while (angle > Math.PI) angle -= 2 * Math.PI;
5804
- while (angle < -Math.PI) angle += 2 * Math.PI;
5805
- return angle;
5806
- }
5807
6033
  }
5808
6034
  class ZoomHandler extends Handler {
5809
6035
  reset(enableTransition = true) {
@@ -5838,12 +6064,16 @@ class InteractionsSystem {
5838
6064
  __publicField(this, "handlers");
5839
6065
  __publicField(this, "handlerArray");
5840
6066
  __publicField(this, "canvas");
5841
- __publicField(this, "mousePointer", new Vector2());
5842
6067
  __publicField(this, "eventManager");
6068
+ __publicField(this, "mousePointerNDC", new Vector2());
6069
+ __publicField(this, "mousePointerWorld", new Vector3());
6070
+ __publicField(this, "mousePointerModel", new Vector2());
6071
+ __publicField(this, "canvasListeners");
5843
6072
  __publicField(this, "dragStart");
5844
6073
  __publicField(this, "dragThreshold", 15);
5845
6074
  __publicField(this, "isDragging", false);
5846
6075
  __publicField(this, "prevBearing", 0);
6076
+ this.renderer = renderer;
5847
6077
  this.events = events;
5848
6078
  this.viewportSystem = viewportSystem;
5849
6079
  this.layerSystem = layerSystem;
@@ -5851,8 +6081,6 @@ class InteractionsSystem {
5851
6081
  this.eventManager = new EventManager(this.canvas, {
5852
6082
  recognizers: [Rotate, [Pan, { event: "pitch", pointers: 2 }, "rotate"]]
5853
6083
  });
5854
- this.configureCameraControls();
5855
- this.attachCanvasListeners();
5856
6084
  const handlers = {
5857
6085
  pan: new PanHandler(viewportSystem, this.canvas, this.eventManager),
5858
6086
  zoom: new ZoomHandler(viewportSystem, this.canvas, this.eventManager),
@@ -5861,17 +6089,32 @@ class InteractionsSystem {
5861
6089
  };
5862
6090
  this.handlers = handlers;
5863
6091
  this.handlerArray = Object.values(handlers);
5864
- this.handlers.pan.enable();
5865
- this.handlers.zoom.enable();
5866
6092
  }
5867
6093
  /**
5868
- * Get bearing angle between current camera orientation and true north (in radians).
5869
- * Angle is in range [0, 2π), going clockwise from north.
6094
+ * Initializes the interactions system and attaches event listeners.
6095
+ * Should be called once as part of renderer initialization.
5870
6096
  */
5871
- // TODO: Move somewhere else
5872
- get bearing() {
5873
- const tau = Math.PI * 2;
5874
- return MathUtils.euclideanModulo(-this.viewportSystem.cameraController.azimuthAngle, tau);
6097
+ init() {
6098
+ this.attachCanvasListeners();
6099
+ if (!this.renderer.isExternalMode) {
6100
+ const controller = this.viewportSystem.controller;
6101
+ controller.connect(this.canvas);
6102
+ controller.addEventListener("transitionstart", () => {
6103
+ this.events.emit("navigation:change");
6104
+ });
6105
+ this.handlers.pan.enable();
6106
+ this.handlers.zoom.enable();
6107
+ }
6108
+ }
6109
+ /**
6110
+ * Disposes the interactions system.
6111
+ * WARNING: This method is final and cannot be undone. To re-enable interactions, create a new renderer instance.
6112
+ */
6113
+ dispose() {
6114
+ for (const handler of this.handlerArray) handler.disable();
6115
+ this.viewportSystem.controller.disconnect();
6116
+ this.detachCanvasListeners();
6117
+ this.eventManager.destroy();
5875
6118
  }
5876
6119
  /**
5877
6120
  * Update camera position and directions.
@@ -5880,77 +6123,67 @@ class InteractionsSystem {
5880
6123
  * @returns true if re-rendering is needed
5881
6124
  */
5882
6125
  updateControls(delta) {
5883
- let needsUpdate = this.viewportSystem.cameraController.update(delta);
6126
+ let needsUpdate = this.viewportSystem.controller.update(delta);
5884
6127
  for (const handler of this.handlerArray) {
5885
6128
  if (handler.isEnabled()) {
5886
6129
  needsUpdate = handler.update(delta) || needsUpdate;
5887
6130
  }
5888
6131
  }
5889
- if (this.bearing !== this.prevBearing) {
5890
- this.prevBearing = this.bearing;
5891
- this.events.emit("navigation:roll", this.bearing);
6132
+ if (this.viewportSystem.bearing !== this.prevBearing) {
6133
+ this.prevBearing = this.viewportSystem.bearing;
6134
+ this.events.emit("navigation:roll", this.viewportSystem.bearing);
5892
6135
  }
5893
6136
  return needsUpdate;
5894
6137
  }
5895
- /** Disconnect the interactions system. */
5896
- disconnect() {
5897
- this.viewportSystem.cameraController.disconnect();
5898
- for (const handler of this.handlerArray) {
5899
- handler.disable();
5900
- }
5901
- }
5902
- configureCameraControls() {
5903
- const controller = this.viewportSystem.cameraController;
5904
- controller.draggingSmoothTime = 0;
5905
- controller.dollyToCursor = true;
5906
- controller.mouseButtons = {
5907
- left: CameraController.ACTION.NONE,
5908
- middle: CameraController.ACTION.NONE,
5909
- right: CameraController.ACTION.NONE,
5910
- wheel: CameraController.ACTION.NONE
5911
- };
5912
- controller.touches = {
5913
- one: CameraController.ACTION.NONE,
5914
- two: CameraController.ACTION.NONE,
5915
- three: CameraController.ACTION.NONE
5916
- };
5917
- controller.connect(this.canvas);
5918
- controller.addEventListener("transitionstart", () => {
5919
- this.events.emit("navigation:change");
5920
- });
5921
- }
5922
6138
  attachCanvasListeners() {
5923
- this.canvas.addEventListener("pointerdown", (event) => {
6139
+ if (this.canvasListeners) return;
6140
+ const pointerdown = (event) => {
5924
6141
  this.isDragging = false;
5925
6142
  this.dragStart = { x: event.offsetX, y: event.offsetY };
5926
- });
5927
- this.canvas.addEventListener("pointerup", (event) => {
6143
+ };
6144
+ const pointerup = (event) => {
5928
6145
  if (!this.dragStart) return;
5929
6146
  const dX = event.offsetX - this.dragStart.x;
5930
6147
  const dY = event.offsetY - this.dragStart.y;
5931
6148
  this.dragStart = void 0;
5932
6149
  if (dX * dX + dY * dY > this.dragThreshold) this.isDragging = true;
5933
- });
6150
+ };
5934
6151
  const mouseEventsMap = {
5935
6152
  mousemove: "pointer:move",
5936
6153
  mouseout: "pointer:out",
5937
6154
  click: "pointer:click"
5938
6155
  };
5939
6156
  const mouseEventKeys = Object.keys(mouseEventsMap);
5940
- mouseEventKeys.forEach((type) => {
5941
- this.canvas.addEventListener(type, (event) => {
5942
- const eventType = mouseEventsMap[type];
5943
- const isDragging = type === "click" && this.isDragging;
5944
- const hasListeners = this.events.hasListeners(eventType);
5945
- if (isDragging || !hasListeners) return;
5946
- clientToCanvas(event, this.canvas, this.mousePointer);
5947
- canvasToNDC(this.mousePointer, this.canvas, this.mousePointer);
5948
- const intersections = this.viewportSystem.getIntersectedObjects(this.mousePointer);
5949
- const point = this.viewportSystem.screenTo("svg", this.mousePointer);
5950
- const defs = this.layerSystem.getIntersectedDefs(intersections);
5951
- this.events.emit(eventType, { event, point, defs });
5952
- });
5953
- });
6157
+ const sharedMouseHandler = (type, event) => {
6158
+ const eventType = mouseEventsMap[type];
6159
+ const isDragging = type === "click" && this.isDragging;
6160
+ const hasListeners = this.events.hasListeners(eventType);
6161
+ if (isDragging || !hasListeners) return;
6162
+ const mousePointer = eventToCanvas(event);
6163
+ this.viewportSystem.canvasToNDC(mousePointer, this.mousePointerNDC);
6164
+ if (!this.viewportSystem.ndcToWorld(this.mousePointerNDC, this.mousePointerWorld)) return;
6165
+ const intersections = this.viewportSystem.getIntersectedObjects(this.mousePointerNDC);
6166
+ const point = this.viewportSystem.worldToModel(this.mousePointerWorld, this.mousePointerModel);
6167
+ const defs = this.layerSystem.getIntersectedDefs(intersections);
6168
+ this.events.emit(eventType, { event, point: { x: point.x, y: point.y }, defs });
6169
+ };
6170
+ const mousemove = (event) => sharedMouseHandler("mousemove", event);
6171
+ const mouseout = (event) => sharedMouseHandler("mouseout", event);
6172
+ const click = (event) => sharedMouseHandler("click", event);
6173
+ const canvasListeners = { pointerdown, pointerup, mousemove, mouseout, click };
6174
+ this.canvas.addEventListener("pointerdown", pointerdown);
6175
+ this.canvas.addEventListener("pointerup", pointerup);
6176
+ mouseEventKeys.forEach((type) => this.canvas.addEventListener(type, canvasListeners[type]));
6177
+ this.canvasListeners = canvasListeners;
6178
+ }
6179
+ detachCanvasListeners() {
6180
+ if (!this.canvasListeners) return;
6181
+ this.canvas.removeEventListener("pointerdown", this.canvasListeners.pointerdown);
6182
+ this.canvas.removeEventListener("pointerup", this.canvasListeners.pointerup);
6183
+ this.canvas.removeEventListener("mousemove", this.canvasListeners.mousemove);
6184
+ this.canvas.removeEventListener("mouseout", this.canvasListeners.mouseout);
6185
+ this.canvas.removeEventListener("click", this.canvasListeners.click);
6186
+ this.canvasListeners = void 0;
5954
6187
  }
5955
6188
  }
5956
6189
  class Renderer {
@@ -5960,6 +6193,7 @@ class Renderer {
5960
6193
  constructor(opts) {
5961
6194
  /** Whether to log debug information */
5962
6195
  __publicField(this, "debugLog");
6196
+ //FIXME: Add https://www.npmjs.com/package/debug
5963
6197
  /** {@link HTMLCanvasElement} that this renderer is rendering to */
5964
6198
  __publicField(this, "canvas");
5965
6199
  __publicField(this, "ui");
@@ -5969,13 +6203,17 @@ class Renderer {
5969
6203
  __publicField(this, "viewportSystem");
5970
6204
  __publicField(this, "interactionsSystem");
5971
6205
  __publicField(this, "controlsSystem");
6206
+ __publicField(this, "controlsAPI");
6207
+ __publicField(this, "eventsAPI");
6208
+ __publicField(this, "viewportAPI");
5972
6209
  __publicField(this, "clock");
5973
6210
  __publicField(this, "renderer");
5974
- __publicField(this, "viewport");
6211
+ __publicField(this, "visibleRectValue");
6212
+ __publicField(this, "memoryInfoExtension", null);
6213
+ __publicField(this, "memoryInfo");
6214
+ __publicField(this, "initialized", false);
6215
+ __publicField(this, "disposed", false);
5975
6216
  __publicField(this, "needsRedraw", true);
5976
- __publicField(this, "memoryInfoExtension");
5977
- __publicField(this, "memoryInfo", "");
5978
- var _a2, _b;
5979
6217
  const { canvas, gl, debugLog = false, ui } = opts;
5980
6218
  this.canvas = canvas;
5981
6219
  this.debugLog = debugLog;
@@ -5990,41 +6228,84 @@ class Renderer {
5990
6228
  this.renderer = new WebGLRenderer(rendererOptions);
5991
6229
  this.renderer.setSize(this.canvas.clientWidth, this.canvas.clientHeight, false);
5992
6230
  this.renderer.setPixelRatio(window.devicePixelRatio);
6231
+ this.renderer.autoClear = !this.isExternalMode;
5993
6232
  this.eventSystem = new EventSystem();
5994
6233
  this.viewportSystem = new ViewportSystem(this, this.eventSystem);
5995
6234
  this.layerSystem = new LayerSystem(this);
5996
6235
  this.interactionsSystem = new InteractionsSystem(this, this.eventSystem, this.viewportSystem, this.layerSystem);
5997
6236
  this.controlsSystem = new ControlsSystem(this, this.viewportSystem, this.interactionsSystem);
5998
- this.memoryInfoExtension = this.renderer.getContext().getExtension("GMAN_webgl_memory");
5999
- this.canvas.addEventListener("webglcontextlost", (e) => this.onContextLost(e), false);
6000
- this.canvas.addEventListener("webglcontextrestored", (e) => this.onContextRestored(e), false);
6001
- void ((_b = (_a2 = this.ui) == null ? void 0 : _a2.stats) == null ? void 0 : _b.init(this.renderer.getContext()));
6237
+ this.initContext(this.renderer.getContext());
6238
+ BatchedMesh.useMultiDraw = this.renderer.extensions.has("WEBGL_multi_draw");
6002
6239
  }
6003
6240
  /**
6004
6241
  * {@link ControlsAPI} instance for controlling the viewport
6005
6242
  */
6006
6243
  get controls() {
6007
- return asControlsAPI(this.controlsSystem);
6244
+ if (this.controlsAPI) return this.controlsAPI;
6245
+ const api = asControlsAPI(this.controlsSystem);
6246
+ const guard = (name) => this.assertInitialized(`controls.${name}`) && this.assertNotDisposed(`controls.${name}`) && this.assertNotExternalMode(`controls.${name}`);
6247
+ this.controlsAPI = {
6248
+ handlers: api.handlers,
6249
+ configure: api.configure,
6250
+ zoomBy: guardFn(guard, api.zoomBy, Promise.resolve()),
6251
+ zoomTo: guardFn(guard, api.zoomTo, Promise.resolve()),
6252
+ panBy: guardFn(guard, api.panBy, Promise.resolve()),
6253
+ panTo: guardFn(guard, api.panTo, Promise.resolve()),
6254
+ rollBy: guardFn(guard, api.rollBy, Promise.resolve()),
6255
+ rollTo: guardFn(guard, api.rollTo, Promise.resolve()),
6256
+ pitchBy: guardFn(guard, api.pitchBy, Promise.resolve()),
6257
+ pitchTo: guardFn(guard, api.pitchTo, Promise.resolve()),
6258
+ resetCamera: guardFn(guard, api.resetCamera, Promise.resolve()),
6259
+ setCameraBounds: guardFn(guard, api.setCameraBounds),
6260
+ getCameraState: guardFn(guard, api.getCameraState, {
6261
+ center: { x: 0, y: 0 },
6262
+ roll: 0,
6263
+ pitch: 0,
6264
+ zoom: 1,
6265
+ ptScale: 1
6266
+ })
6267
+ };
6268
+ return this.controlsAPI;
6008
6269
  }
6009
6270
  /**
6010
6271
  * {@link EventsAPI} instance for subscribing to internal events
6011
6272
  */
6012
6273
  get events() {
6013
- return asEventAPI(this.eventSystem);
6274
+ if (this.eventsAPI) return this.eventsAPI;
6275
+ this.eventsAPI = asEventAPI(this.eventSystem);
6276
+ return this.eventsAPI;
6277
+ }
6278
+ /**
6279
+ * {@link ViewportAPI} instance for view transforms and external transforms.
6280
+ */
6281
+ get viewport() {
6282
+ if (this.viewportAPI) return this.viewportAPI;
6283
+ const api = asViewportAPI(this.viewportSystem);
6284
+ const guard = (name) => this.assertInitialized(`viewport.${name}`) && this.assertNotDisposed(`viewport.${name}`);
6285
+ const guardExternal = (name) => guard(name) && this.assertExternalMode(`viewport.${name}`);
6286
+ this.viewportAPI = {
6287
+ canvasToSvg: guardFn(guard, api.canvasToSvg, { x: 0, y: 0 }),
6288
+ setStaticTransform: guardFn(guardExternal, api.setStaticTransform),
6289
+ setDynamicTransform: guardFn(guardExternal, api.setDynamicTransform)
6290
+ };
6291
+ return this.viewportAPI;
6014
6292
  }
6015
6293
  /**
6016
6294
  * Optional sub-rectangle of the viewport that is used for positioning scene's viewbox
6017
6295
  */
6018
6296
  get visibleRect() {
6019
- return this.viewport;
6297
+ return this.visibleRectValue;
6020
6298
  }
6021
6299
  /**
6022
6300
  * Optional sub-rectangle of the viewport that is used for positioning scene's viewbox
6023
6301
  */
6024
6302
  set visibleRect(rect) {
6025
- this.viewport = rect;
6026
- this.viewportSystem.updateViewport();
6027
- this.update();
6303
+ if (!this.assertNotExternalMode("visibleRect")) return;
6304
+ this.visibleRectValue = rect;
6305
+ if (this.initialized && !this.disposed) {
6306
+ this.viewportSystem.updateViewport();
6307
+ this.update();
6308
+ }
6028
6309
  }
6029
6310
  /**
6030
6311
  * Underlying {@link WebGLRenderer} instance
@@ -6040,64 +6321,73 @@ class Renderer {
6040
6321
  return [this.canvas.width, this.canvas.height];
6041
6322
  }
6042
6323
  /**
6043
- * Sets the renderer to external mode, where parts of rendering process are not managed by the renderer (e.g. Mapbox GL JS).
6044
- * @param staticTransformMatrix static transform matrix to apply to the scene
6324
+ * Returns true if the renderer is in external mode, meaning that webgl context is managed outside of the renderer
6325
+ * (for example, when using Mapbox GL JS as a host context). In this mode renderer will not clear the canvas before
6326
+ * rendering, and will not automatically compute scene and camera transformations. Clients are responsible for setting
6327
+ * the matrices manually by using {@link Renderer.viewport} methods.
6045
6328
  */
6046
- // TODO: Move somewhere
6047
- setExternalTransform(staticTransformMatrix) {
6048
- this.renderer.autoClear = false;
6049
- this.interactionsSystem.disconnect();
6050
- this.viewportSystem.setExternalTransform(staticTransformMatrix);
6329
+ get isExternalMode() {
6330
+ return this.gl !== void 0;
6051
6331
  }
6052
6332
  /**
6053
- * Update scene matrix from dynamic transform matrix.
6054
- * @param dynamicTransformMatrix dynamic transform matrix (changes every frame)
6333
+ * Returns true if the renderer is initialized, meaning that the viewport and scene have been set up.
6055
6334
  */
6056
- updateExternalCamera(dynamicTransformMatrix) {
6057
- this.viewportSystem.updateExternalCamera(dynamicTransformMatrix);
6335
+ get isInitialized() {
6336
+ return this.initialized;
6058
6337
  }
6059
6338
  /**
6060
- * Initialize the scene and start the rendering loop
6339
+ * Returns true if the renderer is disposed, meaning that all WebGL resources have been released.
6340
+ */
6341
+ get isDisposed() {
6342
+ return this.disposed;
6343
+ }
6344
+ /**
6345
+ * Initializes viewport and scene with the given scene definition.
6346
+ * Should be called once on startup. Repeated calls will produce console warnings.
6061
6347
  * @param sceneDef {@link SceneDef} to render
6062
- * @param startLoop whether to start the rendering loop
6063
6348
  */
6064
- start(sceneDef, startLoop = true) {
6065
- this.clock.start();
6349
+ init(sceneDef) {
6350
+ if (!this.assertNotDisposed("init")) return;
6351
+ if (!this.assertNotInitialized("init")) return;
6066
6352
  this.viewportSystem.initViewport(sceneDef);
6353
+ this.interactionsSystem.init();
6067
6354
  this.viewportSystem.scene.add(this.layerSystem.buildScene(sceneDef));
6068
- if (startLoop) this.renderer.setAnimationLoop(() => this.render());
6355
+ this.initialized = true;
6356
+ }
6357
+ /**
6358
+ * Start the rendering loop
6359
+ */
6360
+ start() {
6361
+ if (!this.assertNotDisposed("start")) return;
6362
+ if (!this.assertInitialized("start")) return;
6363
+ if (this.clock.running) return;
6364
+ this.clock.start();
6365
+ this.renderer.setAnimationLoop(() => this.render());
6069
6366
  }
6070
6367
  /**
6071
6368
  * Update the given defs to make them reflect the current state
6072
6369
  * @param defs {@link RenderableDef} array to update
6073
6370
  */
6074
6371
  update(...defs) {
6372
+ if (!this.assertNotDisposed("update")) return;
6373
+ if (!this.assertInitialized("update")) return;
6075
6374
  this.layerSystem.updateDefs(defs);
6076
6375
  this.needsRedraw = true;
6077
6376
  }
6078
6377
  /**
6079
- * Converts coordinates from canvas space to SVG space.
6080
- * @param point point in canvas space (relative to the canvas's top left corner), in css pixels
6081
- * @returns point in SVG space
6082
- */
6083
- screenToSvg(point) {
6084
- const vector2 = new Vector2(point.x, point.y);
6085
- canvasToNDC(vector2, this.canvas, vector2);
6086
- return this.viewportSystem.screenTo("svg", vector2);
6087
- }
6088
- /**
6089
- * Main rendering loop
6378
+ * Render a single frame
6090
6379
  */
6091
6380
  render() {
6092
6381
  var _a2, _b, _c, _d, _e, _f;
6382
+ if (!this.assertNotDisposed("render")) return;
6383
+ if (!this.assertInitialized("render")) return;
6093
6384
  (_b = (_a2 = this.ui) == null ? void 0 : _a2.stats) == null ? void 0 : _b.begin();
6094
- if (this.gl !== void 0) this.renderer.resetState();
6385
+ if (this.isExternalMode) this.renderer.resetState();
6095
6386
  else this.resizeCanvasToDisplaySize();
6096
6387
  this.viewportSystem.updatePtScale();
6097
- const delta = this.clock.getDelta();
6098
6388
  const hasDefsUpdated = this.layerSystem.processPendingUpdates();
6099
- const hasControlsUpdated = this.interactionsSystem.updateControls(delta);
6100
- const needsRedraw = this.needsRedraw || hasControlsUpdated || hasDefsUpdated || this.ui;
6389
+ const hasControlsUpdated = this.interactionsSystem.updateControls(this.clock.getDelta());
6390
+ const needsRedraw = this.needsRedraw || hasControlsUpdated || hasDefsUpdated || this.isExternalMode || this.ui;
6101
6391
  if (needsRedraw) {
6102
6392
  this.renderer.render(this.viewportSystem.scene, this.viewportSystem.camera);
6103
6393
  this.needsRedraw = false;
@@ -6106,6 +6396,30 @@ class Renderer {
6106
6396
  (_f = (_e = this.ui) == null ? void 0 : _e.stats) == null ? void 0 : _f.update();
6107
6397
  this.updateMemoryInfo();
6108
6398
  }
6399
+ /**
6400
+ * Stop the rendering loop
6401
+ */
6402
+ stop() {
6403
+ this.renderer.setAnimationLoop(null);
6404
+ this.clock.stop();
6405
+ }
6406
+ /**
6407
+ * Dispose all WebGL resources. This calls {@link Renderer.stop} internally.
6408
+ * WARNING: This method is final and cannot be undone. Attempting to use the renderer after calling this method
6409
+ * will result in a console warning and no-op methods. If you need to re-initialize the renderer, create a new instance.
6410
+ */
6411
+ dispose() {
6412
+ if (this.disposed) return;
6413
+ this.stop();
6414
+ this.interactionsSystem.dispose();
6415
+ this.layerSystem.disposeScene();
6416
+ this.viewportSystem.scene.clear();
6417
+ this.renderer.clear();
6418
+ this.renderer.info.reset();
6419
+ this.renderer.dispose();
6420
+ this.updateMemoryInfo();
6421
+ this.disposed = true;
6422
+ }
6109
6423
  // https://webgl2fundamentals.org/webgl/lessons/webgl-resizing-the-canvas.html
6110
6424
  resizeCanvasToDisplaySize() {
6111
6425
  const dpr = window.devicePixelRatio;
@@ -6121,38 +6435,100 @@ class Renderer {
6121
6435
  }
6122
6436
  }
6123
6437
  updateMemoryInfo() {
6124
- var _a2;
6438
+ var _a2, _b, _c, _d;
6125
6439
  if (this.memoryInfoExtension && ((_a2 = this.ui) == null ? void 0 : _a2.memoryInfoPanel)) {
6126
6440
  const memoryInfo = this.memoryInfoExtension.getMemoryInfo();
6127
- const memoryInfoContent = JSON.stringify(memoryInfo.memory, null, 2);
6128
- const elapsedTime = this.clock.getElapsedTime() * 1e3;
6129
- if (memoryInfoContent !== this.memoryInfo) {
6441
+ memoryInfo.resources["drawCalls"] = this.renderer.info.render.calls;
6442
+ if (memoryInfo.memory["texture"] !== ((_b = this.memoryInfo) == null ? void 0 : _b.memory["texture"]) || memoryInfo.memory["buffer"] !== ((_c = this.memoryInfo) == null ? void 0 : _c.memory["buffer"]) || memoryInfo.memory["renderbuffer"] !== ((_d = this.memoryInfo) == null ? void 0 : _d.memory["renderbuffer"])) {
6443
+ const elapsedTime = this.clock.getElapsedTime() * 1e3;
6130
6444
  const logMarker = `memoryInfo [${elapsedTime.toFixed(2)}ms since start]`;
6131
6445
  if (this.debugLog) console.log(logMarker, memoryInfo);
6132
- this.memoryInfo = memoryInfoContent;
6446
+ this.memoryInfo = memoryInfo;
6447
+ this.ui.memoryInfoPanel.textContent = JSON.stringify(memoryInfo, null, 2);
6133
6448
  }
6134
- this.ui.memoryInfoPanel.textContent = JSON.stringify(memoryInfo, null, 2);
6135
6449
  }
6136
6450
  }
6137
6451
  onContextLost(event) {
6452
+ var _a2, _b;
6138
6453
  event.preventDefault();
6139
6454
  console.log("webglcontextlost event", event);
6140
- this.renderer.setAnimationLoop(null);
6141
- this.clock.stop();
6142
- if (this.ui) setTimeout(() => this.renderer.forceContextRestore(), 0);
6455
+ const stats = (_a2 = this.ui) == null ? void 0 : _a2.stats;
6456
+ const context = this.renderer.getContext();
6457
+ if (stats && "deleteQuery" in context) {
6458
+ const gpuQueries = stats.gpuQueries;
6459
+ for (const queryInfo of gpuQueries) {
6460
+ this.renderer.getContext().deleteQuery(queryInfo.query);
6461
+ }
6462
+ stats.gpuQueries = [];
6463
+ if (stats.gpuPanel) {
6464
+ stats.dom.removeChild((_b = stats.gpuPanel) == null ? void 0 : _b.canvas);
6465
+ stats.gpuPanel = null;
6466
+ stats._panelId--;
6467
+ }
6468
+ }
6469
+ this.stop();
6143
6470
  }
6144
6471
  onContextRestored(event) {
6145
6472
  event.preventDefault();
6146
6473
  console.log("webglcontextrestored event", event);
6147
- this.renderer.setAnimationLoop(() => this.render());
6148
- this.clock.start();
6474
+ this.initContext(this.renderer.getContext());
6475
+ this.needsRedraw = true;
6476
+ this.start();
6477
+ }
6478
+ initContext(context) {
6479
+ var _a2, _b;
6480
+ this.memoryInfoExtension = context.getExtension("GMAN_webgl_memory");
6481
+ void ((_b = (_a2 = this.ui) == null ? void 0 : _a2.stats) == null ? void 0 : _b.init(context));
6482
+ }
6483
+ assertNotDisposed(funcName) {
6484
+ if (this.disposed) {
6485
+ console.warn(`[Renderer.${funcName}]: Renderer is used after being disposed. Please create a new instance.`);
6486
+ return false;
6487
+ }
6488
+ return true;
6489
+ }
6490
+ assertInitialized(funcName) {
6491
+ if (!this.initialized) {
6492
+ console.warn(`[Renderer.${funcName}]: Renderer is not initialized. Please call init() before using it.`);
6493
+ return false;
6494
+ }
6495
+ return true;
6496
+ }
6497
+ assertNotInitialized(funcName) {
6498
+ if (this.initialized) {
6499
+ console.warn(`[Renderer.${funcName}]: Renderer is already initialized. Please call init() only once.`);
6500
+ return false;
6501
+ }
6502
+ return true;
6149
6503
  }
6504
+ assertNotExternalMode(funcName) {
6505
+ if (this.isExternalMode) {
6506
+ console.warn(`[Renderer.${funcName}]: This operation is not supported in external mode.`);
6507
+ return false;
6508
+ }
6509
+ return true;
6510
+ }
6511
+ assertExternalMode(funcName) {
6512
+ if (!this.isExternalMode) {
6513
+ console.warn(`[Renderer.${funcName}]: This operation is only supported in external mode.`);
6514
+ return false;
6515
+ }
6516
+ return true;
6517
+ }
6518
+ }
6519
+ function guardFn(guard, fn, ...fallback) {
6520
+ return (...args) => {
6521
+ const name = fn.name.split(" ").at(-1);
6522
+ if (!guard(name)) return fallback[0];
6523
+ return fn(...args);
6524
+ };
6150
6525
  }
6151
6526
  export {
6152
6527
  Polygon,
6153
6528
  Rect,
6154
6529
  Renderer,
6155
6530
  createVector2,
6531
+ createVector3,
6156
6532
  isImageDef,
6157
6533
  isImageLayer,
6158
6534
  isLayerDef,