@chocozhang/three-model-render 1.0.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 (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +609 -0
  3. package/dist/camera/index.d.ts +133 -0
  4. package/dist/camera/index.js +291 -0
  5. package/dist/camera/index.js.map +1 -0
  6. package/dist/camera/index.mjs +265 -0
  7. package/dist/camera/index.mjs.map +1 -0
  8. package/dist/core/index.d.ts +102 -0
  9. package/dist/core/index.js +455 -0
  10. package/dist/core/index.js.map +1 -0
  11. package/dist/core/index.mjs +432 -0
  12. package/dist/core/index.mjs.map +1 -0
  13. package/dist/effect/index.d.ts +214 -0
  14. package/dist/effect/index.js +749 -0
  15. package/dist/effect/index.js.map +1 -0
  16. package/dist/effect/index.mjs +728 -0
  17. package/dist/effect/index.mjs.map +1 -0
  18. package/dist/index.d.ts +852 -0
  19. package/dist/index.js +3268 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/index.mjs +3223 -0
  22. package/dist/index.mjs.map +1 -0
  23. package/dist/interaction/index.d.ts +160 -0
  24. package/dist/interaction/index.js +661 -0
  25. package/dist/interaction/index.js.map +1 -0
  26. package/dist/interaction/index.mjs +637 -0
  27. package/dist/interaction/index.mjs.map +1 -0
  28. package/dist/loader/index.d.ts +175 -0
  29. package/dist/loader/index.js +786 -0
  30. package/dist/loader/index.js.map +1 -0
  31. package/dist/loader/index.mjs +758 -0
  32. package/dist/loader/index.mjs.map +1 -0
  33. package/dist/setup/index.d.ts +47 -0
  34. package/dist/setup/index.js +199 -0
  35. package/dist/setup/index.js.map +1 -0
  36. package/dist/setup/index.mjs +178 -0
  37. package/dist/setup/index.mjs.map +1 -0
  38. package/dist/ui/index.d.ts +36 -0
  39. package/dist/ui/index.js +292 -0
  40. package/dist/ui/index.js.map +1 -0
  41. package/dist/ui/index.mjs +271 -0
  42. package/dist/ui/index.mjs.map +1 -0
  43. package/package.json +98 -0
@@ -0,0 +1,749 @@
1
+ 'use strict';
2
+
3
+ var THREE = require('three');
4
+
5
+ function _interopNamespaceDefault(e) {
6
+ var n = Object.create(null);
7
+ if (e) {
8
+ Object.keys(e).forEach(function (k) {
9
+ if (k !== 'default') {
10
+ var d = Object.getOwnPropertyDescriptor(e, k);
11
+ Object.defineProperty(n, k, d.get ? d : {
12
+ enumerable: true,
13
+ get: function () { return e[k]; }
14
+ });
15
+ }
16
+ });
17
+ }
18
+ n.default = e;
19
+ return Object.freeze(n);
20
+ }
21
+
22
+ var THREE__namespace = /*#__PURE__*/_interopNamespaceDefault(THREE);
23
+
24
+ /******************************************************************************
25
+ Copyright (c) Microsoft Corporation.
26
+
27
+ Permission to use, copy, modify, and/or distribute this software for any
28
+ purpose with or without fee is hereby granted.
29
+
30
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
31
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
32
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
33
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
34
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
35
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
36
+ PERFORMANCE OF THIS SOFTWARE.
37
+ ***************************************************************************** */
38
+ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
39
+
40
+
41
+ function __awaiter(thisArg, _arguments, P, generator) {
42
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
43
+ return new (P || (P = Promise))(function (resolve, reject) {
44
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
45
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
46
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
47
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
48
+ });
49
+ }
50
+
51
+ typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
52
+ var e = new Error(message);
53
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
54
+ };
55
+
56
+ function easeInOutQuad(t) {
57
+ return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
58
+ }
59
+ class GroupExploder {
60
+ constructor(scene, camera, controls) {
61
+ // sets and snapshots
62
+ this.currentSet = null;
63
+ this.stateMap = new Map();
64
+ // prevSet preserved to allow async restore
65
+ this.prevSet = null;
66
+ this.prevStateMap = new Map();
67
+ // material context map: material -> Set<contextId>, and snapshot store material->snap
68
+ this.materialContexts = new Map();
69
+ this.materialSnaps = new Map();
70
+ this.contextMaterials = new Map();
71
+ this.animId = null;
72
+ this.cameraAnimId = null;
73
+ this.isExploded = false;
74
+ this.isInitialized = false;
75
+ this.scene = scene;
76
+ this.camera = camera;
77
+ this.controls = controls;
78
+ }
79
+ log(msg) {
80
+ console.log('[GroupExploderDebug]', msg);
81
+ if (this.onLog)
82
+ this.onLog(msg);
83
+ }
84
+ init() {
85
+ if (this.isInitialized)
86
+ return;
87
+ this.isInitialized = true;
88
+ this.log('init() called');
89
+ }
90
+ /**
91
+ * setMeshes(newSet):
92
+ * - Detects content-level changes even if same Set reference is used.
93
+ * - Preserves prevSet/stateMap to allow async restore when needed.
94
+ * - Ensures stateMap contains snapshots for *all meshes in the new set*.
95
+ */
96
+ setMeshes(newSet, options) {
97
+ return __awaiter(this, void 0, void 0, function* () {
98
+ var _a, _b;
99
+ const autoRestorePrev = (_a = options === null || options === void 0 ? void 0 : options.autoRestorePrev) !== null && _a !== void 0 ? _a : true;
100
+ const restoreDuration = (_b = options === null || options === void 0 ? void 0 : options.restoreDuration) !== null && _b !== void 0 ? _b : 300;
101
+ this.log(`setMeshes called. newSetSize=${newSet ? newSet.size : 0}, autoRestorePrev=${autoRestorePrev}`);
102
+ // If the newSet is null and currentSet is null -> nothing
103
+ if (!newSet && !this.currentSet) {
104
+ this.log('setMeshes: both newSet and currentSet are null, nothing to do');
105
+ return;
106
+ }
107
+ // If both exist and are the same reference, we still must detect content changes.
108
+ const sameReference = this.currentSet === newSet;
109
+ // Prepare prevSet snapshot (we copy current to prev)
110
+ if (this.currentSet) {
111
+ this.prevSet = this.currentSet;
112
+ this.prevStateMap = new Map(this.stateMap);
113
+ this.log(`setMeshes: backed up current->prev prevSetSize=${this.prevSet.size}`);
114
+ }
115
+ else {
116
+ this.prevSet = null;
117
+ this.prevStateMap = new Map();
118
+ }
119
+ // If we used to be exploded and need to restore prevSet, do that first (await)
120
+ if (this.prevSet && autoRestorePrev && this.isExploded) {
121
+ this.log('setMeshes: need to restore prevSet before applying newSet');
122
+ yield this.restoreSet(this.prevSet, this.prevStateMap, restoreDuration, { debug: true });
123
+ this.log('setMeshes: prevSet restore done');
124
+ this.prevStateMap.clear();
125
+ this.prevSet = null;
126
+ }
127
+ // Now register newSet: we clear and rebuild stateMap carefully.
128
+ // But we must handle the case where caller reuses same Set object and just mutated elements.
129
+ // We will compute additions and removals.
130
+ const oldSet = this.currentSet;
131
+ this.currentSet = newSet;
132
+ // If newSet is null -> simply clear stateMap
133
+ if (!this.currentSet) {
134
+ this.stateMap.clear();
135
+ this.log('setMeshes: newSet is null -> cleared stateMap');
136
+ this.isExploded = false;
137
+ return;
138
+ }
139
+ // If we have oldSet (could be same reference) then compute diffs
140
+ if (oldSet) {
141
+ // If same reference but size or content differs -> handle diffs
142
+ const wasSameRef = sameReference;
143
+ let added = [];
144
+ let removed = [];
145
+ // Build maps of membership
146
+ const oldMembers = new Set(Array.from(oldSet));
147
+ const newMembers = new Set(Array.from(this.currentSet));
148
+ // find removals
149
+ oldMembers.forEach((m) => {
150
+ if (!newMembers.has(m))
151
+ removed.push(m);
152
+ });
153
+ // find additions
154
+ newMembers.forEach((m) => {
155
+ if (!oldMembers.has(m))
156
+ added.push(m);
157
+ });
158
+ if (wasSameRef && added.length === 0 && removed.length === 0) {
159
+ // truly identical (no content changes)
160
+ this.log('setMeshes: same reference and identical contents -> nothing to update');
161
+ return;
162
+ }
163
+ this.log(`setMeshes: diff detected -> added=${added.length}, removed=${removed.length}`);
164
+ // Remove snapshots for removed meshes
165
+ removed.forEach((m) => {
166
+ if (this.stateMap.has(m)) {
167
+ this.stateMap.delete(m);
168
+ }
169
+ });
170
+ // Ensure snapshots exist for current set members (create for newly added meshes)
171
+ yield this.ensureSnapshotsForSet(this.currentSet);
172
+ this.log(`setMeshes: after diff handling, stateMap size=${this.stateMap.size}`);
173
+ this.isExploded = false;
174
+ return;
175
+ }
176
+ else {
177
+ // no oldSet -> brand new registration
178
+ this.stateMap.clear();
179
+ yield this.ensureSnapshotsForSet(this.currentSet);
180
+ this.log(`setMeshes: recorded stateMap entries for newSet size=${this.stateMap.size}`);
181
+ this.isExploded = false;
182
+ return;
183
+ }
184
+ });
185
+ }
186
+ /**
187
+ * ensureSnapshotsForSet: for each mesh in set, ensure stateMap has an entry.
188
+ * If missing, record current matrixWorld as originalMatrixWorld (best-effort).
189
+ */
190
+ ensureSnapshotsForSet(set) {
191
+ return __awaiter(this, void 0, void 0, function* () {
192
+ set.forEach((m) => {
193
+ try {
194
+ m.updateMatrixWorld(true);
195
+ }
196
+ catch (_a) { }
197
+ if (!this.stateMap.has(m)) {
198
+ try {
199
+ this.stateMap.set(m, {
200
+ originalParent: m.parent || null,
201
+ originalMatrixWorld: (m.matrixWorld && m.matrixWorld.clone()) || new THREE__namespace.Matrix4().copy(m.matrix),
202
+ });
203
+ // Also store in userData for extra resilience
204
+ m.userData.__originalMatrixWorld = this.stateMap.get(m).originalMatrixWorld.clone();
205
+ }
206
+ catch (e) {
207
+ this.log(`ensureSnapshotsForSet: failed to snapshot mesh ${m.name || m.id}: ${e.message}`);
208
+ }
209
+ }
210
+ });
211
+ });
212
+ }
213
+ /**
214
+ * explode: compute targets first, compute targetBound using targets + mesh radii,
215
+ * animate camera to that targetBound, then animate meshes to targets.
216
+ */
217
+ explode(opts) {
218
+ return __awaiter(this, void 0, void 0, function* () {
219
+ var _a;
220
+ if (!this.currentSet || this.currentSet.size === 0) {
221
+ this.log('explode: empty currentSet, nothing to do');
222
+ return;
223
+ }
224
+ const { spacing = 2, duration = 1000, lift = 0.5, cameraPadding = 1.5, mode = 'spiral', dimOthers = { enabled: true, opacity: 0.25 }, debug = false, } = opts || {};
225
+ this.log(`explode called. setSize=${this.currentSet.size}, mode=${mode}, spacing=${spacing}, duration=${duration}, lift=${lift}, dim=${dimOthers.enabled}`);
226
+ this.cancelAnimations();
227
+ const meshes = Array.from(this.currentSet);
228
+ // ensure snapshots exist for any meshes that may have been added after initial registration
229
+ yield this.ensureSnapshotsForSet(this.currentSet);
230
+ // compute center/radius from current meshes (fallback)
231
+ const initial = this.computeBoundingSphereForMeshes(meshes);
232
+ const center = initial.center;
233
+ const baseRadius = Math.max(1, initial.radius);
234
+ this.log(`explode: initial center=${center.toArray().map((n) => n.toFixed(3))}, baseRadius=${baseRadius.toFixed(3)}`);
235
+ // compute targets (pure calculation)
236
+ const targets = this.computeTargetsByMode(meshes, center, baseRadius + spacing, { lift, mode });
237
+ this.log(`explode: computed ${targets.length} target positions`);
238
+ // compute target-based bounding sphere (targets + per-mesh radius)
239
+ const targetBound = this.computeBoundingSphereForPositionsAndMeshes(targets, meshes);
240
+ this.log(`explode: targetBound center=${targetBound.center.toArray().map((n) => n.toFixed(3))}, radius=${targetBound.radius.toFixed(3)}`);
241
+ yield this.animateCameraToFit(targetBound.center, targetBound.radius, { duration: Math.min(600, duration), padding: cameraPadding });
242
+ this.log('explode: camera animation to target bound completed');
243
+ // apply dim if needed with context id
244
+ const contextId = (dimOthers === null || dimOthers === void 0 ? void 0 : dimOthers.enabled) ? this.applyDimToOthers(meshes, (_a = dimOthers.opacity) !== null && _a !== void 0 ? _a : 0.25, { debug }) : null;
245
+ if (contextId)
246
+ this.log(`explode: applied dim for context ${contextId}`);
247
+ // capture starts after camera move
248
+ const starts = meshes.map((m) => {
249
+ const v = new THREE__namespace.Vector3();
250
+ try {
251
+ m.getWorldPosition(v);
252
+ }
253
+ catch (_a) {
254
+ // fallback to originalMatrixWorld if available
255
+ const st = this.stateMap.get(m);
256
+ if (st)
257
+ v.setFromMatrixPosition(st.originalMatrixWorld);
258
+ }
259
+ return v;
260
+ });
261
+ const startTime = performance.now();
262
+ const total = Math.max(1, duration);
263
+ const tick = (now) => {
264
+ const t = Math.min(1, (now - startTime) / total);
265
+ const eased = easeInOutQuad(t);
266
+ for (let i = 0; i < meshes.length; i++) {
267
+ const m = meshes[i];
268
+ const s = starts[i];
269
+ const tar = targets[i];
270
+ const cur = s.clone().lerp(tar, eased);
271
+ if (m.parent) {
272
+ const local = cur.clone();
273
+ m.parent.worldToLocal(local);
274
+ m.position.copy(local);
275
+ }
276
+ else {
277
+ m.position.copy(cur);
278
+ }
279
+ m.updateMatrix();
280
+ }
281
+ if (this.controls && typeof this.controls.update === 'function')
282
+ this.controls.update();
283
+ if (t < 1) {
284
+ this.animId = requestAnimationFrame(tick);
285
+ }
286
+ else {
287
+ this.animId = null;
288
+ this.isExploded = true;
289
+ this.log(`explode: completed. contextId=${contextId !== null && contextId !== void 0 ? contextId : 'none'}`);
290
+ }
291
+ };
292
+ this.animId = requestAnimationFrame(tick);
293
+ return;
294
+ });
295
+ }
296
+ restore(duration = 400) {
297
+ if (!this.currentSet || this.currentSet.size === 0) {
298
+ this.log('restore: no currentSet to restore');
299
+ return Promise.resolve();
300
+ }
301
+ this.log(`restore called for currentSet size=${this.currentSet.size}`);
302
+ return this.restoreSet(this.currentSet, this.stateMap, duration, { debug: true });
303
+ }
304
+ /**
305
+ * restoreSet: reparent and restore transforms using provided stateMap.
306
+ * If missing stateMap entry for a mesh, use fallbacks:
307
+ * 1) mesh.userData.__originalMatrixWorld (if present)
308
+ * 2) mesh.matrixWorld (current) -> smooth lerp to itself (no-op visually)
309
+ */
310
+ restoreSet(set, stateMap, duration = 400, opts) {
311
+ if (!set || set.size === 0) {
312
+ if (opts === null || opts === void 0 ? void 0 : opts.debug)
313
+ this.log('restoreSet: empty set, nothing to restore');
314
+ return Promise.resolve();
315
+ }
316
+ this.cancelAnimations();
317
+ const meshes = Array.from(set);
318
+ this.log(`restoreSet: starting restore for ${meshes.length} meshes (duration=${duration})`);
319
+ const starts = [];
320
+ const targets = [];
321
+ for (const m of meshes) {
322
+ try {
323
+ m.updateMatrixWorld(true);
324
+ }
325
+ catch (_a) { }
326
+ const s = new THREE__namespace.Vector3();
327
+ try {
328
+ m.getWorldPosition(s);
329
+ }
330
+ catch (_b) {
331
+ s.set(0, 0, 0);
332
+ }
333
+ starts.push(s);
334
+ const st = stateMap.get(m);
335
+ if (st) {
336
+ const tar = new THREE__namespace.Vector3();
337
+ tar.setFromMatrixPosition(st.originalMatrixWorld);
338
+ targets.push(tar);
339
+ }
340
+ else {
341
+ // fallback attempts
342
+ const ud = m.userData.__originalMatrixWorld;
343
+ if (ud instanceof THREE__namespace.Matrix4) {
344
+ const tar = new THREE__namespace.Vector3();
345
+ tar.setFromMatrixPosition(ud);
346
+ targets.push(tar);
347
+ this.log(`restoreSet: used userData.__originalMatrixWorld for mesh ${m.name || m.id}`);
348
+ }
349
+ else {
350
+ // fallback to current position to avoid NaN; will be effectively a no-op but avoids error
351
+ const tar = s.clone();
352
+ targets.push(tar);
353
+ this.log(`restoreSet: missing stateMap entry for mesh ${m.name || m.id} -> fallback to current pos`);
354
+ }
355
+ }
356
+ }
357
+ const startTime = performance.now();
358
+ const total = Math.max(1, duration);
359
+ return new Promise((resolve) => {
360
+ const tick = (now) => {
361
+ const t = Math.min(1, (now - startTime) / total);
362
+ const eased = easeInOutQuad(t);
363
+ for (let i = 0; i < meshes.length; i++) {
364
+ const m = meshes[i];
365
+ const s = starts[i];
366
+ const tar = targets[i];
367
+ const cur = s.clone().lerp(tar, eased);
368
+ // final step: ensure reparent to original parent if possible
369
+ const st = stateMap.get(m);
370
+ if (st && t >= 0.999) {
371
+ try {
372
+ const origParent = st.originalParent || this.scene;
373
+ origParent.updateMatrixWorld(true);
374
+ const parentInv = new THREE__namespace.Matrix4().copy(origParent.matrixWorld).invert();
375
+ const localMat = new THREE__namespace.Matrix4().multiplyMatrices(parentInv, st.originalMatrixWorld);
376
+ // reparent (if needed)
377
+ if (m.parent !== origParent) {
378
+ origParent.add(m);
379
+ }
380
+ m.matrix.copy(localMat);
381
+ m.matrix.decompose(m.position, m.quaternion, m.scale);
382
+ m.updateMatrixWorld(true);
383
+ }
384
+ catch (e) {
385
+ // fallback: set world position
386
+ if (m.parent) {
387
+ const local = tar.clone();
388
+ m.parent.worldToLocal(local);
389
+ m.position.copy(local);
390
+ }
391
+ else {
392
+ m.position.copy(tar);
393
+ }
394
+ m.updateMatrixWorld(true);
395
+ this.log(`restoreSet: error finalizing reparent for ${m.name || m.id}: ${e.message}`);
396
+ }
397
+ }
398
+ else {
399
+ // intermediate frames: lerp world -> convert to local relative to current parent
400
+ if (m.parent) {
401
+ const local = cur.clone();
402
+ m.parent.worldToLocal(local);
403
+ m.position.copy(local);
404
+ }
405
+ else {
406
+ m.position.copy(cur);
407
+ }
408
+ m.updateMatrix();
409
+ }
410
+ }
411
+ if (this.controls && typeof this.controls.update === 'function')
412
+ this.controls.update();
413
+ if (t < 1) {
414
+ this.animId = requestAnimationFrame(tick);
415
+ }
416
+ else {
417
+ this.animId = null;
418
+ this.cleanContextsForMeshes(meshes);
419
+ this.isExploded = false;
420
+ this.log('restoreSet: completed and cleaned contexts for restored meshes');
421
+ resolve();
422
+ }
423
+ };
424
+ this.animId = requestAnimationFrame(tick);
425
+ });
426
+ }
427
+ // material dim with context id
428
+ applyDimToOthers(explodingMeshes, opacity = 0.25, opts) {
429
+ const contextId = `ctx_${Date.now()}_${Math.floor(Math.random() * 10000)}`;
430
+ const explodingSet = new Set(explodingMeshes);
431
+ const touched = new Set();
432
+ this.scene.traverse((obj) => {
433
+ if (!obj.isMesh)
434
+ return;
435
+ const mesh = obj;
436
+ if (explodingSet.has(mesh))
437
+ return;
438
+ const applyMat = (mat) => {
439
+ var _a;
440
+ if (!this.materialSnaps.has(mat)) {
441
+ this.materialSnaps.set(mat, {
442
+ transparent: !!mat.transparent,
443
+ opacity: (_a = mat.opacity) !== null && _a !== void 0 ? _a : 1,
444
+ depthWrite: mat.depthWrite,
445
+ });
446
+ }
447
+ let s = this.materialContexts.get(mat);
448
+ if (!s) {
449
+ s = new Set();
450
+ this.materialContexts.set(mat, s);
451
+ }
452
+ s.add(contextId);
453
+ touched.add(mat);
454
+ mat.transparent = true;
455
+ mat.opacity = opacity;
456
+ mat.needsUpdate = true;
457
+ };
458
+ if (Array.isArray(mesh.material)) {
459
+ mesh.material.forEach((m) => applyMat(m));
460
+ }
461
+ else if (mesh.material) {
462
+ applyMat(mesh.material);
463
+ }
464
+ });
465
+ this.contextMaterials.set(contextId, touched);
466
+ this.log(`applyDimToOthers: context=${contextId}, touchedMaterials=${touched.size}`);
467
+ return contextId;
468
+ }
469
+ // clean contexts for meshes (restore materials whose contexts are removed)
470
+ cleanContextsForMeshes(meshes) {
471
+ // conservative strategy: for each context we created, delete it and restore materials accordingly
472
+ for (const [contextId, mats] of Array.from(this.contextMaterials.entries())) {
473
+ mats.forEach((mat) => {
474
+ const ctxSet = this.materialContexts.get(mat);
475
+ if (ctxSet) {
476
+ ctxSet.delete(contextId);
477
+ if (ctxSet.size === 0) {
478
+ const snap = this.materialSnaps.get(mat);
479
+ if (snap) {
480
+ mat.transparent = snap.transparent;
481
+ mat.opacity = snap.opacity;
482
+ if (typeof snap.depthWrite !== 'undefined')
483
+ mat.depthWrite = snap.depthWrite;
484
+ }
485
+ mat.needsUpdate = true;
486
+ this.materialContexts.delete(mat);
487
+ this.materialSnaps.delete(mat);
488
+ }
489
+ else {
490
+ this.materialContexts.set(mat, ctxSet);
491
+ }
492
+ }
493
+ });
494
+ this.contextMaterials.delete(contextId);
495
+ this.log(`cleanContextsForMeshes: removed context ${contextId}`);
496
+ }
497
+ }
498
+ // robust bounding sphere computation; if mesh world pos invalid, try stateMap's originalMatrixWorld as backoff
499
+ computeBoundingSphereForMeshes(meshes) {
500
+ const box = new THREE__namespace.Box3();
501
+ meshes.forEach((m) => {
502
+ try {
503
+ m.updateMatrixWorld(true);
504
+ const pos = new THREE__namespace.Vector3();
505
+ m.getWorldPosition(pos);
506
+ if (!isFinite(pos.x) || !isFinite(pos.y) || !isFinite(pos.z)) {
507
+ // fallback to stateMap's originalMatrixWorld if available
508
+ const st = this.stateMap.get(m);
509
+ if (st) {
510
+ const fallback = new THREE__namespace.Vector3();
511
+ fallback.setFromMatrixPosition(st.originalMatrixWorld);
512
+ if (isFinite(fallback.x) && isFinite(fallback.y) && isFinite(fallback.z)) {
513
+ this.log(`computeBoundingSphereForMeshes: using stateMap originalMatrixWorld for mesh ${m.name || m.id}`);
514
+ pos.copy(fallback);
515
+ }
516
+ else {
517
+ this.log(`computeBoundingSphereForMeshes: skipping mesh ${m.name || m.id} due to invalid positions`);
518
+ return;
519
+ }
520
+ }
521
+ else {
522
+ this.log(`computeBoundingSphereForMeshes: skipping mesh ${m.name || m.id} due to invalid world pos and no snapshot`);
523
+ return;
524
+ }
525
+ }
526
+ let radius = 0;
527
+ const geom = m.geometry || null;
528
+ if (geom) {
529
+ if (!geom.boundingSphere)
530
+ geom.computeBoundingSphere();
531
+ if (geom.boundingSphere) {
532
+ radius = geom.boundingSphere.radius;
533
+ const ws = new THREE__namespace.Vector3();
534
+ m.getWorldScale(ws);
535
+ radius = radius * Math.max(ws.x, ws.y, ws.z, 1e-6);
536
+ }
537
+ }
538
+ if (!isFinite(radius) || radius < 0 || radius > 1e8)
539
+ radius = 0;
540
+ const min = pos.clone().addScalar(-radius);
541
+ const max = pos.clone().addScalar(radius);
542
+ box.expandByPoint(min);
543
+ box.expandByPoint(max);
544
+ }
545
+ catch (e) {
546
+ this.log(`computeBoundingSphereForMeshes: error for mesh ${m.name || m.id}: ${e.message}`);
547
+ }
548
+ });
549
+ const center = new THREE__namespace.Vector3();
550
+ if (box.isEmpty()) {
551
+ center.set(0, 0, 0);
552
+ box.expandByPoint(center);
553
+ }
554
+ box.getCenter(center);
555
+ const sphere = new THREE__namespace.Sphere();
556
+ box.getBoundingSphere(sphere);
557
+ const radius = sphere.radius || Math.max(box.getSize(new THREE__namespace.Vector3()).length() * 0.5, 1.0);
558
+ return { center, radius };
559
+ }
560
+ // compute bounding sphere for positions + mesh radii
561
+ computeBoundingSphereForPositionsAndMeshes(positions, meshes) {
562
+ const box = new THREE__namespace.Box3();
563
+ for (let i = 0; i < positions.length; i++) {
564
+ const p = positions[i];
565
+ if (!isFinite(p.x) || !isFinite(p.y) || !isFinite(p.z)) {
566
+ this.log(`computeBoundingSphereForPositionsAndMeshes: skipping invalid target pos idx=${i}`);
567
+ continue;
568
+ }
569
+ let radius = 0;
570
+ const m = meshes[i];
571
+ try {
572
+ const geom = m.geometry || null;
573
+ if (geom) {
574
+ if (!geom.boundingSphere)
575
+ geom.computeBoundingSphere();
576
+ if (geom.boundingSphere) {
577
+ radius = geom.boundingSphere.radius;
578
+ const ws = new THREE__namespace.Vector3();
579
+ m.getWorldScale(ws);
580
+ radius = radius * Math.max(ws.x, ws.y, ws.z, 1e-6);
581
+ }
582
+ }
583
+ }
584
+ catch (_a) {
585
+ radius = 0;
586
+ }
587
+ if (!isFinite(radius) || radius < 0 || radius > 1e8)
588
+ radius = 0;
589
+ const min = p.clone().addScalar(-radius);
590
+ const max = p.clone().addScalar(radius);
591
+ box.expandByPoint(min);
592
+ box.expandByPoint(max);
593
+ }
594
+ const center = new THREE__namespace.Vector3();
595
+ if (box.isEmpty()) {
596
+ center.set(0, 0, 0);
597
+ box.expandByPoint(center);
598
+ }
599
+ box.getCenter(center);
600
+ const tmp = new THREE__namespace.Sphere();
601
+ box.getBoundingSphere(tmp);
602
+ const radius = tmp.radius || Math.max(box.getSize(new THREE__namespace.Vector3()).length() * 0.5, 1.0);
603
+ return { center, radius };
604
+ }
605
+ // computeTargetsByMode (unchanged logic but pure function)
606
+ computeTargetsByMode(meshes, center, baseRadius, opts) {
607
+ var _a, _b;
608
+ const n = meshes.length;
609
+ const lift = (_a = opts.lift) !== null && _a !== void 0 ? _a : 0.5;
610
+ const mode = (_b = opts.mode) !== null && _b !== void 0 ? _b : 'ring';
611
+ const targets = [];
612
+ if (mode === 'ring') {
613
+ for (let i = 0; i < n; i++) {
614
+ const angle = (i / n) * Math.PI * 2;
615
+ targets.push(new THREE__namespace.Vector3(center.x + Math.cos(angle) * baseRadius, center.y + lift, center.z + Math.sin(angle) * baseRadius));
616
+ }
617
+ return targets;
618
+ }
619
+ if (mode === 'spiral') {
620
+ const turns = Math.max(1, Math.ceil(n / 6));
621
+ for (let i = 0; i < n; i++) {
622
+ const t = i / (n - 1 || 1);
623
+ const angle = t * Math.PI * 2 * turns;
624
+ const radius = baseRadius * (0.3 + 0.7 * t);
625
+ targets.push(new THREE__namespace.Vector3(center.x + Math.cos(angle) * radius, center.y + lift + t * 0.5, center.z + Math.sin(angle) * radius));
626
+ }
627
+ return targets;
628
+ }
629
+ if (mode === 'grid') {
630
+ const cols = Math.ceil(Math.sqrt(n));
631
+ const rows = Math.ceil(n / cols);
632
+ const spacing = Math.max(0.5, baseRadius / Math.max(cols, rows));
633
+ const startX = center.x - ((cols - 1) * spacing) / 2;
634
+ const startZ = center.z - ((rows - 1) * spacing) / 2;
635
+ for (let i = 0; i < n; i++) {
636
+ const r = Math.floor(i / cols);
637
+ const c = i % cols;
638
+ targets.push(new THREE__namespace.Vector3(startX + c * spacing, center.y + lift, startZ + r * spacing));
639
+ }
640
+ return targets;
641
+ }
642
+ // radial
643
+ for (let i = 0; i < n; i++) {
644
+ const t = i / n;
645
+ const angle = t * Math.PI * 2;
646
+ const r = baseRadius * (1 + (i % 3) * 0.4 + Math.floor(i / 3) * 0.6);
647
+ targets.push(new THREE__namespace.Vector3(center.x + Math.cos(angle) * r, center.y + lift + Math.floor(i / 3) * 0.2, center.z + Math.sin(angle) * r));
648
+ }
649
+ return targets;
650
+ }
651
+ animateCameraToFit(targetCenter, targetRadius, opts) {
652
+ var _a, _b, _c;
653
+ const duration = (_a = opts === null || opts === void 0 ? void 0 : opts.duration) !== null && _a !== void 0 ? _a : 600;
654
+ const padding = (_b = opts === null || opts === void 0 ? void 0 : opts.padding) !== null && _b !== void 0 ? _b : 1.5;
655
+ if (!(this.camera instanceof THREE__namespace.PerspectiveCamera)) {
656
+ if (this.controls && this.controls.target) {
657
+ this.controls.target.copy(targetCenter);
658
+ if (typeof this.controls.update === 'function')
659
+ this.controls.update();
660
+ }
661
+ return Promise.resolve();
662
+ }
663
+ const cam = this.camera;
664
+ const fov = (cam.fov * Math.PI) / 180;
665
+ const safeRadius = isFinite(targetRadius) && targetRadius > 0 ? targetRadius : 1;
666
+ const desiredDistance = Math.min(1e6, (safeRadius * ((_c = opts === null || opts === void 0 ? void 0 : opts.padding) !== null && _c !== void 0 ? _c : padding)) / Math.sin(fov / 2));
667
+ const camPos = cam.position.clone();
668
+ const dir = camPos.clone().sub(targetCenter);
669
+ if (dir.length() === 0)
670
+ dir.set(0, 0, 1);
671
+ else
672
+ dir.normalize();
673
+ const newCamPos = targetCenter.clone().add(dir.multiplyScalar(desiredDistance));
674
+ const startPos = cam.position.clone();
675
+ const startTarget = (this.controls && this.controls.target) ? (this.controls.target.clone()) : this.getCameraLookAtPoint();
676
+ const endTarget = targetCenter.clone();
677
+ const startTime = performance.now();
678
+ return new Promise((resolve) => {
679
+ const tick = (now) => {
680
+ const t = Math.min(1, (now - startTime) / Math.max(1, duration));
681
+ const eased = easeInOutQuad(t);
682
+ cam.position.lerpVectors(startPos, newCamPos, eased);
683
+ if (this.controls && this.controls.target)
684
+ this.controls.target.lerpVectors(startTarget, endTarget, eased);
685
+ cam.updateProjectionMatrix();
686
+ if (this.controls && typeof this.controls.update === 'function')
687
+ this.controls.update();
688
+ if (t < 1)
689
+ this.cameraAnimId = requestAnimationFrame(tick);
690
+ else {
691
+ this.cameraAnimId = null;
692
+ this.log(`animateCameraToFit: done. center=${targetCenter.toArray().map((n) => n.toFixed(2))}, radius=${targetRadius.toFixed(2)}`);
693
+ resolve();
694
+ }
695
+ };
696
+ this.cameraAnimId = requestAnimationFrame(tick);
697
+ });
698
+ }
699
+ getCameraLookAtPoint() {
700
+ const dir = new THREE__namespace.Vector3();
701
+ this.camera.getWorldDirection(dir);
702
+ return this.camera.position.clone().add(dir.multiplyScalar(10));
703
+ }
704
+ cancelAnimations() {
705
+ if (this.animId) {
706
+ cancelAnimationFrame(this.animId);
707
+ this.animId = null;
708
+ }
709
+ if (this.cameraAnimId) {
710
+ cancelAnimationFrame(this.cameraAnimId);
711
+ this.cameraAnimId = null;
712
+ }
713
+ }
714
+ dispose() {
715
+ return __awaiter(this, arguments, void 0, function* (restoreBefore = true) {
716
+ this.cancelAnimations();
717
+ if (restoreBefore && this.isExploded) {
718
+ try {
719
+ yield this.restore(200);
720
+ }
721
+ catch (_a) { }
722
+ }
723
+ // force restore of materials
724
+ for (const [mat, ctxs] of Array.from(this.materialContexts.entries())) {
725
+ const snap = this.materialSnaps.get(mat);
726
+ if (snap) {
727
+ mat.transparent = snap.transparent;
728
+ mat.opacity = snap.opacity;
729
+ if (typeof snap.depthWrite !== 'undefined')
730
+ mat.depthWrite = snap.depthWrite;
731
+ mat.needsUpdate = true;
732
+ }
733
+ this.materialContexts.delete(mat);
734
+ this.materialSnaps.delete(mat);
735
+ }
736
+ this.contextMaterials.clear();
737
+ this.stateMap.clear();
738
+ this.prevStateMap.clear();
739
+ this.currentSet = null;
740
+ this.prevSet = null;
741
+ this.isInitialized = false;
742
+ this.isExploded = false;
743
+ this.log('dispose: cleaned up');
744
+ });
745
+ }
746
+ }
747
+
748
+ exports.GroupExploder = GroupExploder;
749
+ //# sourceMappingURL=index.js.map