@chocozhang/three-model-render 1.0.4 → 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.
- package/CHANGELOG.md +39 -0
- package/README.md +46 -6
- package/dist/camera/index.js +6 -10
- package/dist/camera/index.js.map +1 -1
- package/dist/camera/index.mjs +6 -10
- package/dist/camera/index.mjs.map +1 -1
- package/dist/core/index.d.ts +21 -1
- package/dist/core/index.js +70 -9
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +70 -10
- package/dist/core/index.mjs.map +1 -1
- package/dist/effect/index.js +185 -230
- package/dist/effect/index.js.map +1 -1
- package/dist/effect/index.mjs +185 -230
- package/dist/effect/index.mjs.map +1 -1
- package/dist/index.d.ts +50 -23
- package/dist/index.js +793 -801
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +789 -802
- package/dist/index.mjs.map +1 -1
- package/dist/interaction/index.js +7 -9
- package/dist/interaction/index.js.map +1 -1
- package/dist/interaction/index.mjs +7 -9
- package/dist/interaction/index.mjs.map +1 -1
- package/dist/loader/index.d.ts +17 -2
- package/dist/loader/index.js +385 -386
- package/dist/loader/index.js.map +1 -1
- package/dist/loader/index.mjs +384 -387
- package/dist/loader/index.mjs.map +1 -1
- package/dist/setup/index.d.ts +13 -21
- package/dist/setup/index.js +120 -167
- package/dist/setup/index.js.map +1 -1
- package/dist/setup/index.mjs +119 -168
- package/dist/setup/index.mjs.map +1 -1
- package/dist/ui/index.js +15 -17
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/index.mjs +15 -17
- package/dist/ui/index.mjs.map +1 -1
- package/package.json +49 -21
package/dist/effect/index.mjs
CHANGED
|
@@ -1,37 +1,5 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
2
|
|
|
3
|
-
/******************************************************************************
|
|
4
|
-
Copyright (c) Microsoft Corporation.
|
|
5
|
-
|
|
6
|
-
Permission to use, copy, modify, and/or distribute this software for any
|
|
7
|
-
purpose with or without fee is hereby granted.
|
|
8
|
-
|
|
9
|
-
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
10
|
-
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
11
|
-
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
12
|
-
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
13
|
-
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
14
|
-
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
15
|
-
PERFORMANCE OF THIS SOFTWARE.
|
|
16
|
-
***************************************************************************** */
|
|
17
|
-
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
function __awaiter(thisArg, _arguments, P, generator) {
|
|
21
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
22
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
23
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
24
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
25
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
26
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
31
|
-
var e = new Error(message);
|
|
32
|
-
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
3
|
/**
|
|
36
4
|
* @file exploder.ts
|
|
37
5
|
* @description
|
|
@@ -98,205 +66,197 @@ class GroupExploder {
|
|
|
98
66
|
* @param newSet The new set of meshes
|
|
99
67
|
* @param contextId Optional context ID to distinguish business scenarios
|
|
100
68
|
*/
|
|
101
|
-
setMeshes(newSet, options) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
69
|
+
async setMeshes(newSet, options) {
|
|
70
|
+
const autoRestorePrev = options?.autoRestorePrev ?? true;
|
|
71
|
+
const restoreDuration = options?.restoreDuration ?? 300;
|
|
72
|
+
this.log(`setMeshes called. newSetSize=${newSet ? newSet.size : 0}, autoRestorePrev=${autoRestorePrev}`);
|
|
73
|
+
// If the newSet is null and currentSet is null -> nothing
|
|
74
|
+
if (!newSet && !this.currentSet) {
|
|
75
|
+
this.log('setMeshes: both newSet and currentSet are null, nothing to do');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// If both exist and are the same reference, we still must detect content changes.
|
|
79
|
+
const sameReference = this.currentSet === newSet;
|
|
80
|
+
// Prepare prevSet snapshot (we copy current to prev)
|
|
81
|
+
if (this.currentSet) {
|
|
82
|
+
this.prevSet = this.currentSet;
|
|
83
|
+
this.prevStateMap = new Map(this.stateMap);
|
|
84
|
+
this.log(`setMeshes: backed up current->prev prevSetSize=${this.prevSet.size}`);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
this.prevSet = null;
|
|
88
|
+
this.prevStateMap = new Map();
|
|
89
|
+
}
|
|
90
|
+
// If we used to be exploded and need to restore prevSet, do that first (await)
|
|
91
|
+
if (this.prevSet && autoRestorePrev && this.isExploded) {
|
|
92
|
+
this.log('setMeshes: need to restore prevSet before applying newSet');
|
|
93
|
+
await this.restoreSet(this.prevSet, this.prevStateMap, restoreDuration, { debug: true });
|
|
94
|
+
this.log('setMeshes: prevSet restore done');
|
|
95
|
+
this.prevStateMap.clear();
|
|
96
|
+
this.prevSet = null;
|
|
97
|
+
}
|
|
98
|
+
// Now register newSet: we clear and rebuild stateMap carefully.
|
|
99
|
+
// But we must handle the case where caller reuses same Set object and just mutated elements.
|
|
100
|
+
// We will compute additions and removals.
|
|
101
|
+
const oldSet = this.currentSet;
|
|
102
|
+
this.currentSet = newSet;
|
|
103
|
+
// If newSet is null -> simply clear stateMap
|
|
104
|
+
if (!this.currentSet) {
|
|
105
|
+
this.stateMap.clear();
|
|
106
|
+
this.log('setMeshes: newSet is null -> cleared stateMap');
|
|
107
|
+
this.isExploded = false;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// If we have oldSet (could be same reference) then compute diffs
|
|
111
|
+
if (oldSet) {
|
|
112
|
+
// If same reference but size or content differs -> handle diffs
|
|
113
|
+
const wasSameRef = sameReference;
|
|
114
|
+
let added = [];
|
|
115
|
+
let removed = [];
|
|
116
|
+
// Build maps of membership
|
|
117
|
+
const oldMembers = new Set(Array.from(oldSet));
|
|
118
|
+
const newMembers = new Set(Array.from(this.currentSet));
|
|
119
|
+
// find removals
|
|
120
|
+
oldMembers.forEach((m) => {
|
|
121
|
+
if (!newMembers.has(m))
|
|
122
|
+
removed.push(m);
|
|
123
|
+
});
|
|
124
|
+
// find additions
|
|
125
|
+
newMembers.forEach((m) => {
|
|
126
|
+
if (!oldMembers.has(m))
|
|
127
|
+
added.push(m);
|
|
128
|
+
});
|
|
129
|
+
if (wasSameRef && added.length === 0 && removed.length === 0) {
|
|
130
|
+
// truly identical (no content changes)
|
|
131
|
+
this.log('setMeshes: same reference and identical contents -> nothing to update');
|
|
142
132
|
return;
|
|
143
133
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
let removed = [];
|
|
150
|
-
// Build maps of membership
|
|
151
|
-
const oldMembers = new Set(Array.from(oldSet));
|
|
152
|
-
const newMembers = new Set(Array.from(this.currentSet));
|
|
153
|
-
// find removals
|
|
154
|
-
oldMembers.forEach((m) => {
|
|
155
|
-
if (!newMembers.has(m))
|
|
156
|
-
removed.push(m);
|
|
157
|
-
});
|
|
158
|
-
// find additions
|
|
159
|
-
newMembers.forEach((m) => {
|
|
160
|
-
if (!oldMembers.has(m))
|
|
161
|
-
added.push(m);
|
|
162
|
-
});
|
|
163
|
-
if (wasSameRef && added.length === 0 && removed.length === 0) {
|
|
164
|
-
// truly identical (no content changes)
|
|
165
|
-
this.log('setMeshes: same reference and identical contents -> nothing to update');
|
|
166
|
-
return;
|
|
134
|
+
this.log(`setMeshes: diff detected -> added=${added.length}, removed=${removed.length}`);
|
|
135
|
+
// Remove snapshots for removed meshes
|
|
136
|
+
removed.forEach((m) => {
|
|
137
|
+
if (this.stateMap.has(m)) {
|
|
138
|
+
this.stateMap.delete(m);
|
|
167
139
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
this.stateMap.clear();
|
|
184
|
-
yield this.ensureSnapshotsForSet(this.currentSet);
|
|
185
|
-
this.log(`setMeshes: recorded stateMap entries for newSet size=${this.stateMap.size}`);
|
|
186
|
-
this.isExploded = false;
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
});
|
|
140
|
+
});
|
|
141
|
+
// Ensure snapshots exist for current set members (create for newly added meshes)
|
|
142
|
+
await this.ensureSnapshotsForSet(this.currentSet);
|
|
143
|
+
this.log(`setMeshes: after diff handling, stateMap size=${this.stateMap.size}`);
|
|
144
|
+
this.isExploded = false;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
// no oldSet -> brand new registration
|
|
149
|
+
this.stateMap.clear();
|
|
150
|
+
await this.ensureSnapshotsForSet(this.currentSet);
|
|
151
|
+
this.log(`setMeshes: recorded stateMap entries for newSet size=${this.stateMap.size}`);
|
|
152
|
+
this.isExploded = false;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
190
155
|
}
|
|
191
156
|
/**
|
|
192
157
|
* ensureSnapshotsForSet: for each mesh in set, ensure stateMap has an entry.
|
|
193
158
|
* If missing, record current matrixWorld as originalMatrixWorld (best-effort).
|
|
194
159
|
*/
|
|
195
|
-
ensureSnapshotsForSet(set) {
|
|
196
|
-
|
|
197
|
-
|
|
160
|
+
async ensureSnapshotsForSet(set) {
|
|
161
|
+
set.forEach((m) => {
|
|
162
|
+
try {
|
|
163
|
+
m.updateMatrixWorld(true);
|
|
164
|
+
}
|
|
165
|
+
catch { }
|
|
166
|
+
if (!this.stateMap.has(m)) {
|
|
198
167
|
try {
|
|
199
|
-
|
|
168
|
+
this.stateMap.set(m, {
|
|
169
|
+
originalParent: m.parent || null,
|
|
170
|
+
originalMatrixWorld: (m.matrixWorld && m.matrixWorld.clone()) || new THREE.Matrix4().copy(m.matrix),
|
|
171
|
+
});
|
|
172
|
+
// Also store in userData for extra resilience
|
|
173
|
+
m.userData.__originalMatrixWorld = this.stateMap.get(m).originalMatrixWorld.clone();
|
|
200
174
|
}
|
|
201
|
-
catch (
|
|
202
|
-
|
|
203
|
-
try {
|
|
204
|
-
this.stateMap.set(m, {
|
|
205
|
-
originalParent: m.parent || null,
|
|
206
|
-
originalMatrixWorld: (m.matrixWorld && m.matrixWorld.clone()) || new THREE.Matrix4().copy(m.matrix),
|
|
207
|
-
});
|
|
208
|
-
// Also store in userData for extra resilience
|
|
209
|
-
m.userData.__originalMatrixWorld = this.stateMap.get(m).originalMatrixWorld.clone();
|
|
210
|
-
}
|
|
211
|
-
catch (e) {
|
|
212
|
-
this.log(`ensureSnapshotsForSet: failed to snapshot mesh ${m.name || m.id}: ${e.message}`);
|
|
213
|
-
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
this.log(`ensureSnapshotsForSet: failed to snapshot mesh ${m.name || m.id}: ${e.message}`);
|
|
214
177
|
}
|
|
215
|
-
}
|
|
178
|
+
}
|
|
216
179
|
});
|
|
217
180
|
}
|
|
218
181
|
/**
|
|
219
182
|
* explode: compute targets first, compute targetBound using targets + mesh radii,
|
|
220
183
|
* animate camera to that targetBound, then animate meshes to targets.
|
|
221
184
|
*/
|
|
222
|
-
explode(opts) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
185
|
+
async explode(opts) {
|
|
186
|
+
if (!this.currentSet || this.currentSet.size === 0) {
|
|
187
|
+
this.log('explode: empty currentSet, nothing to do');
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const { spacing = 2, duration = 1000, lift = 0.5, cameraPadding = 1.5, mode = 'spiral', dimOthers = { enabled: true, opacity: 0.25 }, debug = false, } = opts || {};
|
|
191
|
+
this.log(`explode called. setSize=${this.currentSet.size}, mode=${mode}, spacing=${spacing}, duration=${duration}, lift=${lift}, dim=${dimOthers.enabled}`);
|
|
192
|
+
this.cancelAnimations();
|
|
193
|
+
const meshes = Array.from(this.currentSet);
|
|
194
|
+
// ensure snapshots exist for any meshes that may have been added after initial registration
|
|
195
|
+
await this.ensureSnapshotsForSet(this.currentSet);
|
|
196
|
+
// compute center/radius from current meshes (fallback)
|
|
197
|
+
const initial = this.computeBoundingSphereForMeshes(meshes);
|
|
198
|
+
const center = initial.center;
|
|
199
|
+
const baseRadius = Math.max(1, initial.radius);
|
|
200
|
+
this.log(`explode: initial center=${center.toArray().map((n) => n.toFixed(3))}, baseRadius=${baseRadius.toFixed(3)}`);
|
|
201
|
+
// compute targets (pure calculation)
|
|
202
|
+
const targets = this.computeTargetsByMode(meshes, center, baseRadius + spacing, { lift, mode });
|
|
203
|
+
this.log(`explode: computed ${targets.length} target positions`);
|
|
204
|
+
// compute target-based bounding sphere (targets + per-mesh radius)
|
|
205
|
+
const targetBound = this.computeBoundingSphereForPositionsAndMeshes(targets, meshes);
|
|
206
|
+
this.log(`explode: targetBound center=${targetBound.center.toArray().map((n) => n.toFixed(3))}, radius=${targetBound.radius.toFixed(3)}`);
|
|
207
|
+
await this.animateCameraToFit(targetBound.center, targetBound.radius, { duration: Math.min(600, duration), padding: cameraPadding });
|
|
208
|
+
this.log('explode: camera animation to target bound completed');
|
|
209
|
+
// apply dim if needed with context id
|
|
210
|
+
const contextId = dimOthers?.enabled ? this.applyDimToOthers(meshes, dimOthers.opacity ?? 0.25, { debug }) : null;
|
|
211
|
+
if (contextId)
|
|
212
|
+
this.log(`explode: applied dim for context ${contextId}`);
|
|
213
|
+
// capture starts after camera move
|
|
214
|
+
const starts = meshes.map((m) => {
|
|
215
|
+
const v = new THREE.Vector3();
|
|
216
|
+
try {
|
|
217
|
+
m.getWorldPosition(v);
|
|
228
218
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
this.log(`explode: applied dim for context ${contextId}`);
|
|
252
|
-
// capture starts after camera move
|
|
253
|
-
const starts = meshes.map((m) => {
|
|
254
|
-
const v = new THREE.Vector3();
|
|
255
|
-
try {
|
|
256
|
-
m.getWorldPosition(v);
|
|
257
|
-
}
|
|
258
|
-
catch (_a) {
|
|
259
|
-
// fallback to originalMatrixWorld if available
|
|
260
|
-
const st = this.stateMap.get(m);
|
|
261
|
-
if (st)
|
|
262
|
-
v.setFromMatrixPosition(st.originalMatrixWorld);
|
|
263
|
-
}
|
|
264
|
-
return v;
|
|
265
|
-
});
|
|
266
|
-
const startTime = performance.now();
|
|
267
|
-
const total = Math.max(1, duration);
|
|
268
|
-
const tick = (now) => {
|
|
269
|
-
const t = Math.min(1, (now - startTime) / total);
|
|
270
|
-
const eased = easeInOutQuad(t);
|
|
271
|
-
for (let i = 0; i < meshes.length; i++) {
|
|
272
|
-
const m = meshes[i];
|
|
273
|
-
const s = starts[i];
|
|
274
|
-
const tar = targets[i];
|
|
275
|
-
const cur = s.clone().lerp(tar, eased);
|
|
276
|
-
if (m.parent) {
|
|
277
|
-
const local = cur.clone();
|
|
278
|
-
m.parent.worldToLocal(local);
|
|
279
|
-
m.position.copy(local);
|
|
280
|
-
}
|
|
281
|
-
else {
|
|
282
|
-
m.position.copy(cur);
|
|
283
|
-
}
|
|
284
|
-
m.updateMatrix();
|
|
285
|
-
}
|
|
286
|
-
if (this.controls && typeof this.controls.update === 'function')
|
|
287
|
-
this.controls.update();
|
|
288
|
-
if (t < 1) {
|
|
289
|
-
this.animId = requestAnimationFrame(tick);
|
|
219
|
+
catch {
|
|
220
|
+
// fallback to originalMatrixWorld if available
|
|
221
|
+
const st = this.stateMap.get(m);
|
|
222
|
+
if (st)
|
|
223
|
+
v.setFromMatrixPosition(st.originalMatrixWorld);
|
|
224
|
+
}
|
|
225
|
+
return v;
|
|
226
|
+
});
|
|
227
|
+
const startTime = performance.now();
|
|
228
|
+
const total = Math.max(1, duration);
|
|
229
|
+
const tick = (now) => {
|
|
230
|
+
const t = Math.min(1, (now - startTime) / total);
|
|
231
|
+
const eased = easeInOutQuad(t);
|
|
232
|
+
for (let i = 0; i < meshes.length; i++) {
|
|
233
|
+
const m = meshes[i];
|
|
234
|
+
const s = starts[i];
|
|
235
|
+
const tar = targets[i];
|
|
236
|
+
const cur = s.clone().lerp(tar, eased);
|
|
237
|
+
if (m.parent) {
|
|
238
|
+
const local = cur.clone();
|
|
239
|
+
m.parent.worldToLocal(local);
|
|
240
|
+
m.position.copy(local);
|
|
290
241
|
}
|
|
291
242
|
else {
|
|
292
|
-
|
|
293
|
-
this.isExploded = true;
|
|
294
|
-
this.log(`explode: completed. contextId=${contextId !== null && contextId !== void 0 ? contextId : 'none'}`);
|
|
243
|
+
m.position.copy(cur);
|
|
295
244
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
245
|
+
m.updateMatrix();
|
|
246
|
+
}
|
|
247
|
+
if (this.controls && typeof this.controls.update === 'function')
|
|
248
|
+
this.controls.update();
|
|
249
|
+
if (t < 1) {
|
|
250
|
+
this.animId = requestAnimationFrame(tick);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
this.animId = null;
|
|
254
|
+
this.isExploded = true;
|
|
255
|
+
this.log(`explode: completed. contextId=${contextId ?? 'none'}`);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
this.animId = requestAnimationFrame(tick);
|
|
259
|
+
return;
|
|
300
260
|
}
|
|
301
261
|
/**
|
|
302
262
|
* Restore all exploded meshes to their original transform:
|
|
@@ -319,7 +279,7 @@ class GroupExploder {
|
|
|
319
279
|
*/
|
|
320
280
|
restoreSet(set, stateMap, duration = 400, opts) {
|
|
321
281
|
if (!set || set.size === 0) {
|
|
322
|
-
if (opts
|
|
282
|
+
if (opts?.debug)
|
|
323
283
|
this.log('restoreSet: empty set, nothing to restore');
|
|
324
284
|
return Promise.resolve();
|
|
325
285
|
}
|
|
@@ -332,12 +292,12 @@ class GroupExploder {
|
|
|
332
292
|
try {
|
|
333
293
|
m.updateMatrixWorld(true);
|
|
334
294
|
}
|
|
335
|
-
catch
|
|
295
|
+
catch { }
|
|
336
296
|
const s = new THREE.Vector3();
|
|
337
297
|
try {
|
|
338
298
|
m.getWorldPosition(s);
|
|
339
299
|
}
|
|
340
|
-
catch
|
|
300
|
+
catch {
|
|
341
301
|
s.set(0, 0, 0);
|
|
342
302
|
}
|
|
343
303
|
starts.push(s);
|
|
@@ -435,7 +395,7 @@ class GroupExploder {
|
|
|
435
395
|
});
|
|
436
396
|
}
|
|
437
397
|
// material dim with context id
|
|
438
|
-
applyDimToOthers(explodingMeshes, opacity = 0.25,
|
|
398
|
+
applyDimToOthers(explodingMeshes, opacity = 0.25, _opts) {
|
|
439
399
|
const contextId = `ctx_${Date.now()}_${Math.floor(Math.random() * 10000)}`;
|
|
440
400
|
const explodingSet = new Set(explodingMeshes);
|
|
441
401
|
const touched = new Set();
|
|
@@ -446,11 +406,10 @@ class GroupExploder {
|
|
|
446
406
|
if (explodingSet.has(mesh))
|
|
447
407
|
return;
|
|
448
408
|
const applyMat = (mat) => {
|
|
449
|
-
var _a;
|
|
450
409
|
if (!this.materialSnaps.has(mat)) {
|
|
451
410
|
this.materialSnaps.set(mat, {
|
|
452
411
|
transparent: !!mat.transparent,
|
|
453
|
-
opacity:
|
|
412
|
+
opacity: mat.opacity ?? 1,
|
|
454
413
|
depthWrite: mat.depthWrite,
|
|
455
414
|
});
|
|
456
415
|
}
|
|
@@ -477,7 +436,7 @@ class GroupExploder {
|
|
|
477
436
|
return contextId;
|
|
478
437
|
}
|
|
479
438
|
// clean contexts for meshes (restore materials whose contexts are removed)
|
|
480
|
-
cleanContextsForMeshes(
|
|
439
|
+
cleanContextsForMeshes(_meshes) {
|
|
481
440
|
// conservative strategy: for each context we created, delete it and restore materials accordingly
|
|
482
441
|
for (const [contextId, mats] of Array.from(this.contextMaterials.entries())) {
|
|
483
442
|
mats.forEach((mat) => {
|
|
@@ -591,7 +550,7 @@ class GroupExploder {
|
|
|
591
550
|
}
|
|
592
551
|
}
|
|
593
552
|
}
|
|
594
|
-
catch
|
|
553
|
+
catch {
|
|
595
554
|
radius = 0;
|
|
596
555
|
}
|
|
597
556
|
if (!isFinite(radius) || radius < 0 || radius > 1e8)
|
|
@@ -614,10 +573,9 @@ class GroupExploder {
|
|
|
614
573
|
}
|
|
615
574
|
// computeTargetsByMode (unchanged logic but pure function)
|
|
616
575
|
computeTargetsByMode(meshes, center, baseRadius, opts) {
|
|
617
|
-
var _a, _b;
|
|
618
576
|
const n = meshes.length;
|
|
619
|
-
const lift =
|
|
620
|
-
const mode =
|
|
577
|
+
const lift = opts.lift ?? 0.5;
|
|
578
|
+
const mode = opts.mode ?? 'ring';
|
|
621
579
|
const targets = [];
|
|
622
580
|
if (mode === 'ring') {
|
|
623
581
|
for (let i = 0; i < n; i++) {
|
|
@@ -659,9 +617,8 @@ class GroupExploder {
|
|
|
659
617
|
return targets;
|
|
660
618
|
}
|
|
661
619
|
animateCameraToFit(targetCenter, targetRadius, opts) {
|
|
662
|
-
|
|
663
|
-
const
|
|
664
|
-
const padding = (_b = opts === null || opts === void 0 ? void 0 : opts.padding) !== null && _b !== void 0 ? _b : 1.5;
|
|
620
|
+
const duration = opts?.duration ?? 600;
|
|
621
|
+
const padding = opts?.padding ?? 1.5;
|
|
665
622
|
if (!(this.camera instanceof THREE.PerspectiveCamera)) {
|
|
666
623
|
if (this.controls && this.controls.target) {
|
|
667
624
|
// Fallback for non-PerspectiveCamera
|
|
@@ -673,14 +630,13 @@ class GroupExploder {
|
|
|
673
630
|
const endPos = endTarget.clone().add(dir.multiplyScalar(dist));
|
|
674
631
|
const startTime = performance.now();
|
|
675
632
|
const tick = (now) => {
|
|
676
|
-
var _a;
|
|
677
633
|
const t = Math.min(1, (now - startTime) / duration);
|
|
678
634
|
const k = easeInOutQuad(t);
|
|
679
635
|
if (this.controls && this.controls.target) {
|
|
680
636
|
this.controls.target.lerpVectors(startTarget, endTarget, k);
|
|
681
637
|
}
|
|
682
638
|
this.camera.position.lerpVectors(startPos, endPos, k);
|
|
683
|
-
if (
|
|
639
|
+
if (this.controls?.update)
|
|
684
640
|
this.controls.update();
|
|
685
641
|
if (t < 1) {
|
|
686
642
|
this.cameraAnimId = requestAnimationFrame(tick);
|
|
@@ -703,8 +659,8 @@ class GroupExploder {
|
|
|
703
659
|
const distH = targetRadius / Math.sin(Math.min(fov, fov * aspect) / 2); // approximate
|
|
704
660
|
const dist = Math.max(distV, distH) * padding;
|
|
705
661
|
const startPos = this.camera.position.clone();
|
|
706
|
-
const startTarget =
|
|
707
|
-
if (!
|
|
662
|
+
const startTarget = this.controls?.target ? this.controls.target.clone() : new THREE.Vector3(); // assumption
|
|
663
|
+
if (!this.controls?.target) {
|
|
708
664
|
this.camera.getWorldDirection(startTarget);
|
|
709
665
|
startTarget.add(startPos);
|
|
710
666
|
}
|
|
@@ -717,13 +673,12 @@ class GroupExploder {
|
|
|
717
673
|
return new Promise((resolve) => {
|
|
718
674
|
const startTime = performance.now();
|
|
719
675
|
const tick = (now) => {
|
|
720
|
-
var _a, _b;
|
|
721
676
|
const t = Math.min(1, (now - startTime) / duration);
|
|
722
677
|
const k = easeInOutQuad(t);
|
|
723
678
|
this.camera.position.lerpVectors(startPos, endPos, k);
|
|
724
679
|
if (this.controls && this.controls.target) {
|
|
725
680
|
this.controls.target.lerpVectors(startTarget, endTarget, k);
|
|
726
|
-
|
|
681
|
+
this.controls.update?.();
|
|
727
682
|
}
|
|
728
683
|
else {
|
|
729
684
|
this.camera.lookAt(endTarget); // simple lookAt if no controls
|