three-zoo 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,60 +1,217 @@
1
- import { Box3, Vector3, PerspectiveCamera, MathUtils, Mesh, InstancedMesh, FrontSide, BufferAttribute, AnimationMixer, DirectionalLight, Spherical, RGBAFormat } from 'three';
1
+ import { PerspectiveCamera, MathUtils, Box3, Vector3, Mesh, InstancedMesh, FrontSide, BufferAttribute, AnimationMixer, DirectionalLight, Spherical, RGBAFormat } from 'three';
2
2
 
3
+ const DEFAULT_HORIZONTAL_FOV = 90;
4
+ const DEFAULT_VERTICAL_FOV = 90;
5
+ const DEFAULT_ASPECT = 1;
6
+ const DEFAULT_NEAR = 1;
7
+ const DEFAULT_FAR = 1000;
8
+ const MIN_FOV = 1;
9
+ const MAX_FOV = 179;
10
+ /**
11
+ * A camera that supports independent horizontal and vertical FOV settings.
12
+ * Extends Three.js PerspectiveCamera to allow separate control over horizontal
13
+ * and vertical fields of view.
14
+ */
15
+ class BiFovCamera extends PerspectiveCamera {
16
+ /**
17
+ * @param horizontalFov - Horizontal FOV in degrees (90° default)
18
+ * @param verticalFov - Vertical FOV in degrees (90° default)
19
+ * @param aspect - Width/height ratio (1 default)
20
+ * @param near - Near clipping plane (1 default)
21
+ * @param far - Far clipping plane (1000 default)
22
+ */
23
+ constructor(horizontalFov = DEFAULT_HORIZONTAL_FOV, verticalFov = DEFAULT_VERTICAL_FOV, aspect = DEFAULT_ASPECT, near = DEFAULT_NEAR, far = DEFAULT_FAR) {
24
+ super(verticalFov, aspect, near, far);
25
+ this.horizontalFovInternal = horizontalFov;
26
+ this.verticalFovInternal = verticalFov;
27
+ this.updateProjectionMatrix();
28
+ }
29
+ /** Current horizontal FOV in degrees */
30
+ get horizontalFov() {
31
+ return this.horizontalFovInternal;
32
+ }
33
+ /** Current vertical FOV in degrees */
34
+ get verticalFov() {
35
+ return this.verticalFovInternal;
36
+ }
37
+ /** Set horizontal FOV in degrees (clamped between 1° and 179°) */
38
+ set horizontalFov(value) {
39
+ this.horizontalFovInternal = MathUtils.clamp(value, MIN_FOV, MAX_FOV);
40
+ this.updateProjectionMatrix();
41
+ }
42
+ /** Set vertical FOV in degrees (clamped between 1° and 179°) */
43
+ set verticalFov(value) {
44
+ this.verticalFovInternal = MathUtils.clamp(value, MIN_FOV, MAX_FOV);
45
+ this.updateProjectionMatrix();
46
+ }
47
+ /**
48
+ * Update both horizontal and vertical FOV
49
+ * @param horizontal - Horizontal FOV in degrees
50
+ * @param vertical - Vertical FOV in degrees
51
+ */
52
+ setFov(horizontal, vertical) {
53
+ this.horizontalFovInternal = MathUtils.clamp(horizontal, MIN_FOV, MAX_FOV);
54
+ this.verticalFovInternal = MathUtils.clamp(vertical, MIN_FOV, MAX_FOV);
55
+ this.updateProjectionMatrix();
56
+ }
57
+ /**
58
+ * Copy FOV settings from another BiFovCamera
59
+ * @param source - Camera to copy from
60
+ */
61
+ copyFovSettings(source) {
62
+ this.horizontalFovInternal = source.horizontalFov;
63
+ this.verticalFovInternal = source.verticalFov;
64
+ this.updateProjectionMatrix();
65
+ }
66
+ /**
67
+ * Updates the projection matrix based on FOV settings and aspect ratio.
68
+ * In landscape: preserves horizontal FOV
69
+ * In portrait: preserves vertical FOV
70
+ */
71
+ updateProjectionMatrix() {
72
+ if (this.aspect >= 1) {
73
+ // Landscape orientation: preserve horizontal FOV
74
+ const radians = MathUtils.degToRad(this.horizontalFovInternal);
75
+ this.fov = MathUtils.radToDeg(Math.atan(Math.tan(radians / 2) / this.aspect) * 2);
76
+ }
77
+ else {
78
+ // Portrait orientation: preserve vertical FOV
79
+ this.fov = this.verticalFovInternal;
80
+ }
81
+ super.updateProjectionMatrix();
82
+ }
83
+ /** Get actual horizontal FOV after aspect ratio adjustments */
84
+ getEffectiveHorizontalFov() {
85
+ if (this.aspect >= 1) {
86
+ return this.horizontalFovInternal;
87
+ }
88
+ const verticalRadians = MathUtils.degToRad(this.verticalFovInternal);
89
+ return MathUtils.radToDeg(Math.atan(Math.tan(verticalRadians / 2) * this.aspect) * 2);
90
+ }
91
+ /** Get actual vertical FOV after aspect ratio adjustments */
92
+ getEffectiveVerticalFov() {
93
+ if (this.aspect < 1) {
94
+ return this.verticalFovInternal;
95
+ }
96
+ const horizontalRadians = MathUtils.degToRad(this.horizontalFovInternal);
97
+ return MathUtils.radToDeg(Math.atan(Math.tan(horizontalRadians / 2) / this.aspect) * 2);
98
+ }
99
+ /** Create a clone of this camera */
100
+ clone() {
101
+ const camera = new BiFovCamera(this.horizontalFovInternal, this.verticalFovInternal, this.aspect, this.near, this.far);
102
+ camera.copy(this, true);
103
+ return camera;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Box3 with additional convenience methods for width, height, depth, etc.
109
+ */
3
110
  class Bounds extends Box3 {
4
- constructor() {
5
- super(...arguments);
6
- this.tempVector3 = new Vector3();
111
+ constructor(object) {
112
+ super();
113
+ /** Temporary vector for calculations */
114
+ this.tempVector3A = new Vector3();
115
+ if (object) {
116
+ this.setFromObject(object);
117
+ }
7
118
  }
119
+ /** Width (x-axis length) */
8
120
  get width() {
9
121
  return this.max.x - this.min.x;
10
122
  }
123
+ /** Height (y-axis length) */
11
124
  get height() {
12
125
  return this.max.y - this.min.y;
13
126
  }
127
+ /** Depth (z-axis length) */
14
128
  get depth() {
15
129
  return this.max.z - this.min.z;
16
130
  }
131
+ /** Length of the box's diagonal */
17
132
  get diagonal() {
18
- this.tempVector3.subVectors(this.max, this.min);
19
- return this.tempVector3.length();
133
+ return this.tempVector3A.subVectors(this.max, this.min).length();
134
+ }
135
+ /** Volume (width * height * depth) */
136
+ getVolume() {
137
+ return this.width * this.height * this.depth;
138
+ }
139
+ /** Surface area (sum of all six faces) */
140
+ getSurfaceArea() {
141
+ const w = this.width;
142
+ const h = this.height;
143
+ const d = this.depth;
144
+ return 2 * (w * h + h * d + d * w);
20
145
  }
21
146
  }
22
147
 
23
- class DoubleFOVCamera extends PerspectiveCamera {
24
- constructor(horizontalFov = 90, verticalFov = 90, aspect = 1, near = 1, far = 1000) {
25
- super(verticalFov, aspect, near, far);
26
- this.horizontalFov = horizontalFov;
27
- this.verticalFov = verticalFov;
28
- }
29
- updateProjectionMatrix() {
30
- if (this.aspect >= 1) {
31
- const radians = MathUtils.degToRad(this.horizontalFov);
32
- this.fov = MathUtils.radToDeg(Math.atan(Math.tan(radians / 2) / this.aspect) * 2);
148
+ const POSITION_COMPONENT_COUNT = 3;
149
+ const NORMAL_COMPONENT_COUNT = 3;
150
+ /**
151
+ * Internal utility to identify identical geometries.
152
+ * @internal
153
+ */
154
+ class GeometryHasher {
155
+ /**
156
+ * Creates a hash for a geometry based on its vertex data.
157
+ * Vertices that differ by less than tolerance are considered the same.
158
+ *
159
+ * @param geometry - Geometry to hash
160
+ * @param tolerance - How close vertices need to be to count as identical
161
+ * @returns Hash string that's the same for matching geometries
162
+ * @internal
163
+ */
164
+ static getGeometryHash(geometry, tolerance) {
165
+ const position = geometry.attributes["position"];
166
+ const positionArray = position.array;
167
+ const positionHashParts = [];
168
+ // Sample vertex positions with tolerance
169
+ for (let i = 0; i < positionArray.length; i += POSITION_COMPONENT_COUNT) {
170
+ const x = Math.round(positionArray[i] / tolerance);
171
+ const y = Math.round(positionArray[i + 1] / tolerance);
172
+ const z = Math.round(positionArray[i + 2] / tolerance);
173
+ positionHashParts.push(`${x},${y},${z}`);
33
174
  }
34
- else {
35
- this.fov = this.verticalFov;
175
+ // Hash normal data if available
176
+ const normal = geometry.attributes["normal"];
177
+ const normalHashParts = [];
178
+ const normalArray = normal.array;
179
+ for (let i = 0; i < normalArray.length; i += NORMAL_COMPONENT_COUNT) {
180
+ const x = Math.round(normalArray[i] / tolerance);
181
+ const y = Math.round(normalArray[i + 1] / tolerance);
182
+ const z = Math.round(normalArray[i + 2] / tolerance);
183
+ normalHashParts.push(`${x},${y},${z}`);
36
184
  }
37
- super.updateProjectionMatrix();
185
+ // Combine position and normal hashes
186
+ const positionHash = positionHashParts.join("|");
187
+ const normalHash = normalHashParts.join("|");
188
+ return `${positionHash}#${normalHash}`;
38
189
  }
39
190
  }
40
191
 
41
- class Enumerator {
192
+ /** Find and modify objects in a Three.js scene */
193
+ class SceneTraversal {
194
+ /** Find first object with exact name match */
42
195
  static getObjectByName(object, name) {
43
- if (object.name === name)
196
+ if (object.name === name) {
44
197
  return object;
198
+ }
45
199
  for (const child of object.children) {
46
- const result = Enumerator.getObjectByName(child, name);
47
- if (result)
200
+ const result = SceneTraversal.getObjectByName(child, name);
201
+ if (result) {
48
202
  return result;
203
+ }
49
204
  }
50
205
  return null;
51
206
  }
207
+ /** Find first material with exact name match */
52
208
  static getMaterialByName(object, name) {
53
209
  if (object instanceof Mesh) {
54
210
  if (Array.isArray(object.material)) {
55
211
  for (const material of object.material) {
56
- if (material.name === name)
212
+ if (material.name === name) {
57
213
  return material;
214
+ }
58
215
  }
59
216
  }
60
217
  else if (object.material.name === name) {
@@ -62,20 +219,23 @@ class Enumerator {
62
219
  }
63
220
  }
64
221
  for (const child of object.children) {
65
- const material = Enumerator.getMaterialByName(child, name);
66
- if (material)
222
+ const material = SceneTraversal.getMaterialByName(child, name);
223
+ if (material) {
67
224
  return material;
225
+ }
68
226
  }
69
227
  return null;
70
228
  }
229
+ /** Process all objects of a specific type */
71
230
  static enumerateObjectsByType(object, type, callback) {
72
231
  if (object instanceof type) {
73
232
  callback(object);
74
233
  }
75
234
  for (const child of object.children) {
76
- Enumerator.enumerateObjectsByType(child, type, callback);
235
+ SceneTraversal.enumerateObjectsByType(child, type, callback);
77
236
  }
78
237
  }
238
+ /** Process all materials in meshes */
79
239
  static enumerateMaterials(object, callback) {
80
240
  if (object instanceof Mesh) {
81
241
  if (Array.isArray(object.material)) {
@@ -88,19 +248,21 @@ class Enumerator {
88
248
  }
89
249
  }
90
250
  for (const child of object.children) {
91
- Enumerator.enumerateMaterials(child, callback);
251
+ SceneTraversal.enumerateMaterials(child, callback);
92
252
  }
93
253
  }
254
+ /** Find all objects whose names match a pattern */
94
255
  static filterObjects(object, name) {
95
256
  let result = [];
96
257
  if (object.name && name.test(object.name)) {
97
258
  result.push(object);
98
259
  }
99
260
  for (const child of object.children) {
100
- result = result.concat(Enumerator.filterObjects(child, name));
261
+ result = result.concat(SceneTraversal.filterObjects(child, name));
101
262
  }
102
263
  return result;
103
264
  }
265
+ /** Find all materials whose names match a pattern */
104
266
  static filterMaterials(object, name) {
105
267
  let result = [];
106
268
  if (object instanceof Mesh) {
@@ -118,224 +280,79 @@ class Enumerator {
118
280
  }
119
281
  }
120
282
  for (const child of object.children) {
121
- result = result.concat(Enumerator.filterMaterials(child, name));
283
+ result = result.concat(SceneTraversal.filterMaterials(child, name));
122
284
  }
123
285
  return result;
124
286
  }
125
- static setShadowRecursive(object, castShadow = true, receiveShadow = true) {
287
+ /** Set shadow properties on meshes */
288
+ static setShadowRecursive(object, castShadow = true, receiveShadow = true, filter) {
126
289
  if (object instanceof Mesh || "isMesh" in object) {
127
290
  object.castShadow = castShadow;
128
291
  object.receiveShadow = receiveShadow;
129
292
  }
130
293
  for (const child of object.children) {
131
- Enumerator.setShadowRecursive(child, castShadow, receiveShadow);
294
+ SceneTraversal.setShadowRecursive(child, castShadow, receiveShadow, filter);
132
295
  }
133
296
  }
134
297
  }
135
298
 
299
+ const MIN_INSTANCE_COUNT = 2;
300
+ const DEFAULT_TOLERANCE = 1e-6;
136
301
  /**
137
- * Utility class for comparing and hashing BufferGeometry instances with tolerance support.
302
+ * Combines identical meshes into instanced versions for better performance.
303
+ * Meshes are considered identical if they share the same geometry and materials.
138
304
  */
139
- class GeometryComparator {
305
+ class InstanceAssembler {
140
306
  /**
141
- * Generates a consistent hash for a BufferGeometry based on its contents and tolerance.
307
+ * Find meshes that can be instanced and combine them.
308
+ * Only processes meshes that:
309
+ * - Have no children
310
+ * - Pass the filter function (if any)
311
+ * - Share geometry with at least one other mesh
142
312
  *
143
- * @param geometry - The geometry to hash
144
- * @param tolerance - Precision level for number comparison (values within tolerance are considered equal)
145
- * @returns A string hash that will be identical for geometrically equivalent geometries
146
- */
147
- static getGeometryHash(geometry, tolerance = 1e-6) {
148
- const hashParts = [];
149
- // Process attributes
150
- const attributes = geometry.attributes;
151
- const attributeNames = Object.keys(attributes).sort(); // Sort for consistent order
152
- for (const name of attributeNames) {
153
- const attribute = attributes[name];
154
- hashParts.push(`${name}:${attribute.itemSize}:${this.getAttributeHash(attribute, tolerance)}`);
155
- }
156
- // Process index if present
157
- if (geometry.index) {
158
- hashParts.push(`index:${this.getAttributeHash(geometry.index, tolerance)}`);
159
- }
160
- return hashParts.join("|");
161
- }
162
- /**
163
- * Compares two BufferGeometry instances for approximate equality.
164
- * Early exit if UUIDs match (same object or cloned geometry).
165
- */
166
- static compare(firstGeometry, secondGeometry, tolerance = 1e-6) {
167
- if (firstGeometry.uuid === secondGeometry.uuid) {
168
- return true;
169
- }
170
- // Use hash comparison for consistent results
171
- return (this.getGeometryHash(firstGeometry, tolerance) ===
172
- this.getGeometryHash(secondGeometry, tolerance));
173
- }
174
- /**
175
- * Generates a hash for a buffer attribute with tolerance.
313
+ * @param container - Object containing meshes to process
314
+ * @param options - Optional settings
176
315
  */
177
- static getAttributeHash(attribute, tolerance) {
178
- const array = attribute.array;
179
- const itemSize = "itemSize" in attribute ? attribute.itemSize : 1;
180
- const hashParts = [];
181
- // Group values by their "tolerance buckets"
182
- for (let i = 0; i < array.length; i += itemSize) {
183
- const itemValues = [];
184
- for (let j = 0; j < itemSize; j++) {
185
- const val = array[i + j];
186
- // Round to nearest tolerance multiple to group similar values
187
- itemValues.push(Math.round(val / tolerance) * tolerance);
188
- }
189
- hashParts.push(itemValues.join(","));
190
- }
191
- return hashParts.join(";");
192
- }
193
- /**
194
- * Compares two buffer attributes with tolerance.
195
- */
196
- static compareBufferAttributes(firstAttribute, secondAttribute, tolerance) {
197
- return (this.getAttributeHash(firstAttribute, tolerance) ===
198
- this.getAttributeHash(secondAttribute, tolerance));
199
- }
200
- }
201
- // import {
202
- // BufferAttribute,
203
- // BufferGeometry,
204
- // InterleavedBufferAttribute,
205
- // } from "three";
206
- // type AnySuitableAttribute = BufferAttribute | InterleavedBufferAttribute;
207
- // /**
208
- // * Utility class for comparing two BufferGeometry instances with tolerance support.
209
- // * Checks geometry attributes (positions, normals, UVs, etc.) and indices (if present).
210
- // */
211
- // export class GeometryComparator {
212
- // /**
213
- // * Compares two BufferGeometry instances for approximate equality.
214
- // * Early exit if UUIDs match (same object or cloned geometry).
215
- // *
216
- // * @param firstGeometry - The first geometry to compare.
217
- // * @param secondGeometry - The second geometry to compare.
218
- // * @param tolerance - Maximum allowed difference between numeric values (default: 1e-6).
219
- // * @returns `true` if geometries are equivalent within tolerance, otherwise `false`.
220
- // */
221
- // public static compare(
222
- // firstGeometry: BufferGeometry,
223
- // secondGeometry: BufferGeometry,
224
- // tolerance = 1e-6,
225
- // ): boolean {
226
- // if (firstGeometry.uuid === secondGeometry.uuid) {
227
- // return true;
228
- // }
229
- // const firstAttributes = firstGeometry.attributes;
230
- // const secondAttributes = secondGeometry.attributes;
231
- // const firstAttributeNames = Object.keys(firstAttributes);
232
- // const secondAttributeNames = Object.keys(secondAttributes);
233
- // if (firstAttributeNames.length !== secondAttributeNames.length) {
234
- // return false;
235
- // }
236
- // for (const attributeName of firstAttributeNames) {
237
- // if (!secondAttributes[attributeName]) {
238
- // return false;
239
- // }
240
- // const firstAttribute = firstAttributes[
241
- // attributeName
242
- // ] as AnySuitableAttribute;
243
- // const secondAttribute = secondAttributes[
244
- // attributeName
245
- // ] as AnySuitableAttribute;
246
- // if (
247
- // firstAttribute.count !== secondAttribute.count ||
248
- // firstAttribute.itemSize !== secondAttribute.itemSize ||
249
- // !this.compareBufferAttributes(
250
- // firstAttribute,
251
- // secondAttribute,
252
- // tolerance,
253
- // )
254
- // ) {
255
- // return false;
256
- // }
257
- // }
258
- // if (firstGeometry.index || secondGeometry.index) {
259
- // if (!firstGeometry.index || !secondGeometry.index) {
260
- // return false;
261
- // }
262
- // if (
263
- // !this.compareBufferAttributes(
264
- // firstGeometry.index,
265
- // secondGeometry.index,
266
- // tolerance,
267
- // )
268
- // ) {
269
- // return false;
270
- // }
271
- // }
272
- // return true;
273
- // }
274
- // /**
275
- // * Compares two buffer attributes (or index buffers) with tolerance.
276
- // *
277
- // * @param firstAttribute - First attribute/indices to compare.
278
- // * @param secondAttribute - Second attribute/indices to compare.
279
- // * @param tolerance - Maximum allowed difference between array elements.
280
- // * @returns `true` if arrays are equal within tolerance, otherwise `false`.
281
- // */
282
- // private static compareBufferAttributes(
283
- // firstAttribute: AnySuitableAttribute,
284
- // secondAttribute: AnySuitableAttribute,
285
- // tolerance: number,
286
- // ): boolean {
287
- // const firstArray = firstAttribute.array;
288
- // const secondArray = secondAttribute.array;
289
- // if (firstArray.length !== secondArray.length) {
290
- // return false;
291
- // }
292
- // for (let index = 0; index < firstArray.length; index++) {
293
- // if (Math.abs(firstArray[index] - secondArray[index]) > tolerance) {
294
- // return false;
295
- // }
296
- // }
297
- // return true;
298
- // }
299
- // }
300
-
301
- class InstanceAssembler {
302
- static assemble(options) {
316
+ static assemble(container, options = {}) {
303
317
  var _a, _b;
304
318
  const dictionary = new Map();
305
- const instancedMeshes = [];
306
- const tolerance = (_a = options.geometryTolerance) !== null && _a !== void 0 ? _a : 1e-6;
307
- const geometryHashCache = new Map();
308
- Enumerator.enumerateObjectsByType(options.container, Mesh, (child) => {
319
+ const instances = [];
320
+ const tolerance = (_a = options.geometryTolerance) !== null && _a !== void 0 ? _a : DEFAULT_TOLERANCE;
321
+ const geometryHashes = new Map();
322
+ SceneTraversal.enumerateObjectsByType(container, Mesh, (child) => {
309
323
  var _a;
310
324
  if (child.children.length === 0 &&
311
325
  (!options.filter || options.filter(child))) {
312
326
  const materials = Array.isArray(child.material)
313
327
  ? child.material
314
328
  : [child.material];
315
- let geometryHash = geometryHashCache.get(child.geometry.uuid);
329
+ let geometryHash = geometryHashes.get(child.geometry.uuid);
316
330
  if (!geometryHash) {
317
- geometryHash = GeometryComparator.getGeometryHash(child.geometry, tolerance);
318
- geometryHashCache.set(child.geometry.uuid, geometryHash);
331
+ geometryHash = GeometryHasher.getGeometryHash(child.geometry, tolerance);
332
+ geometryHashes.set(child.geometry.uuid, geometryHash);
319
333
  }
320
334
  const materialKey = materials.map((m) => m.uuid).join(",");
321
335
  const compositeKey = `${geometryHash}|${materialKey}`;
322
336
  const entry = (_a = dictionary.get(compositeKey)) !== null && _a !== void 0 ? _a : {
323
337
  meshes: [],
324
- materials: materials,
338
+ materials,
325
339
  castShadow: false,
326
340
  receiveShadow: false,
327
341
  };
328
- if (child.castShadow)
342
+ if (child.castShadow) {
329
343
  entry.castShadow = true;
330
- if (child.receiveShadow)
344
+ }
345
+ if (child.receiveShadow) {
331
346
  entry.receiveShadow = true;
347
+ }
332
348
  entry.meshes.push(child);
333
349
  dictionary.set(compositeKey, entry);
334
350
  }
335
351
  });
336
352
  for (const descriptor of dictionary.values()) {
337
- if (descriptor.meshes.length < 2)
353
+ if (descriptor.meshes.length < MIN_INSTANCE_COUNT) {
338
354
  continue;
355
+ }
339
356
  const { meshes, materials, castShadow, receiveShadow } = descriptor;
340
357
  const sortedMeshes = meshes.sort((a, b) => a.name.localeCompare(b.name));
341
358
  const defaultMesh = sortedMeshes[0];
@@ -350,63 +367,74 @@ class InstanceAssembler {
350
367
  instancedMesh.userData[mesh.uuid] = mesh.userData;
351
368
  }
352
369
  instancedMesh.instanceMatrix.needsUpdate = true;
353
- instancedMeshes.push(instancedMesh);
370
+ instances.push(instancedMesh);
354
371
  for (const mesh of sortedMeshes) {
355
372
  (_b = mesh.parent) === null || _b === void 0 ? void 0 : _b.remove(mesh);
356
373
  }
357
374
  }
358
- if (instancedMeshes.length > 0) {
359
- options.container.add(...instancedMeshes);
375
+ if (instances.length > 0) {
376
+ container.add(...instances);
360
377
  }
361
378
  }
362
379
  }
363
380
 
381
+ /** Post-processes a scene based on name patterns */
364
382
  class SceneProcessor {
365
- static process(options) {
366
- const container = options.asset.clone();
367
- InstanceAssembler.assemble({ container: container });
368
- Enumerator.enumerateMaterials(container, (material) => {
369
- material.transparent = SceneProcessor.matchesAny(material.name, options.transparentMaterialNames);
370
- material.depthWrite = !SceneProcessor.matchesAny(material.name, options.noDepthWriteMaterialNames);
383
+ /**
384
+ * Process a scene to set up materials and shadows.
385
+ *
386
+ * @param asset - Scene to process
387
+ * @param options - How to process the scene
388
+ * @returns Processed scene root objects
389
+ */
390
+ static process(asset, options) {
391
+ const container = options.cloneAsset !== false ? asset.clone() : asset;
392
+ if (options.assembleInstances !== false) {
393
+ InstanceAssembler.assemble(container);
394
+ }
395
+ SceneTraversal.enumerateMaterials(container, (material) => {
396
+ material.transparent = SceneProcessor.matchesAny(material.name, options.transparentMaterialExpressions);
397
+ material.depthWrite = !SceneProcessor.matchesAny(material.name, options.noDepthWriteMaterialExpressions);
371
398
  material.side = FrontSide;
372
399
  material.forceSinglePass = true;
373
400
  material.depthTest = true;
374
401
  });
375
- Enumerator.enumerateObjectsByType(container, Mesh, (child) => {
376
- child.castShadow = SceneProcessor.matchesAny(child.name, options.castShadowMeshNames);
377
- child.receiveShadow = SceneProcessor.matchesAny(child.name, options.receiveShadowMeshNames);
402
+ SceneTraversal.enumerateObjectsByType(container, Mesh, (child) => {
403
+ child.castShadow = SceneProcessor.matchesAny(child.name, options.castShadowExpressions);
404
+ child.receiveShadow = SceneProcessor.matchesAny(child.name, options.receiveShadwoExpressions);
378
405
  });
379
406
  return container.children;
380
407
  }
381
- static matchesAny(value, patterns = []) {
382
- return patterns.some((p) => typeof p === "string" ? value === p : p.test(value));
408
+ /** Does the string match any of the patterns? */
409
+ static matchesAny(value, expressions = []) {
410
+ return expressions.some((p) => p.test(value));
383
411
  }
384
412
  }
385
413
 
386
- /**
387
- * Utilities for baking poses and animations from SkinnedMesh into a regular static Mesh.
388
- */
414
+ /** Number of components per vertex */
415
+ const COMPONENT_COUNT = 3;
416
+ /** Convert skinned meshes to regular static meshes */
389
417
  class SkinnedMeshBaker {
390
418
  /**
391
- * Bakes the current pose of a SkinnedMesh into a regular geometry.
392
- * Transforms all vertices according to the current skeleton state.
419
+ * Convert a skinned mesh to a regular mesh in its current pose.
420
+ * The resulting mesh will have no bones but look identical.
393
421
  *
394
- * @param skinnedMesh - SkinnedMesh from which to bake the geometry
395
- * @returns A new Mesh with positions corresponding to the current bone positions
422
+ * @param skinnedMesh - Mesh to convert
423
+ * @returns Static mesh with baked vertex positions
396
424
  */
397
425
  static bakePose(skinnedMesh) {
398
426
  const bakedGeometry = skinnedMesh.geometry.clone();
399
427
  const position = bakedGeometry.attributes["position"];
400
- const newPositions = new Float32Array(position.count * 3);
428
+ const newPositions = new Float32Array(position.count * COMPONENT_COUNT);
401
429
  const target = new Vector3();
402
430
  for (let i = 0; i < position.count; i++) {
403
431
  target.fromBufferAttribute(position, i);
404
432
  skinnedMesh.applyBoneTransform(i, target);
405
- newPositions[i * 3 + 0] = target.x;
406
- newPositions[i * 3 + 1] = target.y;
407
- newPositions[i * 3 + 2] = target.z;
433
+ newPositions[i * COMPONENT_COUNT + 0] = target.x;
434
+ newPositions[i * COMPONENT_COUNT + 1] = target.y;
435
+ newPositions[i * COMPONENT_COUNT + 2] = target.z;
408
436
  }
409
- bakedGeometry.setAttribute("position", new BufferAttribute(newPositions, 3));
437
+ bakedGeometry.setAttribute("position", new BufferAttribute(newPositions, COMPONENT_COUNT));
410
438
  bakedGeometry.computeVertexNormals();
411
439
  bakedGeometry.deleteAttribute("skinIndex");
412
440
  bakedGeometry.deleteAttribute("skinWeight");
@@ -415,13 +443,13 @@ class SkinnedMeshBaker {
415
443
  return mesh;
416
444
  }
417
445
  /**
418
- * Bakes a SkinnedMesh in a specific pose derived from an AnimationClip at the given timestamp.
446
+ * Bake a single frame from an animation into a static mesh.
419
447
  *
420
- * @param armature - The parent object (typically an armature from GLTF) containing the bones
421
- * @param skinnedMesh - The SkinnedMesh to be baked
422
- * @param timeOffset - The animation time in seconds to set
423
- * @param clip - The animation clip
424
- * @returns A new Mesh with geometry matching the specified animation frame
448
+ * @param armature - Root object with bones (usually from GLTF)
449
+ * @param skinnedMesh - Mesh to convert
450
+ * @param timeOffset - Time in seconds within the animation
451
+ * @param clip - Animation to get the pose from
452
+ * @returns Static mesh with baked vertex positions
425
453
  */
426
454
  static bakeAnimationFrame(armature, skinnedMesh, timeOffset, clip) {
427
455
  const mixer = new AnimationMixer(armature);
@@ -434,9 +462,16 @@ class SkinnedMeshBaker {
434
462
  }
435
463
  }
436
464
 
465
+ const RGBA_CHANNEL_COUNT = 4;
466
+ const RGB_CHANNEL_COUNT = 3;
467
+ const LUMINANCE_R = 0.2126;
468
+ const LUMINANCE_G = 0.7152;
469
+ const LUMINANCE_B = 0.0722;
470
+ /** A directional light with spherical positioning controls */
437
471
  class Sun extends DirectionalLight {
438
472
  constructor() {
439
473
  super(...arguments);
474
+ /** Internal vectors to avoid garbage collection */
440
475
  this.tempVector3D0 = new Vector3();
441
476
  this.tempVector3D1 = new Vector3();
442
477
  this.tempVector3D2 = new Vector3();
@@ -448,27 +483,34 @@ class Sun extends DirectionalLight {
448
483
  this.tempBox3 = new Box3();
449
484
  this.tempSpherical = new Spherical();
450
485
  }
486
+ /** Distance from the light to its target */
451
487
  get distance() {
452
488
  return this.position.length();
453
489
  }
490
+ /** Vertical angle from the ground in radians */
454
491
  get elevation() {
455
492
  return this.tempSpherical.setFromVector3(this.position).phi;
456
493
  }
494
+ /** Horizontal angle around the target in radians */
457
495
  get azimuth() {
458
496
  return this.tempSpherical.setFromVector3(this.position).theta;
459
497
  }
498
+ /** Set distance while keeping current angles */
460
499
  set distance(value) {
461
500
  this.tempSpherical.setFromVector3(this.position);
462
501
  this.position.setFromSphericalCoords(value, this.tempSpherical.phi, this.tempSpherical.theta);
463
502
  }
503
+ /** Set elevation while keeping current distance and azimuth */
464
504
  set elevation(value) {
465
505
  this.tempSpherical.setFromVector3(this.position);
466
506
  this.position.setFromSphericalCoords(this.tempSpherical.radius, value, this.tempSpherical.theta);
467
507
  }
508
+ /** Set azimuth while keeping current distance and elevation */
468
509
  set azimuth(value) {
469
510
  this.tempSpherical.setFromVector3(this.position);
470
511
  this.position.setFromSphericalCoords(this.tempSpherical.radius, this.tempSpherical.phi, value);
471
512
  }
513
+ /** Configure shadows to cover all corners of a bounding box */
472
514
  setShadowMapFromBox3(box3) {
473
515
  const camera = this.shadow.camera;
474
516
  this.target.updateWorldMatrix(true, false);
@@ -498,23 +540,26 @@ class Sun extends DirectionalLight {
498
540
  camera.updateWorldMatrix(true, false);
499
541
  camera.updateProjectionMatrix();
500
542
  }
543
+ /** Set light direction based on brightest point in an HDR texture */
501
544
  setDirectionFromHDR(texture, distance = 1) {
502
545
  const data = texture.image.data;
503
546
  const width = texture.image.width;
504
547
  const height = texture.image.height;
505
548
  let maxLuminance = 0;
506
549
  let maxIndex = 0;
507
- const step = texture.format === RGBAFormat ? 4 : 3;
550
+ // Find brightest pixel
551
+ const step = texture.format === RGBAFormat ? RGBA_CHANNEL_COUNT : RGB_CHANNEL_COUNT;
508
552
  for (let i = 0; i < data.length; i += step) {
509
553
  const r = data[i];
510
554
  const g = data[i + 1];
511
555
  const b = data[i + 2];
512
- const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
556
+ const luminance = LUMINANCE_R * r + LUMINANCE_G * g + LUMINANCE_B * b;
513
557
  if (luminance > maxLuminance) {
514
558
  maxLuminance = luminance;
515
559
  maxIndex = i;
516
560
  }
517
561
  }
562
+ // Convert to spherical coordinates
518
563
  const pixelIndex = maxIndex / step;
519
564
  const x = pixelIndex % width;
520
565
  const y = Math.floor(pixelIndex / width);
@@ -526,5 +571,5 @@ class Sun extends DirectionalLight {
526
571
  }
527
572
  }
528
573
 
529
- export { Bounds, DoubleFOVCamera, Enumerator, InstanceAssembler, SceneProcessor, SkinnedMeshBaker, Sun };
574
+ export { BiFovCamera, Bounds, InstanceAssembler, SceneProcessor, SceneTraversal, SkinnedMeshBaker, Sun };
530
575
  //# sourceMappingURL=index.js.map