@expofp/renderer 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +706 -0
  2. package/dist/index.js +3098 -0
  3. package/package.json +29 -0
package/dist/index.js ADDED
@@ -0,0 +1,3098 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+ 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, Raycaster, Sphere, Box3, Spherical, PerspectiveCamera, Scene, Plane, Clock, WebGLRenderer } from "three";
5
+ import { traverseAncestorsGenerator } from "three/examples/jsm/utils/SceneUtils.js";
6
+ import { BatchedText as BatchedText$1, Text as Text$1 } from "troika-three-text";
7
+ import { LineMaterial, LineSegmentsGeometry } from "three/examples/jsm/Addons.js";
8
+ import { MaxRectsPacker, Rectangle } from "maxrects-packer";
9
+ import { converter, parse } from "culori";
10
+ import CameraController from "camera-controls";
11
+ import { EventManager, Rotate } from "mjolnir.js";
12
+ import { DEG2RAD, RAD2DEG } from "three/src/math/MathUtils.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
+ }
33
+ }
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;
43
+ }
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;
60
+ }
61
+ return { array, size, type, format };
62
+ }
63
+ class SquareDataTexture extends DataTexture {
64
+ /**
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.
71
+ */
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;
90
+ }
91
+ /**
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.
96
+ */
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
+ }
110
+ }
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];
128
+ }
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 };
146
+ }
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
+ );
174
+ }
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}`
184
+ );
185
+ }
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
+ `;
229
+ } else {
230
+ const components = this.getUniformComponents(offset, size);
231
+ getFromTexels += /*glsl*/
232
+ `${type} ${name} = ${this.uniformPrefix}texel${tId}.${components};
233
+ `;
234
+ }
235
+ }
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 };
255
+ }
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];
261
+ }
262
+ return components;
263
+ }
264
+ }
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);
294
+ }
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);
327
+ }
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 _a;
334
+ if (!this.isMaterialPatched) this.patchMaterial(material);
335
+ (_a = this.uniformsTexture) == null ? void 0 : _a.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);
340
+ }
341
+ super.onBeforeRender(renderer, scene, camera, geometry, material, group);
342
+ this.batchCount = this.updateIndexBuffer(geometry);
343
+ this._multiDrawCount = 0;
344
+ }
345
+ onAfterRender(renderer, scene, camera, geometry) {
346
+ var _a;
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", (_a = this.parent) == null ? void 0 : _a.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);
355
+ }
356
+ /**
357
+ * Retrieves the value of a uniform at the specified instance ID.
358
+ * @param id instance ID
359
+ * @param name uniform name
360
+ * @param target Optional target object to store the uniform value
361
+ * @returns the uniform value
362
+ */
363
+ getUniformAt(id, name, target) {
364
+ if (!this.uniformsTexture) {
365
+ console.warn(`BatchedMesh.getUniformAt: uniforms texture not initialized`);
366
+ return 0;
367
+ }
368
+ return this.uniformsTexture.getUniformAt(id, name, target);
369
+ }
370
+ /**
371
+ * Sets the value of a uniform at the specified instance ID.
372
+ * @param instanceId instance ID
373
+ * @param name uniform name
374
+ * @param value uniform value
375
+ */
376
+ setUniformAt(instanceId, name, value) {
377
+ if (!this.uniformsTexture) {
378
+ console.warn(`BatchedMesh.setUniformAt: uniforms texture not initialized`);
379
+ return;
380
+ }
381
+ this.uniformsTexture.setUniformAt(instanceId, name, value);
382
+ }
383
+ updateMatrixWorld(force) {
384
+ super.updateMatrixWorld(force);
385
+ if (this.boundsNeedsUpdate) {
386
+ this.computeBoundingBox();
387
+ this.computeBoundingSphere();
388
+ this.boundsNeedsUpdate = false;
389
+ }
390
+ }
391
+ setMatrixAt(instanceId, matrix) {
392
+ super.setMatrixAt(instanceId, matrix);
393
+ this.boundsNeedsUpdate = true;
394
+ return this;
395
+ }
396
+ dispose() {
397
+ var _a;
398
+ this.geometry.setIndex(this.indexBuffer ?? null);
399
+ super.dispose();
400
+ this.uniformSchema = {};
401
+ (_a = this.uniformsTexture) == null ? void 0 : _a.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 _a;
411
+ const vertexCount = geometry.attributes["position"].count;
412
+ const indexCount = ((_a = geometry.index) == null ? void 0 : _a.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 _a;
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 } = ((_a = this.uniformsTexture) == null ? void 0 : _a.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);
604
+ }
605
+ dispose() {
606
+ super.dispose();
607
+ this.dispatchEvent({ type: "dispose" });
608
+ }
609
+ _prepareForRender(material) {
610
+ var _a;
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 = ((_a = this._members.get(text)) == null ? void 0 : _a.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);
678
+ }
679
+ }
680
+ texture.needsUpdate = this.textureNeedsUpdate;
681
+ material.setMatrixTexture(texture);
682
+ }
683
+ setTexData(index, value, texData) {
684
+ if (value !== texData[index]) {
685
+ texData[index] = value;
686
+ this.textureNeedsUpdate = true;
687
+ }
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);
749
+ }
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
+ }
761
+ }
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();
775
+ }
776
+ material._orientation = orient;
777
+ }
778
+ }
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;
836
+ }
837
+ }
838
+ }
839
+ const shader = material.userData.shader;
840
+ if (!shader) {
841
+ material.userData.uDim = uDim;
842
+ material.userData.skipDimTexture = object.userData["skipDimTexture"];
843
+ return;
844
+ }
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;
893
+ }
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
+ );
911
+ const sharedParameters = {
912
+ side: DoubleSide,
913
+ transparent: true,
914
+ depthTest: false
915
+ };
916
+ class MaterialSystem {
917
+ constructor() {
918
+ __publicField(this, "backgroundMaterial");
919
+ __publicField(this, "viewport", new Vector4());
920
+ }
921
+ /**
922
+ * Creates a line material.
923
+ * @param params {@link LineMaterialParameters}
924
+ * @returns LineMaterial instance
925
+ */
926
+ createLineMaterial(params) {
927
+ const material = new LineMaterial({ ...sharedParameters, ...params });
928
+ material.onBeforeCompile = (shader) => {
929
+ shader.defines ?? (shader.defines = {});
930
+ shader.defines["USE_BATCHING_COLOR"] = "";
931
+ shader.vertexShader = shader.vertexShader.replace(
932
+ "#include <common>",
933
+ /*glsl*/
934
+ `
935
+ #include <common>
936
+ #include <batching_pars_vertex>
937
+ `
938
+ ).replace(
939
+ "void main() {",
940
+ `void main() {
941
+ #include <color_vertex>
942
+ #include <batching_vertex>
943
+ `
944
+ );
945
+ };
946
+ material.onBeforeRender = (renderer) => {
947
+ const uniforms = material.uniforms;
948
+ if (uniforms) {
949
+ renderer.getViewport(this.viewport);
950
+ uniforms["resolution"].value.set(this.viewport.z, this.viewport.w);
951
+ }
952
+ };
953
+ this.addPolygonOffset(material);
954
+ addDimToMaterial(material);
955
+ return material;
956
+ }
957
+ /**
958
+ * Creates a color material.
959
+ * @param partialParams {@link MaterialColorParams}
960
+ * @returns MeshBasicMaterial instance
961
+ */
962
+ createColorMaterial(partialParams = {}) {
963
+ const params = {
964
+ color: partialParams.color ?? 16777215,
965
+ opacity: partialParams.opacity ?? 1
966
+ };
967
+ const material = new MeshBasicMaterial({
968
+ ...sharedParameters,
969
+ color: params.color,
970
+ opacity: params.opacity
971
+ });
972
+ this.addPolygonOffset(material);
973
+ addDimToMaterial(material);
974
+ return material;
975
+ }
976
+ /**
977
+ * Creates a texture material.
978
+ * @param map {@link Texture}
979
+ * @param uvOffset whether to enable uv offset with per instance uniforms (for texture atlases)
980
+ * @returns MeshBasicMaterial instance
981
+ */
982
+ createTextureMaterial(map, uvOffset = false) {
983
+ const material = new MeshBasicMaterial({ ...sharedParameters, map });
984
+ if (uvOffset) {
985
+ material.onBeforeCompile = (shader) => {
986
+ shader.vertexShader = shader.vertexShader.replace(
987
+ "#include <uv_vertex>",
988
+ /*glsl*/
989
+ `
990
+ #include <uv_vertex>
991
+ vMapUv = uv * uvOffset.zw + uvOffset.xy;
992
+ `
993
+ );
994
+ };
995
+ }
996
+ this.addPolygonOffset(material);
997
+ addDimToMaterial(material);
998
+ return material;
999
+ }
1000
+ /**
1001
+ * Creates a background material. Used for the background layer to support dimming the background.
1002
+ * @param color background color in css format
1003
+ * @returns MeshBasicMaterial instance
1004
+ */
1005
+ createBackgroundMaterial(color) {
1006
+ if (!this.backgroundMaterial) {
1007
+ this.backgroundMaterial = new MeshBasicMaterial({ ...sharedParameters, color: new Color(color) });
1008
+ this.backgroundMaterial.onBeforeCompile = (shader) => {
1009
+ shader.vertexShader = shader.vertexShader.replace(
1010
+ "#include <project_vertex>",
1011
+ /*glsl*/
1012
+ `
1013
+ gl_Position = vec4(transformed.xy, 1.0, 1.0);
1014
+ `
1015
+ );
1016
+ };
1017
+ this.addPolygonOffset(this.backgroundMaterial);
1018
+ addDimToMaterial(this.backgroundMaterial);
1019
+ }
1020
+ return this.backgroundMaterial;
1021
+ }
1022
+ addPolygonOffset(material) {
1023
+ return;
1024
+ }
1025
+ }
1026
+ function isShapeDef(def) {
1027
+ return def.shape !== void 0;
1028
+ }
1029
+ function isImageDef(def) {
1030
+ return def.source !== void 0;
1031
+ }
1032
+ function isTextDef(def) {
1033
+ return def.lines !== void 0;
1034
+ }
1035
+ function isLineDef(def) {
1036
+ return def.points !== void 0;
1037
+ }
1038
+ function isLayerDef(def) {
1039
+ return def.children !== void 0;
1040
+ }
1041
+ function isShapeLayer(layer) {
1042
+ return layer.children[0] && isShapeDef(layer.children[0]);
1043
+ }
1044
+ function isImageLayer(layer) {
1045
+ return layer.children[0] && isImageDef(layer.children[0]);
1046
+ }
1047
+ function isTextLayer(layer) {
1048
+ return layer.children[0] && isTextDef(layer.children[0]);
1049
+ }
1050
+ function isLineLayer(layer) {
1051
+ return layer.children[0] && isLineDef(layer.children[0]);
1052
+ }
1053
+ function isLayerLayer(layer) {
1054
+ return layer.children[0] && isLayerDef(layer.children[0]);
1055
+ }
1056
+ const INTERACTIVE_LAYER = 1;
1057
+ function setInteractive(object, isInteractive) {
1058
+ if (isInteractive) object.layers.enable(INTERACTIVE_LAYER);
1059
+ else object.layers.disable(INTERACTIVE_LAYER);
1060
+ object.children.forEach((child) => setInteractive(child, isInteractive));
1061
+ }
1062
+ function isVisible(object) {
1063
+ if (!object.visible) return false;
1064
+ return [...traverseAncestorsGenerator(object)].every((obj) => obj.visible);
1065
+ }
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
+ class RenderableSystem {
1080
+ /**
1081
+ * @param type readable name of the system's type for debugging
1082
+ * @param renderer {@link Renderer}
1083
+ */
1084
+ constructor(type, renderer) {
1085
+ __publicField(this, "mapDefToObject", /* @__PURE__ */ new Map());
1086
+ __publicField(this, "mapObjectToDefs", /* @__PURE__ */ new Map());
1087
+ this.type = type;
1088
+ this.renderer = renderer;
1089
+ }
1090
+ /**
1091
+ * Update a def with its current properties.
1092
+ * This public method handles shared properties and delegates to the protected implementation.
1093
+ * @param def object definition to update
1094
+ */
1095
+ updateDef(def) {
1096
+ const mapping = this.getObjectInstanceByDef(def);
1097
+ if (!mapping) return;
1098
+ const { object: mesh, instanceIds } = mapping;
1099
+ for (const instanceId of instanceIds) {
1100
+ mesh.setVisibleAt(instanceId, !def.hidden);
1101
+ }
1102
+ if (def.hidden) return;
1103
+ for (const instanceId of instanceIds) {
1104
+ toggleInstanceDim(mesh, instanceId, def.dim);
1105
+ }
1106
+ this.updateDefImpl(def, mesh, instanceIds);
1107
+ }
1108
+ /**
1109
+ * Update an existing collection with a new layer definition.
1110
+ * This public method handles logging and delegates to the protected implementation.
1111
+ * @param group {@link Group} containing the objects that layer maps to
1112
+ * @param layerDef {@link TypedLayerDef} layer definition to update
1113
+ */
1114
+ updateLayer(group, layerDef) {
1115
+ if (this.renderer.debugLog) console.log(`Updating ${this.type} layer ${layerDef.name}`, layerDef);
1116
+ this.updateLayerImpl(group, layerDef);
1117
+ }
1118
+ /**
1119
+ * Get the def that was intersected by a raycast.
1120
+ * @param intersection - The intersection object containing the intersected object and batchId.
1121
+ * @returns The def that was intersected, or undefined if the intersected object is not a ShapeDef.
1122
+ */
1123
+ getIntersectedDef(intersection) {
1124
+ const mesh = intersection.object;
1125
+ const batchId = intersection.batchId;
1126
+ const shapeDef = this.getDefsByObject(mesh)[batchId];
1127
+ return shapeDef;
1128
+ }
1129
+ /**
1130
+ * Protected implementation method that subclasses can override.
1131
+ * This ensures logging always happens even when the method is overridden.
1132
+ * @param group {@link Group} containing the objects that layer maps to
1133
+ * @param layerDef {@link TypedLayerDef} layer definition to update
1134
+ */
1135
+ updateLayerImpl(group, layerDef) {
1136
+ this.disposeLayer(group);
1137
+ group.add(...this.buildLayer(layerDef).children);
1138
+ }
1139
+ /**
1140
+ * Dispose of all objects in a collection and clear the group.
1141
+ * This provides common disposal logic that can be overridden if needed.
1142
+ * Follows Three.js disposal best practices: https://threejs.org/manual/#en/how-to-dispose-of-objects
1143
+ * @param group {@link Group} containing the objects that layer maps to
1144
+ */
1145
+ disposeLayer(group) {
1146
+ for (const mesh of group.children) {
1147
+ this.disposeObject(mesh);
1148
+ this.unregisterObject(mesh);
1149
+ }
1150
+ group.clear();
1151
+ }
1152
+ /**
1153
+ * Dispose of a single object following Three.js disposal best practices.
1154
+ * Handles materials, textures, and geometries properly.
1155
+ * @param object object to dispose
1156
+ */
1157
+ disposeObject(object) {
1158
+ if ("dispose" in object && typeof object.dispose === "function") {
1159
+ object.dispose();
1160
+ }
1161
+ if ("geometry" in object) {
1162
+ const geometry = object.geometry;
1163
+ geometry == null ? void 0 : geometry.dispose();
1164
+ }
1165
+ if ("material" in object) {
1166
+ const material = object.material;
1167
+ if (material) {
1168
+ if (Array.isArray(material)) {
1169
+ material.forEach((mat) => this.disposeMaterial(mat));
1170
+ } else {
1171
+ this.disposeMaterial(material);
1172
+ }
1173
+ }
1174
+ }
1175
+ }
1176
+ /**
1177
+ * Dispose of a material and all its textures following Three.js best practices.
1178
+ * @param material {@link Material} to dispose
1179
+ */
1180
+ disposeMaterial(material) {
1181
+ const textureProperties = [
1182
+ "map",
1183
+ "normalMap",
1184
+ "emissiveMap",
1185
+ "specularMap",
1186
+ "roughnessMap",
1187
+ "metalnessMap",
1188
+ "alphaMap",
1189
+ "envMap",
1190
+ "lightMap",
1191
+ "aoMap",
1192
+ "displacementMap"
1193
+ ];
1194
+ for (const prop of textureProperties) {
1195
+ const texture = material[prop];
1196
+ if (texture && texture instanceof Texture) {
1197
+ texture.dispose();
1198
+ }
1199
+ }
1200
+ material.dispose();
1201
+ }
1202
+ /**
1203
+ * Register a mapping between a def and an object/instance.
1204
+ * @param def def to register
1205
+ * @param object object to which the def is mapped
1206
+ * @param instanceIds range of instance ids in the container that the def is mapped to
1207
+ */
1208
+ registerDefObject(def, object, instanceIds) {
1209
+ const ids = Array.isArray(instanceIds) ? instanceIds : [instanceIds];
1210
+ if (ids.length === 0) {
1211
+ console.warn(`[RenderableSystem] Tried to register def with empty instanceIds:`, def);
1212
+ return;
1213
+ }
1214
+ this.mapDefToObject.set(def, { object, instanceIds: ids });
1215
+ if (!this.mapObjectToDefs.has(object)) {
1216
+ this.mapObjectToDefs.set(object, []);
1217
+ }
1218
+ this.mapObjectToDefs.get(object).push(def);
1219
+ this.updateDef(def);
1220
+ }
1221
+ /**
1222
+ * Unregister a def and its associated object mapping.
1223
+ * @param def def to unregister
1224
+ */
1225
+ unregisterDef(def) {
1226
+ const mapping = this.mapDefToObject.get(def);
1227
+ if (mapping) {
1228
+ const { object } = mapping;
1229
+ const defs = this.mapObjectToDefs.get(object);
1230
+ if (defs) {
1231
+ this.mapObjectToDefs.set(
1232
+ object,
1233
+ defs.filter((d) => d !== def)
1234
+ );
1235
+ if (this.mapObjectToDefs.get(object).length === 0) {
1236
+ this.mapObjectToDefs.delete(object);
1237
+ }
1238
+ }
1239
+ this.mapDefToObject.delete(def);
1240
+ }
1241
+ }
1242
+ /**
1243
+ * Unregister all defs associated with an object.
1244
+ * @param object object to unregister
1245
+ */
1246
+ unregisterObject(object) {
1247
+ const defs = this.mapObjectToDefs.get(object);
1248
+ if (defs) {
1249
+ for (const def of defs) {
1250
+ this.mapDefToObject.delete(def);
1251
+ }
1252
+ this.mapObjectToDefs.delete(object);
1253
+ }
1254
+ }
1255
+ /**
1256
+ * Clear all mappings.
1257
+ */
1258
+ clearMappings() {
1259
+ this.mapDefToObject.clear();
1260
+ this.mapObjectToDefs.clear();
1261
+ }
1262
+ /**
1263
+ * Lookup object/instance by def.
1264
+ * @param def def to lookup
1265
+ * @returns object and instance ids
1266
+ */
1267
+ getObjectInstanceByDef(def) {
1268
+ const mapping = this.mapDefToObject.get(def);
1269
+ if (!mapping) {
1270
+ console.warn(`[RenderableSystem] No object mapping found for def:`, def);
1271
+ return void 0;
1272
+ }
1273
+ return mapping;
1274
+ }
1275
+ /**
1276
+ * Lookup defs by object.
1277
+ * @param object object to lookup
1278
+ * @returns Array of defs mapped to the object
1279
+ */
1280
+ getDefsByObject(object) {
1281
+ return this.mapObjectToDefs.get(object) ?? [];
1282
+ }
1283
+ /**
1284
+ * Get all objects currently handled by the system.
1285
+ * @returns Array of container objects
1286
+ */
1287
+ getAllObjects() {
1288
+ return Array.from(this.mapObjectToDefs.keys());
1289
+ }
1290
+ }
1291
+ class ImageSystem extends RenderableSystem {
1292
+ /**
1293
+ * @param materialSystem {@link MaterialSystem}
1294
+ * @param renderer {@link Renderer}
1295
+ */
1296
+ constructor(materialSystem, renderer) {
1297
+ super("image", renderer);
1298
+ /** Textures memory limit in megabytes */
1299
+ __publicField(this, "memoryLimitMb");
1300
+ __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());
1305
+ this.materialSystem = materialSystem;
1306
+ const atlasTextureSize = renderer.context.capabilities.maxTextureSize;
1307
+ console.log(`Max texture size: ${atlasTextureSize}`);
1308
+ const padding = 1;
1309
+ this.packer = new MaxRectsPacker(atlasTextureSize, atlasTextureSize, padding, { pot: false });
1310
+ }
1311
+ updateLayerImpl(group, layerDef) {
1312
+ super.updateLayerImpl(group, layerDef);
1313
+ if (this.memoryLimitMb) this.resizeTextures();
1314
+ }
1315
+ buildLayer(layer) {
1316
+ var _a;
1317
+ const group = new Group();
1318
+ const images = layer.children;
1319
+ const bins = this.packImages(images);
1320
+ const uvOffset = new Vector4();
1321
+ for (const bin of bins) {
1322
+ const rectsWithDef = bin.rects.flatMap(
1323
+ (rect) => rect.data.map((imageWithIndex) => ({
1324
+ def: imageWithIndex.def,
1325
+ rect,
1326
+ originalIndex: imageWithIndex.originalIndex
1327
+ }))
1328
+ ).sort((a, b) => a.originalIndex - b.originalIndex);
1329
+ const instanceCount = rectsWithDef.length;
1330
+ const texture = createAtlas(bin);
1331
+ const instanceMaterial = this.materialSystem.createTextureMaterial(texture, true);
1332
+ const instanceGeometry = new PlaneGeometry();
1333
+ const vertexCount = instanceGeometry.attributes["position"].count;
1334
+ const indexCount = ((_a = instanceGeometry.index) == null ? void 0 : _a.count) ?? 0;
1335
+ const batchedMesh = new BatchedMesh(instanceCount, vertexCount, indexCount, instanceMaterial);
1336
+ batchedMesh.sortObjects = false;
1337
+ batchedMesh.addPerInstanceUniforms({ vertex: { uvOffset: "vec4" } });
1338
+ const geometryId = batchedMesh.addGeometry(instanceGeometry);
1339
+ for (const { def, rect } of rectsWithDef) {
1340
+ const instanceId = batchedMesh.addInstance(geometryId);
1341
+ uvOffset.set(rect.x / bin.width, rect.y / bin.height, rect.width / bin.width, rect.height / bin.height);
1342
+ batchedMesh.setUniformAt(instanceId, "uvOffset", uvOffset);
1343
+ this.registerDefObject(def, batchedMesh, instanceId);
1344
+ }
1345
+ const nonResizable = rectsWithDef.some(({ def }) => def.source instanceof HTMLCanvasElement);
1346
+ batchedMesh.userData["nonResizable"] = nonResizable;
1347
+ group.add(batchedMesh);
1348
+ }
1349
+ return group;
1350
+ }
1351
+ /**
1352
+ * Resize textures to fit the memory limit.
1353
+ */
1354
+ resizeTextures() {
1355
+ var _a;
1356
+ if (!this.memoryLimitMb) {
1357
+ console.warn("Memory limit is not set, unable to resize textures.");
1358
+ return;
1359
+ }
1360
+ console.log(`Resizing textures to fit memory limit: ${this.memoryLimitMb} MB`);
1361
+ const texturesToResize = [];
1362
+ let totalResizable = 0;
1363
+ let totalNonResizable = 0;
1364
+ for (const mesh of this.getAllObjects()) {
1365
+ const texture = mesh.material.map;
1366
+ const imageBytes = getTextureSizeBytes(texture);
1367
+ const nonResizable = mesh.userData["nonResizable"];
1368
+ if (nonResizable) {
1369
+ totalNonResizable += imageBytes;
1370
+ } else {
1371
+ totalResizable += imageBytes;
1372
+ texturesToResize.push(mesh);
1373
+ }
1374
+ }
1375
+ const budget = this.memoryLimitMb * 1024 * 1024 - totalNonResizable;
1376
+ if (budget < 0) {
1377
+ console.warn("Memory limit is too low, unable to resize textures.");
1378
+ return;
1379
+ }
1380
+ const resizeFactor = Math.sqrt(budget / totalResizable);
1381
+ if (resizeFactor >= 1) {
1382
+ console.log("Textures are already within the memory limit, no need to resize");
1383
+ return;
1384
+ }
1385
+ console.log(`Resize factor: ${resizeFactor}`);
1386
+ let newTotal = totalNonResizable;
1387
+ for (const mesh of texturesToResize) {
1388
+ const material = mesh.material;
1389
+ const texture = material.map;
1390
+ const resizedTexture = resizeTexture(texture, resizeFactor);
1391
+ const textureDim = `${texture.image.width}x${texture.image.height}`;
1392
+ const resizedDim = `${resizedTexture.image.width}x${resizedTexture.image.height}`;
1393
+ console.log(`Resized atlas for ${mesh.name || ((_a = mesh.parent) == null ? void 0 : _a.name)}, from ${textureDim} to ${resizedDim}`);
1394
+ newTotal += getTextureSizeBytes(resizedTexture);
1395
+ material.map = resizedTexture;
1396
+ material.needsUpdate = true;
1397
+ texture.dispose();
1398
+ mesh.userData["nonResizable"] = true;
1399
+ }
1400
+ console.log(`New memory usage after resizing: ${newTotal} bytes`);
1401
+ }
1402
+ updateDefImpl(imageDef, mesh, instanceIds) {
1403
+ 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);
1415
+ }
1416
+ packImages(images) {
1417
+ this.packer.reset();
1418
+ const rectangles = [];
1419
+ const mapImageSourceToRect = /* @__PURE__ */ new Map();
1420
+ for (let i = 0; i < images.length; i++) {
1421
+ const image = images[i];
1422
+ const imageWithIndex = { def: image, originalIndex: i };
1423
+ const existingRect = mapImageSourceToRect.get(image.source);
1424
+ if (!existingRect) {
1425
+ const sourceWidth = image.source.width;
1426
+ const sourceHeight = image.source.height;
1427
+ const sourceArea = sourceWidth * sourceHeight;
1428
+ const boundsWidth = image.bounds.size.width;
1429
+ const boundsHeight = image.bounds.size.height;
1430
+ const boundsArea = boundsWidth * boundsHeight;
1431
+ const ratio = sourceArea / boundsArea;
1432
+ if (ratio > 1e3) {
1433
+ console.log(`Image bounds: ${boundsWidth}x${boundsHeight}`, `Image source: ${sourceWidth}x${sourceHeight}`);
1434
+ console.warn(`Image bounds area is ${ratio.toFixed(2)} times smaller than the image.`);
1435
+ }
1436
+ const rect = new Rectangle(image.source.width, image.source.height);
1437
+ rect.data = [imageWithIndex];
1438
+ rectangles.push(rect);
1439
+ mapImageSourceToRect.set(image.source, rect);
1440
+ } else {
1441
+ existingRect.data.push(imageWithIndex);
1442
+ }
1443
+ }
1444
+ this.packer.addArray(rectangles);
1445
+ this.packer.bins.forEach((bin) => console.log(`Bin: ${bin.width}x${bin.height}, ${bin.rects.length} rectangles`));
1446
+ return this.packer.bins;
1447
+ }
1448
+ }
1449
+ function createAtlas(bin) {
1450
+ const t0 = performance.now();
1451
+ const canvas = document.createElement("canvas");
1452
+ canvas.width = bin.width;
1453
+ canvas.height = bin.height;
1454
+ const ctx = canvas.getContext("2d");
1455
+ for (const rect of bin.rects) {
1456
+ ctx.drawImage(rect.data[0].def.source, rect.x, rect.y, rect.width, rect.height);
1457
+ }
1458
+ const t1 = performance.now();
1459
+ console.log(`Create atlas took ${(t1 - t0).toFixed(2)} milliseconds.`);
1460
+ return createTexture(canvas);
1461
+ }
1462
+ function resizeTexture(texture, resizeFactor) {
1463
+ const canvas = document.createElement("canvas");
1464
+ canvas.width = Math.floor(texture.image.width * resizeFactor);
1465
+ canvas.height = Math.floor(texture.image.height * resizeFactor);
1466
+ const ctx = canvas.getContext("2d");
1467
+ ctx.drawImage(texture.image, 0, 0, canvas.width, canvas.height);
1468
+ return createTexture(canvas);
1469
+ }
1470
+ function createTexture(source) {
1471
+ const texture = new Texture(source);
1472
+ texture.generateMipmaps = true;
1473
+ texture.colorSpace = SRGBColorSpace;
1474
+ texture.flipY = false;
1475
+ texture.needsUpdate = true;
1476
+ return texture;
1477
+ }
1478
+ function getTextureSizeBytes(texture) {
1479
+ const imageBytes = texture.image.width * texture.image.height * 4 * (texture.generateMipmaps ? 1.33 : 1);
1480
+ return Math.ceil(imageBytes);
1481
+ }
1482
+ class LineSystem extends RenderableSystem {
1483
+ /**
1484
+ * @param materialSystem {@link MaterialSystem}
1485
+ * @param renderer {@link Renderer}
1486
+ */
1487
+ constructor(materialSystem, renderer) {
1488
+ super("line", renderer);
1489
+ __publicField(this, "lineColor", new Color());
1490
+ this.materialSystem = materialSystem;
1491
+ }
1492
+ buildLayer(layer) {
1493
+ var _a;
1494
+ const group = new Group();
1495
+ let vertexCount = 0;
1496
+ let indexCount = 0;
1497
+ const geometries = /* @__PURE__ */ new Map();
1498
+ const lines = layer.children;
1499
+ for (const line of lines) {
1500
+ const lineGeometry = new LineSegmentsGeometry();
1501
+ lineGeometry.setPositions(line.points.flatMap((pt) => [pt.x, pt.y, 0]));
1502
+ geometries.set(line, this.deinterleaveGeometry(lineGeometry));
1503
+ vertexCount += lineGeometry.attributes["position"].count;
1504
+ indexCount += ((_a = lineGeometry.index) == null ? void 0 : _a.count) ?? 0;
1505
+ }
1506
+ const material = this.materialSystem.createLineMaterial({ color: "white" });
1507
+ const batchedMesh = new BatchedMesh(lines.length, vertexCount, indexCount, material);
1508
+ batchedMesh.addPerInstanceUniforms({ vertex: { linewidth: "float" } });
1509
+ batchedMesh.setCustomSort((list) => list.sort((a, b) => a.z - b.z));
1510
+ for (const [line, geometry] of geometries.entries()) {
1511
+ const geometryId = batchedMesh.addGeometry(geometry);
1512
+ const instanceId = batchedMesh.addInstance(geometryId);
1513
+ this.registerDefObject(line, batchedMesh, instanceId);
1514
+ }
1515
+ group.add(batchedMesh);
1516
+ return group;
1517
+ }
1518
+ updateDefImpl(lineDef, mesh, instanceIds) {
1519
+ for (const instanceId of instanceIds) {
1520
+ mesh.setColorAt(instanceId, this.lineColor.set(lineDef.color));
1521
+ mesh.setUniformAt(instanceId, "linewidth", lineDef.width);
1522
+ }
1523
+ }
1524
+ // This is a hack for BatchedMesh
1525
+ deinterleaveGeometry(geometry) {
1526
+ const instanceStart = geometry.getAttribute("instanceStart");
1527
+ const instanceEnd = geometry.getAttribute("instanceEnd");
1528
+ const positions = geometry.getAttribute("position");
1529
+ const newInstanceStartBuffer = new Float32Array(geometry.instanceCount * positions.count * 3);
1530
+ const newInstanceEndBuffer = new Float32Array(geometry.instanceCount * positions.count * 3);
1531
+ const newInstanceStart = new BufferAttribute(newInstanceStartBuffer, 3);
1532
+ const newInstanceEnd = new BufferAttribute(newInstanceEndBuffer, 3);
1533
+ for (let i = 0; i < geometry.instanceCount; i++) {
1534
+ for (let j = 0; j < positions.count; j++) {
1535
+ for (let k = 0; k < 3; k++) {
1536
+ const index = i * positions.count * 3 + j * 3 + k;
1537
+ newInstanceStartBuffer[index] = instanceStart.getComponent(i, k);
1538
+ newInstanceEndBuffer[index] = instanceEnd.getComponent(i, k);
1539
+ }
1540
+ }
1541
+ }
1542
+ geometry.setAttribute("instanceStart", newInstanceStart);
1543
+ geometry.setAttribute("instanceEnd", newInstanceEnd);
1544
+ return geometry;
1545
+ }
1546
+ }
1547
+ function createVector2(vector2) {
1548
+ return vector2 instanceof Vector2 ? vector2 : Array.isArray(vector2) ? new Vector2(vector2[0], vector2[1]) : new Vector2(vector2.x, vector2.y);
1549
+ }
1550
+ class Rect {
1551
+ /**
1552
+ * @param min Top left corner of the rectangle.
1553
+ * @param max Bottom right corner of the rectangle.
1554
+ * @param rotation Optional rotation of the rectangle. In radians, around center. Positive values rotate clockwise.
1555
+ */
1556
+ constructor(min, max, rotation) {
1557
+ /** Top left corner of the rectangle. */
1558
+ __publicField(this, "min");
1559
+ /** Bottom right corner of the rectangle. */
1560
+ __publicField(this, "max");
1561
+ /** Optional rotation of the rectangle. In radians, around center. Positive values rotate clockwise. */
1562
+ __publicField(this, "rotation");
1563
+ __publicField(this, "_center");
1564
+ __publicField(this, "_size");
1565
+ this.min = createVector2(min);
1566
+ this.max = createVector2(max);
1567
+ this.rotation = rotation ?? 0;
1568
+ }
1569
+ /** Center of the rectangle. Read-only, calculated on demand. */
1570
+ get center() {
1571
+ this._center ?? (this._center = this.min.clone().add(this.max).multiplyScalar(0.5));
1572
+ return this._center;
1573
+ }
1574
+ /** Size of the rectangle. Read-only, calculated on demand. */
1575
+ get size() {
1576
+ this._size ?? (this._size = this.max.clone().sub(this.min));
1577
+ return this._size;
1578
+ }
1579
+ /**
1580
+ * Creates a rectangle from an SVG rectangle element.
1581
+ * @param rect {@link SVGRectElement} or {@link SVGImageElement}
1582
+ * @param rotation Optional rotation of the rectangle. In radians, around center. Positive values rotate clockwise.
1583
+ * @returns new {@link Rect} instance
1584
+ */
1585
+ static fromSvg(rect, rotation) {
1586
+ const x = rect.x.baseVal.value;
1587
+ const y = rect.y.baseVal.value;
1588
+ const width = rect.width.baseVal.value;
1589
+ const height = rect.height.baseVal.value;
1590
+ return new Rect([x, y], [x + width, y + height], rotation);
1591
+ }
1592
+ /**
1593
+ * Increases the size of the rectangle by the given amount. Padding is added to both sides of the rectangle.
1594
+ * @param x Horizontal padding.
1595
+ * @param y Vertical padding. If omitted, defaults to the same value as `x`.
1596
+ * @returns this {@link Rect} instance
1597
+ */
1598
+ addPadding(x, y = x) {
1599
+ this.min.add({ x, y });
1600
+ this.max.sub({ x, y });
1601
+ this._center = void 0;
1602
+ this._size = void 0;
1603
+ return this;
1604
+ }
1605
+ }
1606
+ class Polygon {
1607
+ /**
1608
+ * @param vertices Array of polygon vertices.
1609
+ * @param indices Array of polygon indices. Each index is a triplet of vertex indices forming a triangle.
1610
+ */
1611
+ constructor(vertices, indices) {
1612
+ /** Array of polygon vertices. */
1613
+ __publicField(this, "vertices");
1614
+ /** Array of polygon indices. Each index is a triplet of vertex indices forming a triangle. */
1615
+ __publicField(this, "indices");
1616
+ this.vertices = vertices.map(createVector2);
1617
+ this.indices = indices;
1618
+ }
1619
+ /**
1620
+ * Converts a {@link Rect} to a {@link Polygon}.
1621
+ * @param rect {@link Rect} instance
1622
+ * @returns new {@link Polygon} instance
1623
+ */
1624
+ static fromRect(rect) {
1625
+ const vertices = [
1626
+ { x: rect.min.x, y: rect.min.y },
1627
+ { x: rect.max.x, y: rect.min.y },
1628
+ { x: rect.max.x, y: rect.max.y },
1629
+ { x: rect.min.x, y: rect.max.y }
1630
+ ];
1631
+ const indices = [
1632
+ [0, 1, 2],
1633
+ [2, 3, 0]
1634
+ ];
1635
+ return new Polygon(vertices, indices);
1636
+ }
1637
+ /**
1638
+ * Merges multiple polygons into a single one.
1639
+ * @param polygons Array of {@link Polygon} instances
1640
+ * @returns new {@link Polygon} instance
1641
+ */
1642
+ static merge(...polygons) {
1643
+ let indexOffset = 0;
1644
+ const vertices = [];
1645
+ const indices = [];
1646
+ for (const polygon of polygons) {
1647
+ vertices.push(...polygon.vertices);
1648
+ indices.push(...polygon.indices.map((index) => index.map((i) => i + indexOffset)));
1649
+ indexOffset += polygon.vertices.length;
1650
+ }
1651
+ return new Polygon(vertices, indices);
1652
+ }
1653
+ /**
1654
+ * 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
+ }
1688
+ }
1689
+ return [truthy, falsy];
1690
+ }
1691
+ class MeshSystem extends RenderableSystem {
1692
+ /**
1693
+ * @param materialSystem {@link MaterialSystem}
1694
+ * @param renderer {@link Renderer}
1695
+ */
1696
+ constructor(materialSystem, renderer) {
1697
+ super("mesh", renderer);
1698
+ __publicField(this, "toRgbConverter", converter("rgb"));
1699
+ __publicField(this, "meshColor", new Color());
1700
+ this.materialSystem = materialSystem;
1701
+ }
1702
+ buildLayer(layer) {
1703
+ const shapes = layer.children;
1704
+ const mapShapeToNormColor = /* @__PURE__ */ new Map();
1705
+ for (const shapeDef of shapes) {
1706
+ const normColor = this.normalizeColor(shapeDef.color);
1707
+ if (normColor !== void 0) mapShapeToNormColor.set(shapeDef, normColor);
1708
+ }
1709
+ const [opaqueShapes, transparentShapes] = partition(
1710
+ shapes,
1711
+ (shapeDef) => {
1712
+ var _a;
1713
+ return (((_a = mapShapeToNormColor.get(shapeDef)) == null ? void 0 : _a.alpha) ?? 1) === 1;
1714
+ }
1715
+ );
1716
+ const transparentShapesGrouped = groupBy(transparentShapes, (shapeDef) => mapShapeToNormColor.get(shapeDef).alpha);
1717
+ const group = new Group();
1718
+ for (const [opacity, shapes2] of transparentShapesGrouped) {
1719
+ const transparentMesh = this.buildBatchedMesh(shapes2, opacity);
1720
+ transparentMesh.name = "transparent";
1721
+ group.add(transparentMesh);
1722
+ }
1723
+ if (opaqueShapes.length) {
1724
+ const opaqueMesh = this.buildBatchedMesh(opaqueShapes);
1725
+ opaqueMesh.name = "opaque";
1726
+ group.add(opaqueMesh);
1727
+ }
1728
+ return group;
1729
+ }
1730
+ updateDefImpl(shapeDef, mesh, instanceIds) {
1731
+ const color = this.normalizeColor(shapeDef.color);
1732
+ if (color === void 0) return;
1733
+ for (const instanceId of instanceIds) {
1734
+ mesh.setColorAt(instanceId, this.meshColor.setRGB(color.r, color.g, color.b, SRGBColorSpace));
1735
+ }
1736
+ }
1737
+ buildBatchedMesh(shapes, opacity = 1) {
1738
+ var _a, _b;
1739
+ let vertexCount = 0;
1740
+ let indexCount = 0;
1741
+ let rectGeometry = void 0;
1742
+ const shapeDefToGeometry = /* @__PURE__ */ new Map();
1743
+ 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
+ }
1750
+ vertexCount += rectGeometry.getAttribute("position").count;
1751
+ indexCount += ((_a = rectGeometry.index) == null ? void 0 : _a.count) ?? 0;
1752
+ } else if (shapeDef.shape instanceof Polygon) {
1753
+ const geometry = this.buildPolygonGeometry(shapeDef.shape);
1754
+ shapeDefToGeometry.set(shapeDef, geometry);
1755
+ vertexCount += geometry.getAttribute("position").count;
1756
+ indexCount += ((_b = geometry.index) == null ? void 0 : _b.count) ?? 0;
1757
+ }
1758
+ }
1759
+ const material = this.materialSystem.createColorMaterial({ opacity });
1760
+ const batchedMesh = new BatchedMesh(shapes.length, vertexCount, indexCount, material);
1761
+ batchedMesh.setCustomSort((list) => this.sortInstances(batchedMesh, list));
1762
+ const position = new Vector3();
1763
+ const rotation = new Quaternion();
1764
+ const scale = new Vector3();
1765
+ const matrix = new Matrix4();
1766
+ for (const shapeDef of shapes) {
1767
+ let instanceId = void 0;
1768
+ if (shapeDef.shape instanceof Rect) {
1769
+ const rectGeometryId = rectGeometry ? batchedMesh.addGeometry(rectGeometry) : void 0;
1770
+ instanceId = batchedMesh.addInstance(rectGeometryId);
1771
+ position.set(shapeDef.shape.center.x, shapeDef.shape.center.y, 0);
1772
+ rotation.setFromAxisAngle(new Vector3(0, 0, 1), shapeDef.shape.rotation ?? 0);
1773
+ scale.set(shapeDef.shape.size.x, shapeDef.shape.size.y, 1);
1774
+ matrix.compose(position, rotation, scale);
1775
+ batchedMesh.setMatrixAt(instanceId, matrix);
1776
+ } else if (shapeDef.shape instanceof Polygon) {
1777
+ const geometry = shapeDefToGeometry.get(shapeDef);
1778
+ const polygonGeometryId = batchedMesh.addGeometry(geometry);
1779
+ instanceId = batchedMesh.addInstance(polygonGeometryId);
1780
+ }
1781
+ if (instanceId === void 0) continue;
1782
+ this.registerDefObject(shapeDef, batchedMesh, instanceId);
1783
+ }
1784
+ return batchedMesh;
1785
+ }
1786
+ normalizeColor(color) {
1787
+ return typeof color === "string" ? this.toRgbConverter(parse(color)) : this.toRgbConverter(parse(`#${color.toString(16).padStart(6, "0")}`));
1788
+ }
1789
+ buildPolygonGeometry(polygon) {
1790
+ const geometry = new BufferGeometry().setFromPoints(polygon.vertices).setIndex(polygon.indices.flat());
1791
+ return geometry;
1792
+ }
1793
+ sortInstances(mesh, list) {
1794
+ const shapeDefs = this.getDefsByObject(mesh);
1795
+ list.sort((a, b) => {
1796
+ const aDef = shapeDefs[a.index];
1797
+ const bDef = shapeDefs[b.index];
1798
+ const aDim = aDef.dim === false ? 1 : 0;
1799
+ const bDim = bDef.dim === false ? 1 : 0;
1800
+ return aDim - bDim;
1801
+ });
1802
+ }
1803
+ }
1804
+ class TextSystem extends RenderableSystem {
1805
+ /**
1806
+ * @param materialSystem {@link MaterialSystem}
1807
+ * @param renderer {@link Renderer}
1808
+ */
1809
+ constructor(materialSystem, renderer) {
1810
+ super("text", renderer);
1811
+ __publicField(this, "initialTextScale", new Vector2(1, -1));
1812
+ __publicField(this, "textColor", new Color());
1813
+ __publicField(this, "pendingUpdates", /* @__PURE__ */ new Map());
1814
+ __publicField(this, "alignmentOffset", new Vector2());
1815
+ __publicField(this, "alignmentDirection", new Vector2());
1816
+ __publicField(this, "localPosition", new Vector2());
1817
+ __publicField(this, "worldPosition", new Vector2());
1818
+ __publicField(this, "textScale", new Vector2());
1819
+ __publicField(this, "localToMin", new Vector2());
1820
+ __publicField(this, "localToMax", new Vector2());
1821
+ this.materialSystem = materialSystem;
1822
+ }
1823
+ buildLayer(layer) {
1824
+ const group = new Group();
1825
+ const batchedText = this.buildBatchedText(layer);
1826
+ batchedText.sync();
1827
+ group.add(batchedText);
1828
+ return group;
1829
+ }
1830
+ updateLayerImpl(group, layerDef) {
1831
+ const pendingText = this.pendingUpdates.get(group);
1832
+ if (pendingText) {
1833
+ this.pendingUpdates.delete(group);
1834
+ this.unregisterObject(pendingText);
1835
+ group.remove(pendingText);
1836
+ this.disposeObject(pendingText);
1837
+ }
1838
+ const currentText = group.children[0];
1839
+ if (currentText) {
1840
+ this.unregisterObject(currentText);
1841
+ }
1842
+ const newBatchedText = this.buildBatchedText(layerDef);
1843
+ newBatchedText.visible = false;
1844
+ this.pendingUpdates.set(group, newBatchedText);
1845
+ newBatchedText.sync(() => {
1846
+ const currentPendingText = this.pendingUpdates.get(group);
1847
+ if (currentPendingText === newBatchedText) {
1848
+ this.pendingUpdates.delete(group);
1849
+ if (currentText && group.children.includes(currentText)) {
1850
+ group.remove(currentText);
1851
+ this.disposeObject(currentText);
1852
+ }
1853
+ newBatchedText.visible = true;
1854
+ } else {
1855
+ this.unregisterObject(newBatchedText);
1856
+ group.remove(newBatchedText);
1857
+ this.disposeObject(newBatchedText);
1858
+ }
1859
+ });
1860
+ group.add(newBatchedText);
1861
+ }
1862
+ updateDefImpl(textDef, mesh, instanceIds) {
1863
+ for (const [i, line] of textDef.lines.entries()) {
1864
+ const text = mesh.getText(instanceIds[i]);
1865
+ if (text) {
1866
+ text.text = line.text;
1867
+ text.color = this.textColor.set(line.color).getHex(LinearSRGBColorSpace);
1868
+ text.font = line.fontUrl;
1869
+ if (line.stroke) {
1870
+ text.outlineColor = line.stroke.color;
1871
+ text.outlineWidth = line.stroke.width / line.fontSize;
1872
+ }
1873
+ }
1874
+ }
1875
+ this.updateScale(textDef);
1876
+ }
1877
+ buildBatchedText(layer) {
1878
+ const textDefs = layer.children;
1879
+ const batchedText = new BatchedText();
1880
+ batchedText.material = this.materialSystem.createColorMaterial();
1881
+ const mappingData = [];
1882
+ let instanceId = 0;
1883
+ for (const textDef of textDefs) {
1884
+ const instanceIds = [];
1885
+ for (const _ of textDef.lines) {
1886
+ const text = new Text();
1887
+ text.fontSize = 1;
1888
+ text.lineHeight = 1.1;
1889
+ text.whiteSpace = "nowrap";
1890
+ batchedText.addText(text, instanceId);
1891
+ instanceIds.push(instanceId);
1892
+ instanceId++;
1893
+ }
1894
+ mappingData.push({ textDef, instanceIds });
1895
+ }
1896
+ addDim(batchedText);
1897
+ for (const { textDef, instanceIds } of mappingData) {
1898
+ this.registerDefObject(textDef, batchedText, instanceIds);
1899
+ }
1900
+ batchedText.addEventListener("synccomplete", () => this.renderer.update());
1901
+ return batchedText;
1902
+ }
1903
+ // TODO: Simplify
1904
+ updateScale(textDef) {
1905
+ const dpr = this.renderer.context.getPixelRatio();
1906
+ const lines = this.getTextLines(textDef);
1907
+ this.calculateStartInBoundsPosition(
1908
+ textDef,
1909
+ lines,
1910
+ this.alignmentDirection,
1911
+ this.alignmentOffset,
1912
+ this.localPosition
1913
+ );
1914
+ for (const { text, fontSize, height, alignment } of lines) {
1915
+ if (!fontSize || !height) {
1916
+ text.visible = false;
1917
+ continue;
1918
+ }
1919
+ text.visible = true;
1920
+ setAnchorsAndAlignment(text, alignment);
1921
+ this.worldPosition.copy(this.localPosition).rotateAround({ x: 0, y: 0 }, textDef.bounds.rotation).add(textDef.bounds.center);
1922
+ this.textScale.copy(this.initialTextScale).multiplyScalar(fontSize * dpr);
1923
+ text.scale.set(this.textScale.x, this.textScale.y, 1);
1924
+ text.position.set(this.worldPosition.x, this.worldPosition.y, 0);
1925
+ text.rotation.set(0, 0, textDef.bounds.rotation);
1926
+ text.clipRect = this.calculateClipRect(
1927
+ textDef,
1928
+ this.localPosition,
1929
+ this.textScale,
1930
+ this.localToMin,
1931
+ this.localToMax
1932
+ );
1933
+ this.localPosition.y += height * dpr;
1934
+ }
1935
+ }
1936
+ getTextLines(textDef) {
1937
+ const { object: mesh, instanceIds } = this.getObjectInstanceByDef(textDef);
1938
+ const alignment = textDef.alignment;
1939
+ const lines = instanceIds.map((instanceId, i) => {
1940
+ const text = mesh.getText(instanceId);
1941
+ const line = textDef.lines[i];
1942
+ const fontSize = line.fontSize;
1943
+ const height = fontSize ? text.text.split("\n").length * text.lineHeight * fontSize : 0;
1944
+ return { text, fontSize, height, alignment };
1945
+ });
1946
+ if (alignment.vertical === "bottom") lines.reverse();
1947
+ return lines;
1948
+ }
1949
+ calculateStartInBoundsPosition(textDef, lines, alignmentDirection, alignmentOffset, inBoundsPosition) {
1950
+ const [w, h] = textDef.bounds.size;
1951
+ const padding = textDef.padding;
1952
+ const alignment = textDef.alignment;
1953
+ alignmentDirection.set(...getAlignmentDirection(alignment));
1954
+ inBoundsPosition.set(w / 2, h / 2);
1955
+ if (alignment.vertical === "center") {
1956
+ const totalTextHeight = lines.filter((l) => l.height !== void 0).reduce((acc, l) => acc + l.height, 0) * this.renderer.context.getPixelRatio();
1957
+ alignmentOffset.set(0, -(textDef.bounds.size.y - totalTextHeight) / 2);
1958
+ inBoundsPosition.add(alignmentOffset);
1959
+ } else {
1960
+ inBoundsPosition.sub({ x: padding[0], y: padding[1] });
1961
+ }
1962
+ inBoundsPosition.multiply(alignmentDirection);
1963
+ }
1964
+ calculateClipRect(textDef, inBoundsPosition, textScale, toMin, toMax) {
1965
+ toMin.subVectors(textDef.bounds.min, textDef.bounds.center).multiply(this.initialTextScale).sub(inBoundsPosition).divide(textScale);
1966
+ toMax.subVectors(textDef.bounds.max, textDef.bounds.center).multiply(this.initialTextScale).sub(inBoundsPosition).divide(textScale);
1967
+ return [toMin.x, toMin.y, toMax.x, toMax.y];
1968
+ }
1969
+ }
1970
+ function getAlignmentDirection(alignment) {
1971
+ const horizontalFactors = {
1972
+ left: -1,
1973
+ center: 0,
1974
+ right: 1
1975
+ };
1976
+ const verticalFactors = {
1977
+ top: -1,
1978
+ center: -1,
1979
+ bottom: 1
1980
+ };
1981
+ return [horizontalFactors[alignment.horizontal], verticalFactors[alignment.vertical]];
1982
+ }
1983
+ function setAnchorsAndAlignment(text, alignment) {
1984
+ text.anchorX = alignment.horizontal;
1985
+ text.anchorY = alignment.vertical === "bottom" ? "bottom" : "top";
1986
+ text.textAlign = alignment.text ?? alignment.horizontal;
1987
+ }
1988
+ class LayerSystem {
1989
+ /**
1990
+ * @param renderer {@link Renderer}
1991
+ */
1992
+ constructor(renderer) {
1993
+ __publicField(this, "materialSystem");
1994
+ __publicField(this, "meshSystem");
1995
+ __publicField(this, "imageSystem");
1996
+ __publicField(this, "textSystem");
1997
+ __publicField(this, "lineSystem");
1998
+ __publicField(this, "systems");
1999
+ __publicField(this, "mapLayerDefsToObjects", /* @__PURE__ */ new Map());
2000
+ __publicField(this, "mapLayerDefToParent", /* @__PURE__ */ new Map());
2001
+ __publicField(this, "layerDefRenderOrder", []);
2002
+ __publicField(this, "pendingDefs", /* @__PURE__ */ new Set());
2003
+ __publicField(this, "useUpdateBuffering", false);
2004
+ this.renderer = renderer;
2005
+ this.materialSystem = new MaterialSystem();
2006
+ this.meshSystem = new MeshSystem(this.materialSystem, this.renderer);
2007
+ this.imageSystem = new ImageSystem(this.materialSystem, this.renderer);
2008
+ this.textSystem = new TextSystem(this.materialSystem, this.renderer);
2009
+ this.lineSystem = new LineSystem(this.materialSystem, this.renderer);
2010
+ this.systems = [this.meshSystem, this.imageSystem, this.textSystem, this.lineSystem];
2011
+ }
2012
+ /**
2013
+ * Get all defs that are intersected by the given raycast results.
2014
+ * @param intersections {@link Intersection} array
2015
+ * @returns Array of defs that are intersected
2016
+ */
2017
+ getIntersectedDefs(intersections) {
2018
+ const defs = [];
2019
+ for (const intersection of intersections.sort((a, b) => b.object.renderOrder - a.object.renderOrder)) {
2020
+ for (const system of this.systems) {
2021
+ const def = system.getIntersectedDef(intersection);
2022
+ if (def) {
2023
+ defs.push(def);
2024
+ break;
2025
+ }
2026
+ }
2027
+ }
2028
+ return defs;
2029
+ }
2030
+ /**
2031
+ * Update the given defs immediately, or queue them for update if update buffering is enabled.
2032
+ * @param defs {@link RenderableDef} array
2033
+ */
2034
+ updateDefs(defs) {
2035
+ for (const def of defs) {
2036
+ if (this.useUpdateBuffering) this.pendingDefs.add(def);
2037
+ else this.updateDef(def);
2038
+ }
2039
+ }
2040
+ /**
2041
+ * Drain the queued updates within a time budget.
2042
+ * Returns true if any def was updated during this call.
2043
+ * @param timeBudgetMs frame time budget to perform updates in milliseconds
2044
+ * @returns true if any def was updated during this call
2045
+ */
2046
+ processPendingUpdates(timeBudgetMs = 5) {
2047
+ if (!this.useUpdateBuffering) return false;
2048
+ if (this.pendingDefs.size === 0) return false;
2049
+ const startTime = performance.now();
2050
+ let processed = 0;
2051
+ while (this.pendingDefs.size && performance.now() - startTime < timeBudgetMs) {
2052
+ const def = this.pendingDefs.values().next().value;
2053
+ this.pendingDefs.delete(def);
2054
+ this.updateDef(def);
2055
+ processed++;
2056
+ }
2057
+ const took = performance.now() - startTime;
2058
+ if (processed && this.renderer.debugLog) {
2059
+ console.log(
2060
+ `LayerSystem: processed ${processed} defs in ${took.toFixed(2)}ms, ${this.pendingDefs.size} remaining`
2061
+ );
2062
+ }
2063
+ return processed > 0;
2064
+ }
2065
+ /**
2066
+ * Build the scene graph from the given scene definition.
2067
+ * @param sceneDef {@link SceneDef}
2068
+ * @returns root {@link Group} of the scene graph
2069
+ */
2070
+ buildScene(sceneDef) {
2071
+ this.initRenderOrder(sceneDef.rootLayer);
2072
+ if (this.renderer.debugLog)
2073
+ console.log(
2074
+ "Render order",
2075
+ this.layerDefRenderOrder.map((layer) => this.getFullLayerName(layer))
2076
+ );
2077
+ const rootGroup = new Group();
2078
+ rootGroup.name = sceneDef.rootLayer.name;
2079
+ this.mapLayerDefsToObjects.set(sceneDef.rootLayer, rootGroup);
2080
+ if (sceneDef.background) rootGroup.add(this.createBackgroundLayer(sceneDef.background));
2081
+ for (const child of sceneDef.rootLayer.children) {
2082
+ rootGroup.add(this.buildLayer(child));
2083
+ }
2084
+ this.updateLayer(sceneDef.rootLayer, false);
2085
+ if (this.renderer.debugLog) printTree(rootGroup);
2086
+ if (sceneDef.memoryLimit) {
2087
+ this.imageSystem.memoryLimitMb = sceneDef.memoryLimit;
2088
+ this.imageSystem.resizeTextures();
2089
+ }
2090
+ return rootGroup;
2091
+ }
2092
+ updateDef(def) {
2093
+ if (isShapeDef(def)) this.meshSystem.updateDef(def);
2094
+ else if (isImageDef(def)) this.imageSystem.updateDef(def);
2095
+ else if (isTextDef(def)) this.textSystem.updateDef(def);
2096
+ else if (isLineDef(def)) this.lineSystem.updateDef(def);
2097
+ else this.updateLayer(def, true);
2098
+ }
2099
+ updateLayer(layerDef, updateChildren) {
2100
+ const layerObject = this.mapLayerDefsToObjects.get(layerDef);
2101
+ if (!layerObject) return;
2102
+ if (updateChildren) {
2103
+ const group = layerObject;
2104
+ if (isImageLayer(layerDef)) this.imageSystem.updateLayer(group, layerDef);
2105
+ else if (isLineLayer(layerDef)) this.lineSystem.updateLayer(group, layerDef);
2106
+ else if (isTextLayer(layerDef)) this.textSystem.updateLayer(group, layerDef);
2107
+ else if (isShapeLayer(layerDef)) this.meshSystem.updateLayer(group, layerDef);
2108
+ this.setLayerName(group, group.name);
2109
+ }
2110
+ layerObject.visible = !layerDef.hidden && layerDef.children.length > 0;
2111
+ if (layerDef.interactive !== void 0) setInteractive(layerObject, layerDef.interactive);
2112
+ setDimming(layerObject, layerDef.dim);
2113
+ this.setRenderOrder(layerObject, layerDef);
2114
+ }
2115
+ buildLayer(layerDef, parentPrefix = "") {
2116
+ const layerFullName = parentPrefix + layerDef.name;
2117
+ console.log(`Building layer ${layerFullName}...`);
2118
+ let layerObject;
2119
+ if (isShapeLayer(layerDef)) layerObject = this.meshSystem.buildLayer(layerDef);
2120
+ else if (isImageLayer(layerDef)) layerObject = this.imageSystem.buildLayer(layerDef);
2121
+ else if (isTextLayer(layerDef)) layerObject = this.textSystem.buildLayer(layerDef);
2122
+ else if (isLineLayer(layerDef)) layerObject = this.lineSystem.buildLayer(layerDef);
2123
+ else {
2124
+ layerObject = new Group();
2125
+ layerDef.children.map((layer) => this.buildLayer(layer, parentPrefix + `${layerDef.name}:`)).forEach((g) => layerObject.add(g));
2126
+ }
2127
+ this.setLayerName(layerObject, layerFullName);
2128
+ this.mapLayerDefsToObjects.set(layerDef, layerObject);
2129
+ this.updateLayer(layerDef, false);
2130
+ return layerObject;
2131
+ }
2132
+ setLayerName(object, layerFullName) {
2133
+ object.name = layerFullName;
2134
+ for (const child of object.children) {
2135
+ if (child.isGroup) continue;
2136
+ const childName = child.name ? `${layerFullName}:${child.name}` : layerFullName;
2137
+ child.name = childName;
2138
+ }
2139
+ }
2140
+ setRenderOrder(object, layer) {
2141
+ const renderOrder = this.layerDefRenderOrder.indexOf(layer) + 1;
2142
+ if (renderOrder == 0) return;
2143
+ if (object.isGroup) {
2144
+ object.children.forEach((child) => this.setRenderOrder(child, layer));
2145
+ return;
2146
+ }
2147
+ object.renderOrder = renderOrder;
2148
+ }
2149
+ createBackgroundLayer(color) {
2150
+ const backgroundGeometry = new PlaneGeometry(2, 2);
2151
+ const backgroundMaterial = this.materialSystem.createBackgroundMaterial(color);
2152
+ const backgroundMesh = new Mesh(backgroundGeometry, backgroundMaterial);
2153
+ backgroundMesh.frustumCulled = false;
2154
+ backgroundMesh.renderOrder = 0;
2155
+ backgroundMesh.name = "background";
2156
+ return backgroundMesh;
2157
+ }
2158
+ initRenderOrder(rootLayer) {
2159
+ const stack = [rootLayer];
2160
+ while (stack.length) {
2161
+ const layer = stack.pop();
2162
+ if (isLayerLayer(layer)) {
2163
+ const children = [...layer.children];
2164
+ children.reverse();
2165
+ for (const child of children) {
2166
+ this.mapLayerDefToParent.set(child, layer);
2167
+ stack.push(child);
2168
+ }
2169
+ } else {
2170
+ this.layerDefRenderOrder.push(layer);
2171
+ }
2172
+ }
2173
+ }
2174
+ getFullLayerName(layerDef) {
2175
+ let fullName = layerDef.name;
2176
+ let parent = this.mapLayerDefToParent.get(layerDef);
2177
+ while (parent) {
2178
+ fullName = parent.name + ":" + fullName;
2179
+ parent = this.mapLayerDefToParent.get(parent);
2180
+ }
2181
+ return fullName;
2182
+ }
2183
+ }
2184
+ const subsetOfTHREE = {
2185
+ Vector2,
2186
+ Vector3,
2187
+ Vector4,
2188
+ Quaternion,
2189
+ Matrix4,
2190
+ Spherical,
2191
+ Box3,
2192
+ Sphere,
2193
+ Raycaster
2194
+ };
2195
+ CameraController.install({ THREE: subsetOfTHREE });
2196
+ class CameraSystem {
2197
+ /**
2198
+ * @param renderer {@link Renderer} instance
2199
+ */
2200
+ constructor(renderer) {
2201
+ /** {@link CameraController} instance. Used to smoothly animate the camera. */
2202
+ __publicField(this, "controller");
2203
+ __publicField(this, "camera");
2204
+ __publicField(this, "externalCamera");
2205
+ __publicField(this, "zoomIdentityDistance");
2206
+ __publicField(this, "zoomBounds");
2207
+ this.renderer = renderer;
2208
+ const [w, h] = renderer.size;
2209
+ this.camera = new PerspectiveCamera(90, w / (h || 1));
2210
+ this.camera.up.set(0, 0, -1);
2211
+ this.zoomIdentityDistance = h / 2;
2212
+ this.camera.position.y = this.zoomIdentityDistance;
2213
+ this.controller = new CameraController(this.camera);
2214
+ void this.controller.rotatePolarTo(0);
2215
+ }
2216
+ /** Current camera instance. */
2217
+ get currentCamera() {
2218
+ return this.externalCamera ?? this.camera;
2219
+ }
2220
+ /** Current camera zoom factor. */
2221
+ get zoomFactor() {
2222
+ return this.camera.position.z ? this.zoomIdentityDistance / this.camera.position.z : 1;
2223
+ }
2224
+ /**
2225
+ * Initializes the camera with the given zoom bounds.
2226
+ * @param zoomBounds [minZoom, maxZoom]
2227
+ */
2228
+ initCamera(zoomBounds) {
2229
+ this.zoomBounds = zoomBounds;
2230
+ this.updateCamera();
2231
+ }
2232
+ /** Updates the camera when the renderer size changes. */
2233
+ updateCamera() {
2234
+ if (!this.zoomBounds) return;
2235
+ const [w, h] = this.renderer.size;
2236
+ this.zoomIdentityDistance = -h / 2;
2237
+ const maxDistance = Math.abs(this.zoomIdentityDistance / this.zoomBounds[0]);
2238
+ const minDistance = Math.abs(this.zoomIdentityDistance / this.zoomBounds[1]);
2239
+ this.camera.aspect = w / (h || 1);
2240
+ this.camera.far = maxDistance + 1;
2241
+ this.camera.near = minDistance - 1;
2242
+ this.camera.updateProjectionMatrix();
2243
+ }
2244
+ /**
2245
+ * Calculates the camera distance from the scene's plane for a given zoom factor.
2246
+ * @param zoomFactor Zoom factor
2247
+ * @returns Corresponding camera distance on the Z axis
2248
+ */
2249
+ zoomFactorToDistance(zoomFactor) {
2250
+ return zoomFactor > 0 ? Math.abs(this.zoomIdentityDistance / zoomFactor) : this.zoomIdentityDistance;
2251
+ }
2252
+ syncController(minDistance, maxDistance, zoomFactor) {
2253
+ if (this.renderer.debugLog) console.log("syncController", minDistance, maxDistance, zoomFactor);
2254
+ this.controller.minDistance = minDistance;
2255
+ this.controller.maxDistance = maxDistance;
2256
+ void this.controller.setLookAt(
2257
+ this.camera.position.x,
2258
+ this.camera.position.y,
2259
+ -this.zoomFactorToDistance(zoomFactor),
2260
+ this.camera.position.x,
2261
+ this.camera.position.y,
2262
+ 0,
2263
+ false
2264
+ );
2265
+ }
2266
+ }
2267
+ class SceneSystem {
2268
+ /**
2269
+ * @param renderer {@link Renderer} instance
2270
+ */
2271
+ constructor(renderer) {
2272
+ /** {@link Scene} instance */
2273
+ __publicField(this, "scene");
2274
+ /** World matrix - SVG → World transform */
2275
+ __publicField(this, "worldMatrix", new Matrix4());
2276
+ /** Inverse world matrix - World → SVG transform */
2277
+ __publicField(this, "inverseWorldMatrix", new Matrix4());
2278
+ __publicField(this, "translationMatrix", new Matrix4());
2279
+ __publicField(this, "scaleMatrix", new Matrix4());
2280
+ __publicField(this, "visibleRectOffsetMatrix", new Matrix4());
2281
+ __publicField(this, "viewbox");
2282
+ this.renderer = renderer;
2283
+ this.scene = new Scene();
2284
+ this.scene.matrixAutoUpdate = false;
2285
+ }
2286
+ /**
2287
+ * Initializes the scene with the given SVG viewbox.
2288
+ * @param viewbox {@link Rect} viewbox
2289
+ */
2290
+ initScene(viewbox) {
2291
+ this.viewbox = viewbox;
2292
+ this.updateScene();
2293
+ }
2294
+ /**
2295
+ * Updates the scene transform when the renderer size changes.
2296
+ */
2297
+ updateScene() {
2298
+ if (!this.viewbox) return;
2299
+ const dpr = this.renderer.context.getPixelRatio();
2300
+ const visibleRect = this.renderer.visibleRect;
2301
+ const [viewBoxWidth, viewBoxHeight] = this.viewbox.size;
2302
+ const [visibleRectWidth, visibleRectHeight] = (visibleRect == null ? void 0 : visibleRect.size.clone().multiplyScalar(dpr)) ?? this.renderer.size;
2303
+ const scaleFactor = Math.min(visibleRectWidth / viewBoxWidth, visibleRectHeight / viewBoxHeight);
2304
+ const [centerX, centerY] = this.viewbox.center;
2305
+ this.translationMatrix.makeTranslation(-centerX, -centerY, 0);
2306
+ this.scaleMatrix.makeScale(scaleFactor, scaleFactor, 1);
2307
+ if (visibleRect) {
2308
+ const visibleRectCenter = visibleRect.center.clone().multiplyScalar(dpr);
2309
+ const canvasCenter = new Vector2(...this.renderer.size).multiplyScalar(0.5);
2310
+ const offset = visibleRectCenter.sub(canvasCenter);
2311
+ this.visibleRectOffsetMatrix.makeTranslation(offset.x, offset.y, 0);
2312
+ }
2313
+ this.composeMatrices();
2314
+ }
2315
+ composeMatrices() {
2316
+ this.worldMatrix.copy(this.translationMatrix).premultiply(this.scaleMatrix);
2317
+ if (this.renderer.visibleRect) this.worldMatrix.premultiply(this.visibleRectOffsetMatrix);
2318
+ this.scene.matrix.copy(this.worldMatrix);
2319
+ this.inverseWorldMatrix.copy(this.scene.matrix).invert();
2320
+ this.scene.matrixWorldNeedsUpdate = true;
2321
+ }
2322
+ }
2323
+ class ViewportSystem {
2324
+ /**
2325
+ * @param renderer {@link Renderer} instance
2326
+ * @param eventSystem {@link EventSystem} instance
2327
+ */
2328
+ constructor(renderer, eventSystem) {
2329
+ __publicField(this, "sceneSystem");
2330
+ __publicField(this, "cameraSystem");
2331
+ __publicField(this, "raycaster", new Raycaster());
2332
+ __publicField(this, "intersectionPoint", new Vector3());
2333
+ __publicField(this, "viewboxPlane", new Plane(new Vector3(0, 0, 1), 0));
2334
+ __publicField(this, "pxToSvgScaleThreshold", 1e-3);
2335
+ __publicField(this, "prevPxToSvgScale");
2336
+ this.renderer = renderer;
2337
+ this.eventSystem = eventSystem;
2338
+ this.sceneSystem = new SceneSystem(renderer);
2339
+ this.cameraSystem = new CameraSystem(renderer);
2340
+ this.raycaster.layers.set(INTERACTIVE_LAYER);
2341
+ }
2342
+ /** {@link Scene} instance */
2343
+ get scene() {
2344
+ return this.sceneSystem.scene;
2345
+ }
2346
+ /** Current {@link Camera} instance */
2347
+ get camera() {
2348
+ return this.cameraSystem.currentCamera;
2349
+ }
2350
+ /** {@link CameraController} instance */
2351
+ get cameraController() {
2352
+ return this.cameraSystem.controller;
2353
+ }
2354
+ /**
2355
+ * Initializes the viewport and zoom bounds with the given scene definition.
2356
+ * @param sceneDef {@link SceneDef} scene definition
2357
+ */
2358
+ initViewport(sceneDef) {
2359
+ this.sceneSystem.initScene(sceneDef.viewbox);
2360
+ this.cameraSystem.initCamera([0.1, sceneDef.viewbox.size.width > 1e5 ? 100 : 35]);
2361
+ }
2362
+ /** Updates the viewport when the renderer size changes. */
2363
+ updateViewport() {
2364
+ this.cameraSystem.updateCamera();
2365
+ this.sceneSystem.updateScene();
2366
+ }
2367
+ /**
2368
+ * Recalculates the svg to pixel scale factor and emits the event if necessary.
2369
+ */
2370
+ updatePtScale() {
2371
+ const scaleVector = new Vector3();
2372
+ scaleVector.setFromMatrixScale(this.scene.matrix);
2373
+ let scaleFactor;
2374
+ if (scaleVector.z === 1) {
2375
+ scaleFactor = scaleVector.x;
2376
+ } else {
2377
+ const perspectiveW = this.scene.matrix.elements[15];
2378
+ const halfViewportWidth = this.renderer.size[0] / 2;
2379
+ scaleFactor = halfViewportWidth * scaleVector.x / perspectiveW;
2380
+ }
2381
+ const denominator = scaleFactor * this.cameraSystem.zoomFactor;
2382
+ const pxToSvgScale = 1 / denominator;
2383
+ if (Math.abs(pxToSvgScale - (this.prevPxToSvgScale ?? 0)) < this.pxToSvgScaleThreshold) return;
2384
+ if (this.renderer.debugLog) console.log("pxToSvgScale", +pxToSvgScale.toFixed(3));
2385
+ this.eventSystem.emit("viewport:ptscale", pxToSvgScale);
2386
+ this.prevPxToSvgScale = pxToSvgScale;
2387
+ }
2388
+ /**
2389
+ * Gets the objects intersected by the raycaster.
2390
+ * @param normalizedCoords raycast point in NDC (normalized device coordinates
2391
+ * @returns Array of {@link Intersection} instances
2392
+ */
2393
+ getIntersectedObjects(normalizedCoords) {
2394
+ const { scene, camera } = this;
2395
+ this.raycaster.setFromCamera(normalizedCoords, camera);
2396
+ const intersections = this.raycaster.intersectObject(scene, true).filter((i) => isVisible(i.object));
2397
+ return intersections;
2398
+ }
2399
+ /**
2400
+ * Converts a point from screen coordinates to the given coordinate space.
2401
+ * @param space Space to convert to (either "svg" or "world")
2402
+ * @param normalizedCoords Point in NDC (normalized device coordinates)
2403
+ * @returns Point in the given space
2404
+ */
2405
+ screenTo(space, normalizedCoords) {
2406
+ this.raycaster.setFromCamera(normalizedCoords, this.camera);
2407
+ this.raycaster.ray.intersectPlane(this.viewboxPlane, this.intersectionPoint);
2408
+ if (space === "svg") this.intersectionPoint.applyMatrix4(this.sceneSystem.inverseWorldMatrix);
2409
+ return { x: this.intersectionPoint.x, y: this.intersectionPoint.y };
2410
+ }
2411
+ }
2412
+ class EventSystem {
2413
+ constructor() {
2414
+ // Store handlers opaquely to keep implementation simple and avoid 'any'.
2415
+ __publicField(this, "handlers", /* @__PURE__ */ new Map());
2416
+ }
2417
+ /**
2418
+ * Add an event listener for a specific event
2419
+ * @param event - The event to listen for
2420
+ * @param handler - The handler to call when the event is emitted
2421
+ */
2422
+ addEventListener(event, handler) {
2423
+ let set = this.handlers.get(event);
2424
+ if (!set) {
2425
+ set = /* @__PURE__ */ new Set();
2426
+ this.handlers.set(event, set);
2427
+ }
2428
+ set.add(handler);
2429
+ }
2430
+ /**
2431
+ * Remove an event listener for a specific event
2432
+ * @param event - The event to remove the listener for
2433
+ * @param handler - The handler to remove
2434
+ */
2435
+ removeEventListener(event, handler) {
2436
+ const set = this.handlers.get(event);
2437
+ if (!set) return;
2438
+ set.delete(handler);
2439
+ if (set.size === 0) this.handlers.delete(event);
2440
+ }
2441
+ /**
2442
+ * Check if there are any listeners for a specific event
2443
+ * @param event - The event to check
2444
+ * @returns true if there are listeners, false otherwise
2445
+ */
2446
+ hasListeners(event) {
2447
+ const set = this.handlers.get(event);
2448
+ return !!set && set.size > 0;
2449
+ }
2450
+ /**
2451
+ * Emit an event with a specific payload
2452
+ * @param event - The event to emit
2453
+ * @param args - The payload to emit
2454
+ */
2455
+ emit(event, ...args) {
2456
+ const set = this.handlers.get(event);
2457
+ if (!set) return;
2458
+ for (const fn of Array.from(set)) {
2459
+ if (args.length === 0) {
2460
+ fn();
2461
+ } else {
2462
+ fn(args[0]);
2463
+ }
2464
+ }
2465
+ }
2466
+ /**
2467
+ * Clear all event listeners
2468
+ */
2469
+ clear() {
2470
+ this.handlers.clear();
2471
+ }
2472
+ }
2473
+ function asEventAPI(system) {
2474
+ return {
2475
+ addEventListener: system.addEventListener.bind(system),
2476
+ removeEventListener: system.removeEventListener.bind(system),
2477
+ hasListeners: system.hasListeners.bind(system),
2478
+ clear: system.clear.bind(system)
2479
+ };
2480
+ }
2481
+ function normalizeEventCoordinates(event, domElement, target) {
2482
+ const { left, top } = domElement.getBoundingClientRect();
2483
+ return canvasToNDC({ x: event.clientX - left, y: event.clientY - top }, domElement, target);
2484
+ }
2485
+ function canvasToNDC(coordinates, domElement, target) {
2486
+ target = target ?? new Vector2();
2487
+ const { width, height } = domElement.getBoundingClientRect();
2488
+ const uv = [coordinates.x / width, coordinates.y / height];
2489
+ target.set(uv[0] * 2 - 1, -uv[1] * 2 + 1);
2490
+ return target;
2491
+ }
2492
+ class Handler {
2493
+ /**
2494
+ * @param controller The camera-controls instance for camera manipulation
2495
+ * @param domElement The DOM element to attach event listeners to
2496
+ * @param eventManager Shared Mjolnir EventManager for gesture recognition
2497
+ */
2498
+ constructor(controller, domElement, eventManager) {
2499
+ /** Whether this handler is enabled */
2500
+ __publicField(this, "enabled", false);
2501
+ this.controller = controller;
2502
+ this.domElement = domElement;
2503
+ this.eventManager = eventManager;
2504
+ }
2505
+ /**
2506
+ * Per-frame update for this handler.
2507
+ * Called every frame in the render loop.
2508
+ * Override if handler needs per-frame logic (e.g., smooth interpolation).
2509
+ * @param delta Time elapsed since last frame in seconds
2510
+ * @returns true if the handler made changes that require re-rendering
2511
+ */
2512
+ update(delta) {
2513
+ return false;
2514
+ }
2515
+ /**
2516
+ * Configure this handler with handler-specific options.
2517
+ * Does NOT handle enabled state - use enable()/disable() for that.
2518
+ * Subclasses should override to handle their specific options
2519
+ * @param options Partial options to update (handler-specific, not including enabled flag)
2520
+ */
2521
+ configure(options) {
2522
+ }
2523
+ /**
2524
+ * Enable this handler.
2525
+ * Calls onEnable() hook for subclass-specific enabling logic
2526
+ */
2527
+ enable() {
2528
+ if (!this.enabled) {
2529
+ this.enabled = true;
2530
+ this.onEnable();
2531
+ }
2532
+ }
2533
+ /**
2534
+ * Disable this handler.
2535
+ * Calls onDisable() hook for subclass-specific disabling logic
2536
+ */
2537
+ disable() {
2538
+ if (this.enabled) {
2539
+ this.enabled = false;
2540
+ this.onDisable();
2541
+ }
2542
+ }
2543
+ /**
2544
+ * Check if this handler is currently enabled
2545
+ * @returns true if enabled, false otherwise
2546
+ */
2547
+ isEnabled() {
2548
+ return this.enabled;
2549
+ }
2550
+ }
2551
+ class PanHandler extends Handler {
2552
+ reset() {
2553
+ void this.controller.moveTo(0, 0, 0, true);
2554
+ }
2555
+ /**
2556
+ * Enable pan gestures.
2557
+ * Configures camera-controls to handle left-click drag and single-touch drag
2558
+ */
2559
+ onEnable() {
2560
+ this.controller.mouseButtons.left = CameraController.ACTION.TRUCK;
2561
+ this.controller.touches.one = CameraController.ACTION.TOUCH_TRUCK;
2562
+ }
2563
+ /**
2564
+ * Disable pan gestures.
2565
+ * Removes pan actions from camera-controls
2566
+ */
2567
+ onDisable() {
2568
+ this.controller.mouseButtons.left = CameraController.ACTION.NONE;
2569
+ this.controller.touches.one = CameraController.ACTION.NONE;
2570
+ }
2571
+ // FIXME: Add inertia
2572
+ }
2573
+ const ROTATION_THRESHOLD = 25;
2574
+ class RollHandler extends Handler {
2575
+ constructor() {
2576
+ super(...arguments);
2577
+ __publicField(this, "rotating", false);
2578
+ __publicField(this, "startVector");
2579
+ __publicField(this, "vector");
2580
+ __publicField(this, "minDiameter", 0);
2581
+ __publicField(this, "startBearing", 0);
2582
+ __publicField(this, "raycaster", new Raycaster());
2583
+ __publicField(this, "pivotWorld", new Vector3());
2584
+ __publicField(this, "tempVec2", new Vector2());
2585
+ __publicField(this, "tempVec3", new Vector3());
2586
+ __publicField(this, "onRotateStart", (event) => {
2587
+ console.log("RollHandler: rotatestart", event);
2588
+ if (!this.enabled) {
2589
+ console.log("RollHandler: not enabled");
2590
+ return;
2591
+ }
2592
+ const { pointers } = event;
2593
+ console.log("RollHandler: pointers", pointers == null ? void 0 : pointers.length);
2594
+ if (pointers.length !== 2) return;
2595
+ const p0 = new Vector2(pointers[0].offsetX, pointers[0].offsetY);
2596
+ const p1 = new Vector2(pointers[1].offsetX, pointers[1].offsetY);
2597
+ this.startVector = this.vector = p0.clone().sub(p1);
2598
+ this.minDiameter = p0.distanceTo(p1);
2599
+ this.startBearing = 0;
2600
+ this.rotating = false;
2601
+ console.log("RollHandler: initialized rotation, diameter:", this.minDiameter);
2602
+ });
2603
+ __publicField(this, "onRotate", (event) => {
2604
+ if (!this.enabled) return;
2605
+ const { pointers, rotation } = event;
2606
+ if (pointers.length !== 2 || !this.startVector) {
2607
+ console.log("RollHandler: rotate ignored", pointers == null ? void 0 : pointers.length, !!this.startVector);
2608
+ return;
2609
+ }
2610
+ const p0 = new Vector2(pointers[0].offsetX, pointers[0].offsetY);
2611
+ const p1 = new Vector2(pointers[1].offsetX, pointers[1].offsetY);
2612
+ this.vector = p0.clone().sub(p1);
2613
+ if (!this.rotating && this.isBelowThreshold(this.vector)) {
2614
+ console.log("RollHandler: below threshold");
2615
+ return;
2616
+ }
2617
+ if (!this.rotating) {
2618
+ this.rotating = true;
2619
+ this.startBearing = rotation;
2620
+ console.log("RollHandler: rotation started, bearing:", rotation);
2621
+ }
2622
+ const bearingDelta = -(rotation - this.startBearing);
2623
+ this.startBearing = rotation;
2624
+ if (Math.abs(bearingDelta) < 0.01) {
2625
+ console.log("RollHandler: delta too small:", bearingDelta);
2626
+ return;
2627
+ }
2628
+ console.log("RollHandler: rotating by", bearingDelta, "degrees");
2629
+ const midpointPx = p0.clone().add(p1).multiplyScalar(0.5);
2630
+ this.normalizeScreenCoords(midpointPx, this.tempVec2);
2631
+ this.unprojectToWorldPlane(this.tempVec2, this.pivotWorld);
2632
+ void this.controller.rotate(bearingDelta * DEG2RAD, 0, false);
2633
+ this.projectWorldToScreen(this.pivotWorld, this.tempVec2);
2634
+ const pivotNewPx = this.denormalizeScreenCoords(this.tempVec2, this.tempVec3);
2635
+ const screenDeltaX = midpointPx.x - pivotNewPx.x;
2636
+ const screenDeltaY = midpointPx.y - pivotNewPx.y;
2637
+ const worldPan = this.screenDeltaToWorldDelta(screenDeltaX, screenDeltaY);
2638
+ void this.controller.truck(worldPan.x, worldPan.y, false);
2639
+ });
2640
+ __publicField(this, "onRotateEnd", () => {
2641
+ this.rotating = false;
2642
+ this.startVector = void 0;
2643
+ this.vector = void 0;
2644
+ this.minDiameter = 0;
2645
+ });
2646
+ }
2647
+ reset() {
2648
+ void this.controller.rotateAzimuthTo(0, true);
2649
+ this.rotating = false;
2650
+ this.startVector = void 0;
2651
+ this.vector = void 0;
2652
+ this.minDiameter = 0;
2653
+ }
2654
+ /**
2655
+ * Enable roll gestures.
2656
+ * - Mobile: custom two-finger rotation with pivot compensation
2657
+ */
2658
+ onEnable() {
2659
+ this.controller.maxAzimuthAngle = Infinity;
2660
+ this.controller.minAzimuthAngle = -Infinity;
2661
+ this.eventManager.on("rotatestart", this.onRotateStart);
2662
+ this.eventManager.on("rotate", this.onRotate);
2663
+ this.eventManager.on("rotateend", this.onRotateEnd);
2664
+ }
2665
+ /**
2666
+ * Disable roll gestures.
2667
+ * Restricts azimuth angle to zero and removes event listeners
2668
+ */
2669
+ onDisable() {
2670
+ this.controller.maxAzimuthAngle = 0;
2671
+ this.controller.minAzimuthAngle = 0;
2672
+ this.eventManager.off("rotatestart", this.onRotateStart);
2673
+ this.eventManager.off("rotate", this.onRotate);
2674
+ this.eventManager.off("rotateend", this.onRotateEnd);
2675
+ this.rotating = false;
2676
+ }
2677
+ /**
2678
+ * Check if rotation is below threshold (Mapbox-style).
2679
+ * Threshold is in pixels along circumference, scaled by touch circle diameter.
2680
+ * @param vector Current vector between fingers
2681
+ * @returns true if below threshold, false otherwise
2682
+ */
2683
+ isBelowThreshold(vector) {
2684
+ this.minDiameter = Math.min(this.minDiameter, vector.length());
2685
+ const circumference = Math.PI * this.minDiameter;
2686
+ const thresholdDegrees = ROTATION_THRESHOLD / circumference * 360;
2687
+ if (!this.startVector) return true;
2688
+ const bearingDeltaSinceStart = this.getBearingDelta(vector, this.startVector);
2689
+ return Math.abs(bearingDeltaSinceStart) < thresholdDegrees;
2690
+ }
2691
+ /**
2692
+ * Get signed angle between two vectors in degrees
2693
+ * @param a First vector
2694
+ * @param b Second vector
2695
+ * @returns Angle difference in degrees
2696
+ */
2697
+ getBearingDelta(a, b) {
2698
+ return a.angle() - b.angle();
2699
+ }
2700
+ /**
2701
+ * Normalize screen pixel coordinates to NDC [-1, 1]
2702
+ * @param screenPos Screen position in pixels
2703
+ * @param out Output vector for NDC coordinates
2704
+ * @returns NDC coordinates
2705
+ */
2706
+ normalizeScreenCoords(screenPos, out) {
2707
+ const canvas = this.domElement;
2708
+ return out.set(screenPos.x / canvas.width * 2 - 1, -(screenPos.y / canvas.height) * 2 + 1);
2709
+ }
2710
+ /**
2711
+ * Denormalize NDC coordinates to screen pixels
2712
+ * @param ndc NDC coordinates [-1, 1]
2713
+ * @param out Output vector for screen pixel coordinates
2714
+ * @returns Screen pixel coordinates
2715
+ */
2716
+ denormalizeScreenCoords(ndc, out) {
2717
+ const canvas = this.domElement;
2718
+ return out.set((ndc.x + 1) * 0.5 * canvas.width, (1 - ndc.y) * 0.5 * canvas.height, 0);
2719
+ }
2720
+ /**
2721
+ * Unproject screen NDC coordinates to world plane (Z=0)
2722
+ * @param ndc NDC coordinates [-1, 1]
2723
+ * @param out Output vector for world coordinates
2724
+ * @returns World coordinates on Z=0 plane
2725
+ */
2726
+ unprojectToWorldPlane(ndc, out) {
2727
+ const camera = this.controller.camera;
2728
+ this.raycaster.setFromCamera(ndc, camera);
2729
+ const ray = this.raycaster.ray;
2730
+ const t = -ray.origin.z / ray.direction.z;
2731
+ return out.copy(ray.origin).addScaledVector(ray.direction, t);
2732
+ }
2733
+ /**
2734
+ * Project world point to screen NDC coordinates
2735
+ * @param worldPos World position
2736
+ * @param out Output vector for NDC coordinates
2737
+ * @returns NDC coordinates
2738
+ */
2739
+ projectWorldToScreen(worldPos, out) {
2740
+ const camera = this.controller.camera;
2741
+ const projected = this.tempVec3.copy(worldPos).project(camera);
2742
+ return out.set(projected.x, projected.y);
2743
+ }
2744
+ /**
2745
+ * Convert screen-space pixel delta to world-space translation.
2746
+ * For top-down view, this is a simple perspective calculation.
2747
+ * @param deltaX Screen delta X in pixels
2748
+ * @param deltaY Screen delta Y in pixels
2749
+ * @returns World-space translation vector
2750
+ */
2751
+ screenDeltaToWorldDelta(deltaX, deltaY) {
2752
+ const camera = this.controller.camera;
2753
+ const canvas = this.domElement;
2754
+ const distance = Math.abs(camera.position.z);
2755
+ const fov = camera.getEffectiveFOV() * DEG2RAD;
2756
+ const worldHeight = 2 * distance * Math.tan(fov / 2);
2757
+ const worldWidth = worldHeight * camera.aspect;
2758
+ return this.tempVec2.set(deltaX / canvas.width * worldWidth, -(deltaY / canvas.height) * worldHeight);
2759
+ }
2760
+ }
2761
+ class InteractionsSystem {
2762
+ /**
2763
+ * @param renderer {@link Renderer} instance
2764
+ * @param events {@link EventSystem} instance
2765
+ * @param viewportSystem {@link ViewportSystem} instance
2766
+ * @param layerSystem {@link LayerSystem} instance
2767
+ */
2768
+ constructor(renderer, events, viewportSystem, layerSystem) {
2769
+ /** Gesture handlers for camera controls */
2770
+ __publicField(this, "handlers");
2771
+ __publicField(this, "handlerArray");
2772
+ __publicField(this, "canvas");
2773
+ __publicField(this, "mousePointer", new Vector2());
2774
+ __publicField(this, "eventManager");
2775
+ __publicField(this, "dragStart");
2776
+ __publicField(this, "dragThreshold", 15);
2777
+ __publicField(this, "isDragging", false);
2778
+ this.renderer = renderer;
2779
+ this.events = events;
2780
+ this.viewportSystem = viewportSystem;
2781
+ this.layerSystem = layerSystem;
2782
+ this.canvas = renderer.canvas;
2783
+ this.eventManager = new EventManager(this.canvas, {
2784
+ recognizers: [[Rotate, { enable: true }]]
2785
+ });
2786
+ console.log("EventManager created with Rotate recognizer");
2787
+ this.configureCameraControls();
2788
+ this.attachCanvasListeners();
2789
+ const controller = viewportSystem.cameraController;
2790
+ this.handlers = {
2791
+ pan: new PanHandler(controller, this.canvas, this.eventManager),
2792
+ roll: new RollHandler(controller, this.canvas, this.eventManager, viewportSystem)
2793
+ };
2794
+ this.handlerArray = Object.values(this.handlers);
2795
+ this.handlers.pan.enable();
2796
+ this.handlers.roll.enable();
2797
+ }
2798
+ /**
2799
+ * Update camera position and directions.
2800
+ * This should be called in your tick loop every time, and returns true if re-rendering is needed.
2801
+ * @param delta delta time in seconds
2802
+ * @returns true if re-rendering is needed
2803
+ */
2804
+ updateControls(delta) {
2805
+ let needsUpdate = this.viewportSystem.cameraController.update(delta);
2806
+ if (needsUpdate) {
2807
+ console.log(
2808
+ // "azimuth angle",
2809
+ // Math.round(this.viewportSystem.cameraController.azimuthAngle * RAD2DEG),
2810
+ "polar angle",
2811
+ Math.round(this.viewportSystem.cameraController.polarAngle * RAD2DEG),
2812
+ "camera position",
2813
+ this.viewportSystem.cameraController.camera.position.toArray().map(Math.round)
2814
+ );
2815
+ }
2816
+ for (const handler of this.handlerArray) {
2817
+ if (handler.isEnabled()) {
2818
+ needsUpdate = handler.update(delta) || needsUpdate;
2819
+ }
2820
+ }
2821
+ return needsUpdate;
2822
+ }
2823
+ configureCameraControls() {
2824
+ const controller = this.viewportSystem.cameraController;
2825
+ controller.draggingSmoothTime = 0;
2826
+ controller.dollyToCursor = true;
2827
+ controller.restThreshold = 1;
2828
+ controller.maxAzimuthAngle = 0;
2829
+ controller.minAzimuthAngle = 0;
2830
+ controller.maxPolarAngle = 0;
2831
+ controller.mouseButtons = {
2832
+ left: CameraController.ACTION.NONE,
2833
+ middle: CameraController.ACTION.NONE,
2834
+ right: CameraController.ACTION.ROTATE,
2835
+ wheel: CameraController.ACTION.NONE
2836
+ };
2837
+ controller.touches = {
2838
+ one: CameraController.ACTION.NONE,
2839
+ two: CameraController.ACTION.NONE,
2840
+ // Disabled - handlers implement custom gestures
2841
+ three: CameraController.ACTION.NONE
2842
+ };
2843
+ controller.connect(this.canvas);
2844
+ }
2845
+ attachCanvasListeners() {
2846
+ this.canvas.addEventListener("pointerdown", (event) => {
2847
+ this.isDragging = false;
2848
+ this.dragStart = { x: event.offsetX, y: event.offsetY };
2849
+ });
2850
+ this.canvas.addEventListener("pointerup", (event) => {
2851
+ if (!this.dragStart) return;
2852
+ const dX = event.offsetX - this.dragStart.x;
2853
+ const dY = event.offsetY - this.dragStart.y;
2854
+ this.dragStart = void 0;
2855
+ if (dX * dX + dY * dY > this.dragThreshold) this.isDragging = true;
2856
+ });
2857
+ const mouseEventsMap = {
2858
+ mousemove: "pointer:move",
2859
+ mouseout: "pointer:out",
2860
+ click: "pointer:click"
2861
+ };
2862
+ const mouseEventKeys = Object.keys(mouseEventsMap);
2863
+ mouseEventKeys.forEach((type) => {
2864
+ this.canvas.addEventListener(type, (event) => {
2865
+ const eventType = mouseEventsMap[type];
2866
+ const isDragging = type === "click" && this.isDragging;
2867
+ const hasListeners = this.events.hasListeners(eventType);
2868
+ if (isDragging || !hasListeners) return;
2869
+ normalizeEventCoordinates(event, this.canvas, this.mousePointer);
2870
+ const intersections = this.viewportSystem.getIntersectedObjects(this.mousePointer);
2871
+ const point = this.viewportSystem.screenTo("svg", this.mousePointer);
2872
+ const defs = this.layerSystem.getIntersectedDefs(intersections);
2873
+ this.events.emit(eventType, { event, point, defs });
2874
+ });
2875
+ });
2876
+ }
2877
+ }
2878
+ class Renderer {
2879
+ /**
2880
+ * @param opts {@link RendererOptions}
2881
+ */
2882
+ constructor(opts) {
2883
+ /** Whether to log debug information */
2884
+ __publicField(this, "debugLog");
2885
+ /** {@link HTMLCanvasElement} that this renderer is rendering to */
2886
+ __publicField(this, "canvas");
2887
+ __publicField(this, "ui");
2888
+ __publicField(this, "clock");
2889
+ __publicField(this, "renderer");
2890
+ __publicField(this, "eventSystem");
2891
+ __publicField(this, "layerSystem");
2892
+ __publicField(this, "viewportSystem");
2893
+ __publicField(this, "interactionsSystem");
2894
+ //private navigationSystem: NavigationSystem;
2895
+ __publicField(this, "memoryInfoExtension");
2896
+ __publicField(this, "memoryInfo", "");
2897
+ __publicField(this, "viewport");
2898
+ __publicField(this, "needsRedraw", true);
2899
+ __publicField(this, "isExternalMode", false);
2900
+ var _a, _b;
2901
+ const { canvas, gl, debugLog = false, ui } = opts;
2902
+ this.canvas = canvas;
2903
+ this.debugLog = debugLog;
2904
+ this.ui = ui;
2905
+ const rendererOptions = {
2906
+ antialias: true,
2907
+ context: gl,
2908
+ canvas: this.canvas
2909
+ };
2910
+ this.clock = new Clock();
2911
+ this.renderer = new WebGLRenderer(rendererOptions);
2912
+ this.renderer.setSize(this.canvas.clientWidth, this.canvas.clientHeight, false);
2913
+ this.renderer.setPixelRatio(window.devicePixelRatio);
2914
+ this.eventSystem = new EventSystem();
2915
+ this.viewportSystem = new ViewportSystem(this, this.eventSystem);
2916
+ this.layerSystem = new LayerSystem(this);
2917
+ this.interactionsSystem = new InteractionsSystem(this, this.eventSystem, this.viewportSystem, this.layerSystem);
2918
+ this.memoryInfoExtension = this.renderer.getContext().getExtension("GMAN_webgl_memory");
2919
+ this.canvas.addEventListener("webglcontextlost", (e) => this.onContextLost(e), false);
2920
+ this.canvas.addEventListener("webglcontextrestored", (e) => this.onContextRestored(e), false);
2921
+ void ((_b = (_a = this.ui) == null ? void 0 : _a.stats) == null ? void 0 : _b.init(this.renderer.getContext()));
2922
+ }
2923
+ /**
2924
+ * {@link NavigationAPI} instance for controlling the viewport
2925
+ */
2926
+ get controls() {
2927
+ return {
2928
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
2929
+ zoomBy: () => {
2930
+ },
2931
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
2932
+ zoomTo: () => {
2933
+ },
2934
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
2935
+ resetCamera: () => {
2936
+ },
2937
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
2938
+ configure: () => {
2939
+ },
2940
+ handlers: this.interactionsSystem.handlers,
2941
+ controller: this.viewportSystem.cameraController
2942
+ };
2943
+ }
2944
+ /**
2945
+ * {@link EventsAPI} instance for subscribing to internal events
2946
+ */
2947
+ get events() {
2948
+ return asEventAPI(this.eventSystem);
2949
+ }
2950
+ /**
2951
+ * Optional sub-rectangle of the viewport that is used for positioning scene's viewbox
2952
+ */
2953
+ get visibleRect() {
2954
+ return this.viewport;
2955
+ }
2956
+ /**
2957
+ * Optional sub-rectangle of the viewport that is used for positioning scene's viewbox
2958
+ */
2959
+ set visibleRect(rect) {
2960
+ this.viewport = rect;
2961
+ this.viewportSystem.updateViewport();
2962
+ this.update();
2963
+ }
2964
+ /**
2965
+ * Underlying {@link WebGLRenderer} instance
2966
+ */
2967
+ get context() {
2968
+ return this.renderer;
2969
+ }
2970
+ /**
2971
+ * Size of the canvas's drawing buffer in pixels (not to be confused with the client size in CSS pixels). Readonly.
2972
+ * @internal
2973
+ */
2974
+ get size() {
2975
+ return [this.canvas.width, this.canvas.height];
2976
+ }
2977
+ /**
2978
+ * Sets the renderer to external mode, where parts of rendering process are not managed by the renderer (e.g. Mapbox GL JS).
2979
+ * @param staticTransformMatrix static transform matrix to apply to the scene
2980
+ */
2981
+ // FIXME: Move to controls system
2982
+ configureExternalMode(staticTransformMatrix) {
2983
+ }
2984
+ /**
2985
+ * Update scene matrix from dynamic transform matrix.
2986
+ * @param dynamicTransformMatrix dynamic transform matrix (changes every frame)
2987
+ */
2988
+ updateExternalTransformMatrix(dynamicTransformMatrix) {
2989
+ }
2990
+ /**
2991
+ * Initialize the scene and start the rendering loop
2992
+ * @param sceneDef {@link SceneDef} to render
2993
+ * @param startLoop whether to start the rendering loop
2994
+ */
2995
+ start(sceneDef, startLoop = true) {
2996
+ this.clock.start();
2997
+ this.viewportSystem.initViewport(sceneDef);
2998
+ this.viewportSystem.scene.add(this.layerSystem.buildScene(sceneDef));
2999
+ if (startLoop) this.renderer.setAnimationLoop(() => this.render());
3000
+ }
3001
+ /**
3002
+ * Update the given defs to make them reflect the current state
3003
+ * @param defs {@link RenderableDef} array to update
3004
+ */
3005
+ update(...defs) {
3006
+ this.layerSystem.updateDefs(defs);
3007
+ this.needsRedraw = true;
3008
+ }
3009
+ // FIXME: Move to viewport system?
3010
+ /**
3011
+ * Converts coordinates from canvas space to SVG space.
3012
+ * @param point point in canvas space (relative to the canvas's top left corner)
3013
+ * @returns point in SVG space
3014
+ */
3015
+ screenToSvg(point) {
3016
+ const vector2 = new Vector2(point.x, point.y);
3017
+ canvasToNDC(vector2, this.canvas, vector2);
3018
+ return this.viewportSystem.screenTo("svg", vector2);
3019
+ }
3020
+ /**
3021
+ * Main rendering loop
3022
+ */
3023
+ render() {
3024
+ var _a, _b, _c, _d, _e, _f;
3025
+ (_b = (_a = this.ui) == null ? void 0 : _a.stats) == null ? void 0 : _b.begin();
3026
+ if (this.isExternalMode) this.renderer.resetState();
3027
+ else this.resizeCanvasToDisplaySize();
3028
+ this.viewportSystem.updatePtScale();
3029
+ const delta = this.clock.getDelta();
3030
+ const hasDefsUpdated = this.layerSystem.processPendingUpdates();
3031
+ const hasControlsUpdated = this.interactionsSystem.updateControls(delta);
3032
+ const needsRedraw = this.needsRedraw || hasControlsUpdated || hasDefsUpdated || this.ui;
3033
+ if (needsRedraw) {
3034
+ this.renderer.render(this.viewportSystem.scene, this.viewportSystem.camera);
3035
+ this.needsRedraw = false;
3036
+ }
3037
+ (_d = (_c = this.ui) == null ? void 0 : _c.stats) == null ? void 0 : _d.end();
3038
+ (_f = (_e = this.ui) == null ? void 0 : _e.stats) == null ? void 0 : _f.update();
3039
+ this.updateMemoryInfo();
3040
+ }
3041
+ // https://webgl2fundamentals.org/webgl/lessons/webgl-resizing-the-canvas.html
3042
+ resizeCanvasToDisplaySize() {
3043
+ const dpr = window.devicePixelRatio;
3044
+ const { width, height } = this.canvas.getBoundingClientRect();
3045
+ const displayWidth = Math.floor(width * dpr);
3046
+ const displayHeight = Math.floor(height * dpr);
3047
+ if (this.canvas.width !== displayWidth || this.canvas.height !== displayHeight || this.renderer.getPixelRatio() !== dpr) {
3048
+ if (this.debugLog) console.log("renderer resize", width, height, dpr);
3049
+ this.renderer.setSize(width, height, false);
3050
+ this.renderer.setPixelRatio(dpr);
3051
+ this.viewportSystem.updateViewport();
3052
+ this.update();
3053
+ }
3054
+ }
3055
+ updateMemoryInfo() {
3056
+ var _a;
3057
+ if (this.memoryInfoExtension && ((_a = this.ui) == null ? void 0 : _a.memoryInfoPanel)) {
3058
+ const memoryInfo = this.memoryInfoExtension.getMemoryInfo();
3059
+ const memoryInfoContent = JSON.stringify(memoryInfo.memory, null, 2);
3060
+ const elapsedTime = this.clock.getElapsedTime() * 1e3;
3061
+ if (memoryInfoContent !== this.memoryInfo) {
3062
+ const logMarker = `memoryInfo [${elapsedTime.toFixed(2)}ms since start]`;
3063
+ if (this.debugLog) console.log(logMarker, memoryInfo);
3064
+ this.memoryInfo = memoryInfoContent;
3065
+ }
3066
+ this.ui.memoryInfoPanel.textContent = JSON.stringify(memoryInfo, null, 2);
3067
+ }
3068
+ }
3069
+ onContextLost(event) {
3070
+ event.preventDefault();
3071
+ console.log("webglcontextlost event", event);
3072
+ this.renderer.setAnimationLoop(null);
3073
+ this.clock.stop();
3074
+ if (this.ui) setTimeout(() => this.renderer.forceContextRestore(), 0);
3075
+ }
3076
+ onContextRestored(event) {
3077
+ event.preventDefault();
3078
+ console.log("webglcontextrestored event", event);
3079
+ this.renderer.setAnimationLoop(() => this.render());
3080
+ this.clock.start();
3081
+ }
3082
+ }
3083
+ export {
3084
+ Polygon,
3085
+ Rect,
3086
+ Renderer,
3087
+ createVector2,
3088
+ isImageDef,
3089
+ isImageLayer,
3090
+ isLayerDef,
3091
+ isLayerLayer,
3092
+ isLineDef,
3093
+ isLineLayer,
3094
+ isShapeDef,
3095
+ isShapeLayer,
3096
+ isTextDef,
3097
+ isTextLayer
3098
+ };