@chocozhang/three-model-render 1.0.3 → 1.0.5

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/CHANGELOG.md +39 -0
  2. package/README.md +134 -97
  3. package/dist/camera/index.d.ts +59 -36
  4. package/dist/camera/index.js +83 -67
  5. package/dist/camera/index.js.map +1 -1
  6. package/dist/camera/index.mjs +83 -67
  7. package/dist/camera/index.mjs.map +1 -1
  8. package/dist/core/index.d.ts +81 -28
  9. package/dist/core/index.js +194 -104
  10. package/dist/core/index.js.map +1 -1
  11. package/dist/core/index.mjs +194 -105
  12. package/dist/core/index.mjs.map +1 -1
  13. package/dist/effect/index.d.ts +47 -134
  14. package/dist/effect/index.js +287 -288
  15. package/dist/effect/index.js.map +1 -1
  16. package/dist/effect/index.mjs +287 -288
  17. package/dist/effect/index.mjs.map +1 -1
  18. package/dist/index.d.ts +432 -349
  19. package/dist/index.js +1399 -1228
  20. package/dist/index.js.map +1 -1
  21. package/dist/index.mjs +1395 -1229
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/interaction/index.d.ts +85 -52
  24. package/dist/interaction/index.js +168 -142
  25. package/dist/interaction/index.js.map +1 -1
  26. package/dist/interaction/index.mjs +168 -142
  27. package/dist/interaction/index.mjs.map +1 -1
  28. package/dist/loader/index.d.ts +106 -58
  29. package/dist/loader/index.js +492 -454
  30. package/dist/loader/index.js.map +1 -1
  31. package/dist/loader/index.mjs +491 -455
  32. package/dist/loader/index.mjs.map +1 -1
  33. package/dist/setup/index.d.ts +26 -24
  34. package/dist/setup/index.js +125 -163
  35. package/dist/setup/index.js.map +1 -1
  36. package/dist/setup/index.mjs +124 -164
  37. package/dist/setup/index.mjs.map +1 -1
  38. package/dist/ui/index.d.ts +18 -7
  39. package/dist/ui/index.js +45 -37
  40. package/dist/ui/index.js.map +1 -1
  41. package/dist/ui/index.mjs +45 -37
  42. package/dist/ui/index.mjs.map +1 -1
  43. package/package.json +50 -22
@@ -21,42 +21,34 @@ function _interopNamespaceDefault(e) {
21
21
 
22
22
  var THREE__namespace = /*#__PURE__*/_interopNamespaceDefault(THREE);
23
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
-
24
+ /**
25
+ * @file exploder.ts
26
+ * @description
27
+ * GroupExploder - Three.js based model explosion effect tool (Vue3 + TS Support)
28
+ * ----------------------------------------------------------------------
29
+ * This tool is used to perform "explode / restore" animations on a set of specified Meshes:
30
+ * - Initialize only once (onMounted)
31
+ * - Supports dynamic switching of models and automatically restores the explosion state of the previous model
32
+ * - Supports multiple arrangement modes (ring / spiral / grid / radial)
33
+ * - Supports automatic transparency for non-exploded objects (dimOthers)
34
+ * - Supports automatic camera positioning to the best observation point
35
+ * - All animations use native requestAnimationFrame
36
+ *
37
+ * @best-practice
38
+ * - Initialize in `onMounted`.
39
+ * - Use `setMeshes` to update the active set of meshes to explode.
40
+ * - Call `explode()` to trigger the effect and `restore()` to reset.
41
+ */
56
42
  function easeInOutQuad(t) {
57
43
  return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
58
44
  }
59
45
  class GroupExploder {
46
+ /**
47
+ * Constructor
48
+ * @param scene Three.js Scene instance
49
+ * @param camera Three.js Camera (usually PerspectiveCamera)
50
+ * @param controls OrbitControls instance (must be bound to camera)
51
+ */
60
52
  constructor(scene, camera, controls) {
61
53
  // sets and snapshots
62
54
  this.currentSet = null;
@@ -88,211 +80,210 @@ class GroupExploder {
88
80
  this.log('init() called');
89
81
  }
90
82
  /**
91
- * setMeshes(newSet):
83
+ * Set the current set of meshes for explosion.
92
84
  * - Detects content-level changes even if same Set reference is used.
93
85
  * - Preserves prevSet/stateMap to allow async restore when needed.
94
86
  * - Ensures stateMap contains snapshots for *all meshes in the new set*.
87
+ * @param newSet The new set of meshes
88
+ * @param contextId Optional context ID to distinguish business scenarios
95
89
  */
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;
90
+ async setMeshes(newSet, options) {
91
+ const autoRestorePrev = options?.autoRestorePrev ?? true;
92
+ const restoreDuration = options?.restoreDuration ?? 300;
93
+ this.log(`setMeshes called. newSetSize=${newSet ? newSet.size : 0}, autoRestorePrev=${autoRestorePrev}`);
94
+ // If the newSet is null and currentSet is null -> nothing
95
+ if (!newSet && !this.currentSet) {
96
+ this.log('setMeshes: both newSet and currentSet are null, nothing to do');
97
+ return;
98
+ }
99
+ // If both exist and are the same reference, we still must detect content changes.
100
+ const sameReference = this.currentSet === newSet;
101
+ // Prepare prevSet snapshot (we copy current to prev)
102
+ if (this.currentSet) {
103
+ this.prevSet = this.currentSet;
104
+ this.prevStateMap = new Map(this.stateMap);
105
+ this.log(`setMeshes: backed up current->prev prevSetSize=${this.prevSet.size}`);
106
+ }
107
+ else {
108
+ this.prevSet = null;
109
+ this.prevStateMap = new Map();
110
+ }
111
+ // If we used to be exploded and need to restore prevSet, do that first (await)
112
+ if (this.prevSet && autoRestorePrev && this.isExploded) {
113
+ this.log('setMeshes: need to restore prevSet before applying newSet');
114
+ await this.restoreSet(this.prevSet, this.prevStateMap, restoreDuration, { debug: true });
115
+ this.log('setMeshes: prevSet restore done');
116
+ this.prevStateMap.clear();
117
+ this.prevSet = null;
118
+ }
119
+ // Now register newSet: we clear and rebuild stateMap carefully.
120
+ // But we must handle the case where caller reuses same Set object and just mutated elements.
121
+ // We will compute additions and removals.
122
+ const oldSet = this.currentSet;
123
+ this.currentSet = newSet;
124
+ // If newSet is null -> simply clear stateMap
125
+ if (!this.currentSet) {
126
+ this.stateMap.clear();
127
+ this.log('setMeshes: newSet is null -> cleared stateMap');
128
+ this.isExploded = false;
129
+ return;
130
+ }
131
+ // If we have oldSet (could be same reference) then compute diffs
132
+ if (oldSet) {
133
+ // If same reference but size or content differs -> handle diffs
134
+ const wasSameRef = sameReference;
135
+ let added = [];
136
+ let removed = [];
137
+ // Build maps of membership
138
+ const oldMembers = new Set(Array.from(oldSet));
139
+ const newMembers = new Set(Array.from(this.currentSet));
140
+ // find removals
141
+ oldMembers.forEach((m) => {
142
+ if (!newMembers.has(m))
143
+ removed.push(m);
144
+ });
145
+ // find additions
146
+ newMembers.forEach((m) => {
147
+ if (!oldMembers.has(m))
148
+ added.push(m);
149
+ });
150
+ if (wasSameRef && added.length === 0 && removed.length === 0) {
151
+ // truly identical (no content changes)
152
+ this.log('setMeshes: same reference and identical contents -> nothing to update');
137
153
  return;
138
154
  }
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;
155
+ this.log(`setMeshes: diff detected -> added=${added.length}, removed=${removed.length}`);
156
+ // Remove snapshots for removed meshes
157
+ removed.forEach((m) => {
158
+ if (this.stateMap.has(m)) {
159
+ this.stateMap.delete(m);
162
160
  }
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
- });
161
+ });
162
+ // Ensure snapshots exist for current set members (create for newly added meshes)
163
+ await this.ensureSnapshotsForSet(this.currentSet);
164
+ this.log(`setMeshes: after diff handling, stateMap size=${this.stateMap.size}`);
165
+ this.isExploded = false;
166
+ return;
167
+ }
168
+ else {
169
+ // no oldSet -> brand new registration
170
+ this.stateMap.clear();
171
+ await this.ensureSnapshotsForSet(this.currentSet);
172
+ this.log(`setMeshes: recorded stateMap entries for newSet size=${this.stateMap.size}`);
173
+ this.isExploded = false;
174
+ return;
175
+ }
185
176
  }
186
177
  /**
187
178
  * ensureSnapshotsForSet: for each mesh in set, ensure stateMap has an entry.
188
179
  * If missing, record current matrixWorld as originalMatrixWorld (best-effort).
189
180
  */
190
- ensureSnapshotsForSet(set) {
191
- return __awaiter(this, void 0, void 0, function* () {
192
- set.forEach((m) => {
181
+ async ensureSnapshotsForSet(set) {
182
+ set.forEach((m) => {
183
+ try {
184
+ m.updateMatrixWorld(true);
185
+ }
186
+ catch { }
187
+ if (!this.stateMap.has(m)) {
193
188
  try {
194
- m.updateMatrixWorld(true);
189
+ this.stateMap.set(m, {
190
+ originalParent: m.parent || null,
191
+ originalMatrixWorld: (m.matrixWorld && m.matrixWorld.clone()) || new THREE__namespace.Matrix4().copy(m.matrix),
192
+ });
193
+ // Also store in userData for extra resilience
194
+ m.userData.__originalMatrixWorld = this.stateMap.get(m).originalMatrixWorld.clone();
195
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
- }
196
+ catch (e) {
197
+ this.log(`ensureSnapshotsForSet: failed to snapshot mesh ${m.name || m.id}: ${e.message}`);
209
198
  }
210
- });
199
+ }
211
200
  });
212
201
  }
213
202
  /**
214
203
  * explode: compute targets first, compute targetBound using targets + mesh radii,
215
204
  * animate camera to that targetBound, then animate meshes to targets.
216
205
  */
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;
206
+ async explode(opts) {
207
+ if (!this.currentSet || this.currentSet.size === 0) {
208
+ this.log('explode: empty currentSet, nothing to do');
209
+ return;
210
+ }
211
+ const { spacing = 2, duration = 1000, lift = 0.5, cameraPadding = 1.5, mode = 'spiral', dimOthers = { enabled: true, opacity: 0.25 }, debug = false, } = opts || {};
212
+ this.log(`explode called. setSize=${this.currentSet.size}, mode=${mode}, spacing=${spacing}, duration=${duration}, lift=${lift}, dim=${dimOthers.enabled}`);
213
+ this.cancelAnimations();
214
+ const meshes = Array.from(this.currentSet);
215
+ // ensure snapshots exist for any meshes that may have been added after initial registration
216
+ await this.ensureSnapshotsForSet(this.currentSet);
217
+ // compute center/radius from current meshes (fallback)
218
+ const initial = this.computeBoundingSphereForMeshes(meshes);
219
+ const center = initial.center;
220
+ const baseRadius = Math.max(1, initial.radius);
221
+ this.log(`explode: initial center=${center.toArray().map((n) => n.toFixed(3))}, baseRadius=${baseRadius.toFixed(3)}`);
222
+ // compute targets (pure calculation)
223
+ const targets = this.computeTargetsByMode(meshes, center, baseRadius + spacing, { lift, mode });
224
+ this.log(`explode: computed ${targets.length} target positions`);
225
+ // compute target-based bounding sphere (targets + per-mesh radius)
226
+ const targetBound = this.computeBoundingSphereForPositionsAndMeshes(targets, meshes);
227
+ this.log(`explode: targetBound center=${targetBound.center.toArray().map((n) => n.toFixed(3))}, radius=${targetBound.radius.toFixed(3)}`);
228
+ await this.animateCameraToFit(targetBound.center, targetBound.radius, { duration: Math.min(600, duration), padding: cameraPadding });
229
+ this.log('explode: camera animation to target bound completed');
230
+ // apply dim if needed with context id
231
+ const contextId = dimOthers?.enabled ? this.applyDimToOthers(meshes, dimOthers.opacity ?? 0.25, { debug }) : null;
232
+ if (contextId)
233
+ this.log(`explode: applied dim for context ${contextId}`);
234
+ // capture starts after camera move
235
+ const starts = meshes.map((m) => {
236
+ const v = new THREE__namespace.Vector3();
237
+ try {
238
+ m.getWorldPosition(v);
223
239
  }
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);
240
+ catch {
241
+ // fallback to originalMatrixWorld if available
242
+ const st = this.stateMap.get(m);
243
+ if (st)
244
+ v.setFromMatrixPosition(st.originalMatrixWorld);
245
+ }
246
+ return v;
247
+ });
248
+ const startTime = performance.now();
249
+ const total = Math.max(1, duration);
250
+ const tick = (now) => {
251
+ const t = Math.min(1, (now - startTime) / total);
252
+ const eased = easeInOutQuad(t);
253
+ for (let i = 0; i < meshes.length; i++) {
254
+ const m = meshes[i];
255
+ const s = starts[i];
256
+ const tar = targets[i];
257
+ const cur = s.clone().lerp(tar, eased);
258
+ if (m.parent) {
259
+ const local = cur.clone();
260
+ m.parent.worldToLocal(local);
261
+ m.position.copy(local);
285
262
  }
286
263
  else {
287
- this.animId = null;
288
- this.isExploded = true;
289
- this.log(`explode: completed. contextId=${contextId !== null && contextId !== void 0 ? contextId : 'none'}`);
264
+ m.position.copy(cur);
290
265
  }
291
- };
292
- this.animId = requestAnimationFrame(tick);
293
- return;
294
- });
266
+ m.updateMatrix();
267
+ }
268
+ if (this.controls && typeof this.controls.update === 'function')
269
+ this.controls.update();
270
+ if (t < 1) {
271
+ this.animId = requestAnimationFrame(tick);
272
+ }
273
+ else {
274
+ this.animId = null;
275
+ this.isExploded = true;
276
+ this.log(`explode: completed. contextId=${contextId ?? 'none'}`);
277
+ }
278
+ };
279
+ this.animId = requestAnimationFrame(tick);
280
+ return;
295
281
  }
282
+ /**
283
+ * Restore all exploded meshes to their original transform:
284
+ * - Supports smooth animation
285
+ * - Automatically cancels transparency
286
+ */
296
287
  restore(duration = 400) {
297
288
  if (!this.currentSet || this.currentSet.size === 0) {
298
289
  this.log('restore: no currentSet to restore');
@@ -309,7 +300,7 @@ class GroupExploder {
309
300
  */
310
301
  restoreSet(set, stateMap, duration = 400, opts) {
311
302
  if (!set || set.size === 0) {
312
- if (opts === null || opts === void 0 ? void 0 : opts.debug)
303
+ if (opts?.debug)
313
304
  this.log('restoreSet: empty set, nothing to restore');
314
305
  return Promise.resolve();
315
306
  }
@@ -322,12 +313,12 @@ class GroupExploder {
322
313
  try {
323
314
  m.updateMatrixWorld(true);
324
315
  }
325
- catch (_a) { }
316
+ catch { }
326
317
  const s = new THREE__namespace.Vector3();
327
318
  try {
328
319
  m.getWorldPosition(s);
329
320
  }
330
- catch (_b) {
321
+ catch {
331
322
  s.set(0, 0, 0);
332
323
  }
333
324
  starts.push(s);
@@ -425,7 +416,7 @@ class GroupExploder {
425
416
  });
426
417
  }
427
418
  // material dim with context id
428
- applyDimToOthers(explodingMeshes, opacity = 0.25, opts) {
419
+ applyDimToOthers(explodingMeshes, opacity = 0.25, _opts) {
429
420
  const contextId = `ctx_${Date.now()}_${Math.floor(Math.random() * 10000)}`;
430
421
  const explodingSet = new Set(explodingMeshes);
431
422
  const touched = new Set();
@@ -436,11 +427,10 @@ class GroupExploder {
436
427
  if (explodingSet.has(mesh))
437
428
  return;
438
429
  const applyMat = (mat) => {
439
- var _a;
440
430
  if (!this.materialSnaps.has(mat)) {
441
431
  this.materialSnaps.set(mat, {
442
432
  transparent: !!mat.transparent,
443
- opacity: (_a = mat.opacity) !== null && _a !== void 0 ? _a : 1,
433
+ opacity: mat.opacity ?? 1,
444
434
  depthWrite: mat.depthWrite,
445
435
  });
446
436
  }
@@ -467,7 +457,7 @@ class GroupExploder {
467
457
  return contextId;
468
458
  }
469
459
  // clean contexts for meshes (restore materials whose contexts are removed)
470
- cleanContextsForMeshes(meshes) {
460
+ cleanContextsForMeshes(_meshes) {
471
461
  // conservative strategy: for each context we created, delete it and restore materials accordingly
472
462
  for (const [contextId, mats] of Array.from(this.contextMaterials.entries())) {
473
463
  mats.forEach((mat) => {
@@ -581,7 +571,7 @@ class GroupExploder {
581
571
  }
582
572
  }
583
573
  }
584
- catch (_a) {
574
+ catch {
585
575
  radius = 0;
586
576
  }
587
577
  if (!isFinite(radius) || radius < 0 || radius > 1e8)
@@ -604,10 +594,9 @@ class GroupExploder {
604
594
  }
605
595
  // computeTargetsByMode (unchanged logic but pure function)
606
596
  computeTargetsByMode(meshes, center, baseRadius, opts) {
607
- var _a, _b;
608
597
  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';
598
+ const lift = opts.lift ?? 0.5;
599
+ const mode = opts.mode ?? 'ring';
611
600
  const targets = [];
612
601
  if (mode === 'ring') {
613
602
  for (let i = 0; i < n; i++) {
@@ -649,99 +638,109 @@ class GroupExploder {
649
638
  return targets;
650
639
  }
651
640
  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;
641
+ const duration = opts?.duration ?? 600;
642
+ const padding = opts?.padding ?? 1.5;
655
643
  if (!(this.camera instanceof THREE__namespace.PerspectiveCamera)) {
656
644
  if (this.controls && this.controls.target) {
657
- this.controls.target.copy(targetCenter);
658
- if (typeof this.controls.update === 'function')
659
- this.controls.update();
645
+ // Fallback for non-PerspectiveCamera
646
+ const startTarget = this.controls.target.clone();
647
+ const startPos = this.camera.position.clone();
648
+ const endTarget = targetCenter.clone();
649
+ const dir = startPos.clone().sub(startTarget).normalize();
650
+ const dist = startPos.distanceTo(startTarget);
651
+ const endPos = endTarget.clone().add(dir.multiplyScalar(dist));
652
+ const startTime = performance.now();
653
+ const tick = (now) => {
654
+ const t = Math.min(1, (now - startTime) / duration);
655
+ const k = easeInOutQuad(t);
656
+ if (this.controls && this.controls.target) {
657
+ this.controls.target.lerpVectors(startTarget, endTarget, k);
658
+ }
659
+ this.camera.position.lerpVectors(startPos, endPos, k);
660
+ if (this.controls?.update)
661
+ this.controls.update();
662
+ if (t < 1) {
663
+ this.cameraAnimId = requestAnimationFrame(tick);
664
+ }
665
+ else {
666
+ this.cameraAnimId = null;
667
+ }
668
+ };
669
+ this.cameraAnimId = requestAnimationFrame(tick);
660
670
  }
661
671
  return Promise.resolve();
662
672
  }
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)
673
+ // PerspectiveCamera logic
674
+ const fov = THREE__namespace.MathUtils.degToRad(this.camera.fov);
675
+ const aspect = this.camera.aspect;
676
+ // Calculate distance needed to fit the sphere
677
+ // tan(fov/2) = radius / distance => distance = radius / tan(fov/2)
678
+ // We also consider aspect ratio for horizontal fit
679
+ const distV = targetRadius / Math.sin(fov / 2);
680
+ const distH = targetRadius / Math.sin(Math.min(fov, fov * aspect) / 2); // approximate
681
+ const dist = Math.max(distV, distH) * padding;
682
+ const startPos = this.camera.position.clone();
683
+ const startTarget = this.controls?.target ? this.controls.target.clone() : new THREE__namespace.Vector3(); // assumption
684
+ if (!this.controls?.target) {
685
+ this.camera.getWorldDirection(startTarget);
686
+ startTarget.add(startPos);
687
+ }
688
+ // Determine end position: keep current viewing direction relative to center
689
+ const dir = startPos.clone().sub(startTarget).normalize();
690
+ if (dir.lengthSq() < 0.001)
670
691
  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
692
  const endTarget = targetCenter.clone();
677
- const startTime = performance.now();
693
+ const endPos = endTarget.clone().add(dir.multiplyScalar(dist));
678
694
  return new Promise((resolve) => {
695
+ const startTime = performance.now();
679
696
  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)
697
+ const t = Math.min(1, (now - startTime) / duration);
698
+ const k = easeInOutQuad(t);
699
+ this.camera.position.lerpVectors(startPos, endPos, k);
700
+ if (this.controls && this.controls.target) {
701
+ this.controls.target.lerpVectors(startTarget, endTarget, k);
702
+ this.controls.update?.();
703
+ }
704
+ else {
705
+ this.camera.lookAt(endTarget); // simple lookAt if no controls
706
+ }
707
+ if (t < 1) {
689
708
  this.cameraAnimId = requestAnimationFrame(tick);
709
+ }
690
710
  else {
691
711
  this.cameraAnimId = null;
692
- this.log(`animateCameraToFit: done. center=${targetCenter.toArray().map((n) => n.toFixed(2))}, radius=${targetRadius.toFixed(2)}`);
693
712
  resolve();
694
713
  }
695
714
  };
696
715
  this.cameraAnimId = requestAnimationFrame(tick);
697
716
  });
698
717
  }
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
- }
718
+ /**
719
+ * Cancel all running animations
720
+ */
704
721
  cancelAnimations() {
705
- if (this.animId) {
722
+ if (this.animId !== null) {
706
723
  cancelAnimationFrame(this.animId);
707
724
  this.animId = null;
708
725
  }
709
- if (this.cameraAnimId) {
726
+ if (this.cameraAnimId !== null) {
710
727
  cancelAnimationFrame(this.cameraAnimId);
711
728
  this.cameraAnimId = null;
712
729
  }
713
730
  }
731
+ /**
732
+ * Dispose: remove listener, cancel animation, clear references
733
+ */
714
734
  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
- });
735
+ this.cancelAnimations();
736
+ this.currentSet = null;
737
+ this.prevSet = null;
738
+ this.stateMap.clear();
739
+ this.prevStateMap.clear();
740
+ this.materialContexts.clear();
741
+ this.materialSnaps.clear();
742
+ this.contextMaterials.clear();
743
+ this.log('dispose() called, resources cleaned up');
745
744
  }
746
745
  }
747
746