sa2kit 1.1.0 → 1.2.0

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/mmd/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  'use strict';
2
2
 
3
3
  require('../chunk-DGUM43GV.js');
4
- var React2 = require('react');
5
- var THREE2 = require('three');
4
+ var React6 = require('react');
5
+ var THREE = require('three');
6
6
  var threeStdlib = require('three-stdlib');
7
+ var lucideReact = require('lucide-react');
7
8
 
8
9
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
10
 
@@ -25,77 +26,44 @@ function _interopNamespace(e) {
25
26
  return Object.freeze(n);
26
27
  }
27
28
 
28
- var React2__default = /*#__PURE__*/_interopDefault(React2);
29
- var THREE2__namespace = /*#__PURE__*/_interopNamespace(THREE2);
29
+ var React6__default = /*#__PURE__*/_interopDefault(React6);
30
+ var THREE__namespace = /*#__PURE__*/_interopNamespace(THREE);
30
31
 
31
32
  // src/mmd/utils/ammo-loader.ts
32
33
  var ammoPromise = null;
33
- var loadAmmo = (config) => {
34
- const configKey = `${config.scriptPath}|${config.wasmBasePath}`;
35
- const currentConfigKey = window.__AMMO_CONFIG_KEY__;
36
- if (ammoPromise && currentConfigKey === configKey) {
34
+ var loadAmmo = (path = "/libs/ammo.wasm.js") => {
35
+ if (ammoPromise) {
37
36
  return ammoPromise;
38
37
  }
39
- if (currentConfigKey && currentConfigKey !== configKey) {
40
- ammoPromise = null;
41
- window.Ammo = void 0;
42
- }
43
38
  ammoPromise = new Promise((resolve, reject) => {
44
- if (typeof window === "undefined") {
45
- resolve();
39
+ if (typeof window.Ammo === "function") {
40
+ window.Ammo().then((lib) => {
41
+ resolve(lib);
42
+ });
46
43
  return;
47
44
  }
48
- if (window.Ammo && currentConfigKey === configKey) {
49
- console.log("\u2705 [Ammo] \u5DF2\u52A0\u8F7D\uFF0C\u76F4\u63A5\u4F7F\u7528");
50
- resolve();
45
+ if (typeof window.Ammo === "object") {
46
+ resolve(window.Ammo);
51
47
  return;
52
48
  }
53
- console.log("\u{1F4E6} [Ammo] \u5F00\u59CB\u52A0\u8F7D Ammo.js...");
54
- console.log("\u{1F4C2} [Ammo] \u811A\u672C\u8DEF\u5F84:", config.scriptPath);
55
- console.log("\u{1F4C2} [Ammo] WASM \u57FA\u7840\u8DEF\u5F84:", config.wasmBasePath);
56
- window.__AMMO_CONFIG_KEY__ = configKey;
57
- window.AMMO_PATH = config.wasmBasePath;
58
49
  const script = document.createElement("script");
59
- script.src = config.scriptPath;
50
+ script.src = path;
60
51
  script.async = true;
61
52
  script.onload = () => {
62
- console.log("\u2705 [Ammo] \u811A\u672C\u52A0\u8F7D\u5B8C\u6210\uFF0C\u7B49\u5F85\u521D\u59CB\u5316...");
63
- const checkAmmo = () => {
64
- if (typeof window.Ammo === "function") {
65
- console.log("\u{1F504} [Ammo] \u5F00\u59CB\u521D\u59CB\u5316 WASM...");
66
- window.Ammo({
67
- locateFile: (path) => {
68
- console.log("\u{1F4CD} [Ammo] \u5B9A\u4F4D\u6587\u4EF6:", path);
69
- if (path.endsWith(".wasm")) {
70
- return config.wasmBasePath + path;
71
- }
72
- return path;
73
- }
74
- }).then((AmmoLib) => {
75
- console.log("\u2705 [Ammo] \u521D\u59CB\u5316\u5B8C\u6210\uFF01");
76
- window.Ammo = AmmoLib;
77
- resolve();
78
- }).catch((err) => {
79
- console.error("\u274C [Ammo] \u521D\u59CB\u5316\u5931\u8D25:", err);
80
- reject(err);
81
- });
82
- } else {
83
- if (window.Ammo) {
84
- console.log("\u2705 [Ammo] \u5DF2\u521D\u59CB\u5316");
85
- resolve();
86
- } else {
87
- console.log("\u23F3 [Ammo] \u7B49\u5F85\u521D\u59CB\u5316...");
88
- setTimeout(checkAmmo, 100);
89
- }
90
- }
91
- };
92
- checkAmmo();
53
+ if (typeof window.Ammo === "function") {
54
+ window.Ammo().then((lib) => {
55
+ resolve(lib || window.Ammo);
56
+ }).catch((err) => {
57
+ console.error("Ammo initialization failed:", err);
58
+ reject(err);
59
+ });
60
+ } else {
61
+ reject(new Error("Ammo.js loaded but window.Ammo is not a function"));
62
+ }
93
63
  };
94
- script.onerror = (e) => {
95
- console.error("\u274C [Ammo] \u52A0\u8F7D\u5931\u8D25:", e);
96
- reject(new Error(`Failed to load Ammo.js from ${config.scriptPath}. Please ensure the file exists.`));
97
- ammoPromise = null;
98
- window.__AMMO_CONFIG_KEY__ = void 0;
64
+ script.onerror = (err) => {
65
+ console.error("Failed to load Ammo.js script:", err);
66
+ reject(new Error(`Failed to load Ammo.js from ${path}`));
99
67
  };
100
68
  document.body.appendChild(script);
101
69
  });
@@ -103,1650 +71,1554 @@ var loadAmmo = (config) => {
103
71
  };
104
72
 
105
73
  // src/mmd/components/MMDPlayerBase.tsx
106
- var MMDPlayerBase = ({
107
- modelUrl,
108
- vmdUrl,
109
- cameraUrl,
110
- audioUrl,
111
- physics = true,
112
- width = "100%",
113
- height = "100%",
114
- onLoad,
115
- onProgress,
116
- onError
117
- }) => {
118
- const containerRef = React2.useRef(null);
119
- const [loading, setLoading] = React2.useState(true);
120
- const [error, setError] = React2.useState(null);
121
- React2.useEffect(() => {
74
+ var MMDPlayerBase = React6.forwardRef((props, ref) => {
75
+ const {
76
+ resources,
77
+ stage = {},
78
+ mobileOptimization = { enabled: true },
79
+ autoPlay = false,
80
+ loop = true,
81
+ volume = 1,
82
+ muted = false,
83
+ showAxes = false,
84
+ onLoad,
85
+ onLoadProgress,
86
+ onError,
87
+ onPlay,
88
+ onPause,
89
+ onEnded,
90
+ onTimeUpdate,
91
+ className,
92
+ style
93
+ } = props;
94
+ const containerRef = React6.useRef(null);
95
+ const sceneRef = React6.useRef(null);
96
+ const cameraRef = React6.useRef(null);
97
+ const rendererRef = React6.useRef(null);
98
+ const controlsRef = React6.useRef(null);
99
+ const helperRef = React6.useRef(null);
100
+ const axesHelperRef = React6.useRef(null);
101
+ const clockRef = React6.useRef(new THREE__namespace.Clock());
102
+ const animationIdRef = React6.useRef(null);
103
+ const resizeObserverRef = React6.useRef(null);
104
+ const isReadyRef = React6.useRef(false);
105
+ const isPlayingRef = React6.useRef(false);
106
+ const initIdRef = React6.useRef(0);
107
+ const durationRef = React6.useRef(0);
108
+ const animationClipRef = React6.useRef(null);
109
+ const loopRef = React6.useRef(loop);
110
+ const audioRef = React6.useRef(null);
111
+ const physicsComponentsRef = React6.useRef({
112
+ configs: [],
113
+ dispatchers: [],
114
+ caches: [],
115
+ solvers: [],
116
+ worlds: []
117
+ });
118
+ const startTimeRef = React6.useRef(Date.now());
119
+ const modelSwitchCountRef = React6.useRef(0);
120
+ React6.useImperativeHandle(ref, () => ({
121
+ play: () => {
122
+ if (!isReadyRef.current) return;
123
+ isPlayingRef.current = true;
124
+ if (!clockRef.current.running) clockRef.current.start();
125
+ onPlay?.();
126
+ },
127
+ pause: () => {
128
+ if (!isPlayingRef.current) return;
129
+ isPlayingRef.current = false;
130
+ clockRef.current.stop();
131
+ onPause?.();
132
+ },
133
+ stop: () => {
134
+ isPlayingRef.current = false;
135
+ clockRef.current.stop();
136
+ onPause?.();
137
+ },
138
+ seek: (time) => {
139
+ console.warn("Seek not fully implemented in MMDPlayerBase yet");
140
+ },
141
+ getCurrentTime: () => {
142
+ const elapsed = clockRef.current.elapsedTime;
143
+ const duration = durationRef.current;
144
+ if (duration > 0 && loopRef.current) {
145
+ return elapsed % duration;
146
+ }
147
+ return elapsed;
148
+ },
149
+ getDuration: () => durationRef.current,
150
+ isPlaying: () => isPlayingRef.current,
151
+ snapshot: () => {
152
+ if (!rendererRef.current) return "";
153
+ if (sceneRef.current && cameraRef.current) {
154
+ rendererRef.current.render(sceneRef.current, cameraRef.current);
155
+ }
156
+ return rendererRef.current.domElement.toDataURL("image/png");
157
+ }
158
+ }));
159
+ React6.useEffect(() => {
122
160
  if (!containerRef.current) return;
123
- let scene;
124
- let camera;
125
- let renderer;
126
- let effect;
127
- let helper;
128
- let clock;
129
- let animationId;
130
161
  const init = async () => {
162
+ const myId = ++initIdRef.current;
163
+ const checkCancelled = () => {
164
+ return myId !== initIdRef.current || !containerRef.current;
165
+ };
166
+ if (containerRef.current) {
167
+ containerRef.current.innerHTML = "";
168
+ }
169
+ physicsComponentsRef.current = {
170
+ configs: [],
171
+ dispatchers: [],
172
+ caches: [],
173
+ solvers: [],
174
+ worlds: []
175
+ };
176
+ if (modelSwitchCountRef.current === 0) {
177
+ startTimeRef.current = Date.now();
178
+ modelSwitchCountRef.current = 1;
179
+ console.log("[MMDPlayerBase] \u{1F550} \u7CFB\u7EDF\u542F\u52A8\u65F6\u95F4:", new Date(startTimeRef.current).toLocaleString());
180
+ } else {
181
+ modelSwitchCountRef.current++;
182
+ const runningTime = Date.now() - startTimeRef.current;
183
+ const minutes = Math.floor(runningTime / 6e4);
184
+ const seconds = Math.floor(runningTime % 6e4 / 1e3);
185
+ console.log(`[MMDPlayerBase] \u{1F504} \u6A21\u578B\u5207\u6362 #${modelSwitchCountRef.current} (\u8FD0\u884C\u65F6\u95F4: ${minutes}\u5206${seconds}\u79D2)`);
186
+ }
131
187
  try {
132
- setLoading(true);
133
- setError(null);
134
- if (physics) {
135
- await loadAmmo({
136
- scriptPath: "/mikutalking/libs/ammo.wasm.js",
137
- wasmBasePath: "/mikutalking/libs/"
138
- });
188
+ if (stage.enablePhysics !== false && !mobileOptimization.disablePhysics) {
189
+ console.log("[MMDPlayerBase] Loading Ammo.js physics engine...");
190
+ await loadAmmo(stage.physicsPath);
191
+ if (checkCancelled()) return;
192
+ console.log("[MMDPlayerBase] Ammo.js loaded successfully");
193
+ const Ammo = window.Ammo;
194
+ if (Ammo) {
195
+ console.log("[MMDPlayerBase] Setting up physics component tracking...");
196
+ const originalBtDefaultCollisionConfiguration = Ammo.btDefaultCollisionConfiguration;
197
+ const originalBtCollisionDispatcher = Ammo.btCollisionDispatcher;
198
+ const originalBtDbvtBroadphase = Ammo.btDbvtBroadphase;
199
+ const originalBtSequentialImpulseConstraintSolver = Ammo.btSequentialImpulseConstraintSolver;
200
+ const originalBtDiscreteDynamicsWorld = Ammo.btDiscreteDynamicsWorld;
201
+ const componentsRef = physicsComponentsRef.current;
202
+ Ammo.btDefaultCollisionConfiguration = function(...args) {
203
+ const obj = new originalBtDefaultCollisionConfiguration(...args);
204
+ componentsRef.configs.push(obj);
205
+ console.log(`[MMDPlayerBase] \u{1F50D} Captured btDefaultCollisionConfiguration #${componentsRef.configs.length}`);
206
+ return obj;
207
+ };
208
+ Ammo.btCollisionDispatcher = function(...args) {
209
+ const obj = new originalBtCollisionDispatcher(...args);
210
+ componentsRef.dispatchers.push(obj);
211
+ console.log(`[MMDPlayerBase] \u{1F50D} Captured btCollisionDispatcher #${componentsRef.dispatchers.length}`);
212
+ return obj;
213
+ };
214
+ Ammo.btDbvtBroadphase = function(...args) {
215
+ const obj = new originalBtDbvtBroadphase(...args);
216
+ componentsRef.caches.push(obj);
217
+ console.log(`[MMDPlayerBase] \u{1F50D} Captured btDbvtBroadphase #${componentsRef.caches.length}`);
218
+ return obj;
219
+ };
220
+ Ammo.btSequentialImpulseConstraintSolver = function(...args) {
221
+ const obj = new originalBtSequentialImpulseConstraintSolver(...args);
222
+ componentsRef.solvers.push(obj);
223
+ console.log(`[MMDPlayerBase] \u{1F50D} Captured btSequentialImpulseConstraintSolver #${componentsRef.solvers.length}`);
224
+ return obj;
225
+ };
226
+ Ammo.btDiscreteDynamicsWorld = function(...args) {
227
+ const obj = new originalBtDiscreteDynamicsWorld(...args);
228
+ componentsRef.worlds.push(obj);
229
+ console.log(`[MMDPlayerBase] \u{1F50D} Captured btDiscreteDynamicsWorld #${componentsRef.worlds.length}`);
230
+ return obj;
231
+ };
232
+ console.log("[MMDPlayerBase] \u2705 Physics component tracking setup complete");
233
+ }
234
+ } else {
235
+ console.log("[MMDPlayerBase] Physics disabled");
139
236
  }
140
237
  const container = containerRef.current;
141
- const w = container.clientWidth;
142
- const h = container.clientHeight;
143
- scene = new THREE2__namespace.Scene();
144
- scene.background = new THREE2__namespace.Color(16777215);
145
- camera = new THREE2__namespace.PerspectiveCamera(45, w / h, 1, 2e3);
146
- camera.position.z = 30;
147
- const ambient = new THREE2__namespace.AmbientLight(6710886);
148
- scene.add(ambient);
149
- const directionalLight = new THREE2__namespace.DirectionalLight(8943462);
150
- directionalLight.position.set(-1, 1, 1).normalize();
151
- scene.add(directionalLight);
152
- renderer = new THREE2__namespace.WebGLRenderer({ antialias: true });
153
- renderer.setPixelRatio(window.devicePixelRatio);
154
- renderer.setSize(w, h);
238
+ const width = container.clientWidth || 300;
239
+ const height = container.clientHeight || 150;
240
+ const scene = new THREE__namespace.Scene();
241
+ if (stage.backgroundColor) {
242
+ scene.background = new THREE__namespace.Color(stage.backgroundColor);
243
+ }
244
+ sceneRef.current = scene;
245
+ const camera = new THREE__namespace.PerspectiveCamera(45, width / height, 0.1, 2e3);
246
+ if (stage.cameraPosition) {
247
+ const pos = stage.cameraPosition;
248
+ camera.position.set(pos.x, pos.y, pos.z);
249
+ } else {
250
+ camera.position.set(0, 20, 30);
251
+ }
252
+ cameraRef.current = camera;
253
+ const renderer = new THREE__namespace.WebGLRenderer({
254
+ antialias: !mobileOptimization.enabled,
255
+ alpha: true,
256
+ preserveDrawingBuffer: true
257
+ });
258
+ renderer.setSize(width, height);
259
+ renderer.setPixelRatio(mobileOptimization.enabled ? mobileOptimization.pixelRatio || 1 : window.devicePixelRatio);
260
+ if (checkCancelled()) {
261
+ renderer.dispose();
262
+ return;
263
+ }
264
+ container.innerHTML = "";
265
+ renderer.domElement.style.display = "block";
266
+ renderer.domElement.style.width = "100%";
267
+ renderer.domElement.style.height = "100%";
268
+ renderer.domElement.style.outline = "none";
269
+ if (stage.enableShadow !== false && !mobileOptimization.reduceShadowQuality) {
270
+ renderer.shadowMap.enabled = true;
271
+ renderer.shadowMap.type = THREE__namespace.PCFSoftShadowMap;
272
+ }
155
273
  container.appendChild(renderer.domElement);
156
- effect = new threeStdlib.OutlineEffect(renderer);
157
- helper = new threeStdlib.MMDAnimationHelper({
274
+ rendererRef.current = renderer;
275
+ const ambientLight = new THREE__namespace.AmbientLight(16777215, stage.ambientLightIntensity ?? 0.5);
276
+ scene.add(ambientLight);
277
+ const dirLight = new THREE__namespace.DirectionalLight(16777215, stage.directionalLightIntensity ?? 0.8);
278
+ dirLight.position.set(10, 20, 10);
279
+ if (stage.enableShadow !== false) {
280
+ dirLight.castShadow = true;
281
+ dirLight.shadow.mapSize.width = mobileOptimization.enabled ? 1024 : 2048;
282
+ dirLight.shadow.mapSize.height = mobileOptimization.enabled ? 1024 : 2048;
283
+ dirLight.shadow.bias = -1e-4;
284
+ }
285
+ scene.add(dirLight);
286
+ const controls = new threeStdlib.OrbitControls(camera, renderer.domElement);
287
+ controls.minDistance = 10;
288
+ controls.maxDistance = 100;
289
+ if (stage.cameraTarget) {
290
+ const target = stage.cameraTarget;
291
+ controls.target.set(target.x, target.y, target.z);
292
+ } else {
293
+ controls.target.set(0, 10, 0);
294
+ }
295
+ controls.update();
296
+ controlsRef.current = controls;
297
+ if (showAxes) {
298
+ const axesHelper = new THREE__namespace.AxesHelper(20);
299
+ scene.add(axesHelper);
300
+ axesHelperRef.current = axesHelper;
301
+ }
302
+ const onResize = () => {
303
+ if (!containerRef.current || !cameraRef.current || !rendererRef.current) return;
304
+ const w = containerRef.current.clientWidth;
305
+ const h = containerRef.current.clientHeight;
306
+ if (w === 0 || h === 0) return;
307
+ cameraRef.current.aspect = w / h;
308
+ cameraRef.current.updateProjectionMatrix();
309
+ rendererRef.current.setSize(w, h);
310
+ };
311
+ const resizeObserver = new ResizeObserver(onResize);
312
+ resizeObserver.observe(container);
313
+ resizeObserverRef.current = resizeObserver;
314
+ onResize();
315
+ console.log("[MMDPlayerBase] Start loading resources...", resources);
316
+ const loader = new threeStdlib.MMDLoader();
317
+ const helper = new threeStdlib.MMDAnimationHelper({
158
318
  afterglow: 2
159
319
  });
160
- const loader = new threeStdlib.MMDLoader();
161
- loader.load(
162
- modelUrl,
163
- (mesh) => {
164
- scene.add(mesh);
165
- if (vmdUrl) {
166
- loader.loadAnimation(
167
- vmdUrl,
168
- mesh,
169
- (vmdObject) => {
170
- helper.add(mesh, {
171
- animation: vmdObject,
172
- physics
173
- });
174
- },
175
- (xhr) => {
176
- if (onProgress) onProgress(xhr);
177
- },
178
- (err) => {
179
- console.error("Error loading animation", err);
180
- if (onError) onError(err);
181
- }
182
- );
183
- } else {
184
- helper.add(mesh, { physics });
185
- }
186
- if (cameraUrl) {
187
- loader.loadAnimation(
188
- cameraUrl,
189
- camera,
190
- (cameraVmdObject) => {
191
- helper.add(camera, {
192
- animation: cameraVmdObject
193
- });
194
- },
195
- void 0,
196
- (err) => {
197
- console.error("Error loading camera motion", err);
320
+ helperRef.current = helper;
321
+ const loadModelPromise = new Promise((resolve, reject) => {
322
+ if (resources.motionPath) {
323
+ console.log("[MMDPlayerBase] Loading model with motion:", resources.motionPath);
324
+ loader.loadWithAnimation(
325
+ resources.modelPath,
326
+ resources.motionPath,
327
+ (mmd) => {
328
+ resolve({ mesh: mmd.mesh, animation: mmd.animation });
329
+ },
330
+ (xhr) => {
331
+ if (xhr.lengthComputable) {
332
+ const percent = xhr.loaded / xhr.total * 100;
333
+ onLoadProgress?.(percent, "model+motion");
198
334
  }
199
- );
200
- }
201
- if (audioUrl) {
202
- new THREE2__namespace.AudioLoader().load(
203
- audioUrl,
204
- (buffer) => {
205
- const listener = new THREE2__namespace.AudioListener();
206
- camera.add(listener);
207
- const audio = new THREE2__namespace.Audio(listener);
208
- audio.setBuffer(buffer);
209
- helper.add(audio);
210
- },
211
- void 0,
212
- (err) => {
213
- console.error("Error loading audio", err);
335
+ },
336
+ (err) => reject(err)
337
+ );
338
+ } else {
339
+ console.log("[MMDPlayerBase] Loading model only");
340
+ loader.load(
341
+ resources.modelPath,
342
+ (mesh2) => {
343
+ resolve({ mesh: mesh2 });
344
+ },
345
+ (xhr) => {
346
+ if (xhr.lengthComputable) {
347
+ const percent = xhr.loaded / xhr.total * 100;
348
+ onLoadProgress?.(percent, "model");
214
349
  }
350
+ },
351
+ (err) => reject(err)
352
+ );
353
+ }
354
+ });
355
+ const { mesh, animation } = await loadModelPromise;
356
+ if (checkCancelled()) return;
357
+ console.log("[MMDPlayerBase] Model loaded:", mesh);
358
+ if (animation) {
359
+ animationClipRef.current = animation;
360
+ durationRef.current = animation.duration;
361
+ console.log("[MMDPlayerBase] Animation duration:", animation.duration);
362
+ }
363
+ const box = new THREE__namespace.Box3().setFromObject(mesh);
364
+ if (!box.isEmpty()) {
365
+ const center = box.getCenter(new THREE__namespace.Vector3());
366
+ const size = box.getSize(new THREE__namespace.Vector3());
367
+ console.log("[MMDPlayerBase] Model bounds:", { center, size });
368
+ if (!stage.cameraTarget) {
369
+ controls.target.set(center.x, center.y + size.y * 0.35, center.z);
370
+ if (!stage.cameraPosition) {
371
+ const maxDim = Math.max(size.x, size.y, size.z);
372
+ const dist = maxDim * 2;
373
+ camera.position.set(
374
+ center.x,
375
+ // X: 水平对齐
376
+ center.y + size.y * 0.6,
377
+ // Y: 稍高于模型中心(眼睛平视或略俯视)
378
+ center.z + dist
379
+ // Z: 在模型正前方(+Z 方向)
215
380
  );
381
+ console.log("[MMDPlayerBase] Auto camera position:", camera.position);
216
382
  }
217
- if (onLoad) onLoad();
218
- setLoading(false);
219
- },
220
- (xhr) => {
221
- if (onProgress) onProgress(xhr);
222
- },
223
- (err) => {
224
- setError("Failed to load model");
225
- if (onError) onError(err);
226
- setLoading(false);
383
+ controls.update();
227
384
  }
228
- );
229
- clock = new THREE2__namespace.Clock();
230
- const animate = () => {
231
- animationId = requestAnimationFrame(animate);
232
- helper.update(clock.getDelta());
233
- effect.render(scene, camera);
234
- };
385
+ }
386
+ mesh.castShadow = true;
387
+ mesh.receiveShadow = true;
388
+ const enablePhysics = stage.enablePhysics !== false && !mobileOptimization.disablePhysics;
389
+ helper.add(mesh, {
390
+ animation,
391
+ physics: enablePhysics
392
+ });
393
+ scene.add(mesh);
394
+ if (resources.cameraPath) {
395
+ loader.loadAnimation(
396
+ resources.cameraPath,
397
+ camera,
398
+ (cameraAnimation) => {
399
+ if (checkCancelled()) return;
400
+ helper.add(camera, {
401
+ animation: cameraAnimation
402
+ });
403
+ },
404
+ void 0,
405
+ (err) => console.error("Failed to load camera motion:", err)
406
+ );
407
+ }
408
+ if (resources.audioPath) {
409
+ const listener = new THREE__namespace.AudioListener();
410
+ camera.add(listener);
411
+ const sound = new THREE__namespace.Audio(listener);
412
+ const audioLoader = new THREE__namespace.AudioLoader();
413
+ audioLoader.load(
414
+ resources.audioPath,
415
+ (buffer) => {
416
+ if (checkCancelled()) return;
417
+ sound.setBuffer(buffer);
418
+ sound.setLoop(loopRef.current);
419
+ sound.setVolume(volume);
420
+ audioRef.current = sound;
421
+ helper.add(sound, {
422
+ delay: 0,
423
+ duration: buffer.duration
424
+ });
425
+ },
426
+ void 0,
427
+ (err) => console.error("Failed to load audio:", err)
428
+ );
429
+ }
430
+ if (resources.stageModelPath) {
431
+ loader.load(
432
+ resources.stageModelPath,
433
+ (stageMesh) => {
434
+ if (checkCancelled()) return;
435
+ stageMesh.castShadow = true;
436
+ stageMesh.receiveShadow = true;
437
+ scene.add(stageMesh);
438
+ },
439
+ void 0,
440
+ (err) => console.error("Failed to load stage:", err)
441
+ );
442
+ }
443
+ if (checkCancelled()) return;
444
+ isReadyRef.current = true;
445
+ onLoad?.();
446
+ if (autoPlay) {
447
+ setTimeout(() => {
448
+ if (checkCancelled()) return;
449
+ isPlayingRef.current = true;
450
+ if (!clockRef.current.running) clockRef.current.start();
451
+ onPlay?.();
452
+ }, 100);
453
+ }
235
454
  animate();
236
- const handleResize = () => {
237
- if (!container) return;
238
- const width2 = container.clientWidth;
239
- const height2 = container.clientHeight;
240
- camera.aspect = width2 / height2;
241
- camera.updateProjectionMatrix();
242
- effect.setSize(width2, height2);
243
- };
244
- window.addEventListener("resize", handleResize);
245
- return () => {
246
- window.removeEventListener("resize", handleResize);
247
- };
248
- } catch (e) {
249
- console.error(e);
250
- setError("Initialization error");
251
- setLoading(false);
455
+ } catch (error) {
456
+ if (checkCancelled()) return;
457
+ console.error("MMDPlayerBase initialization failed:", error);
458
+ const errorMessage = error instanceof Error ? error.message : String(error);
459
+ if (errorMessage.includes("OOM") || errorMessage.includes("out of memory")) {
460
+ const runningTime = Date.now() - startTimeRef.current;
461
+ const hours = Math.floor(runningTime / 36e5);
462
+ const minutes = Math.floor(runningTime % 36e5 / 6e4);
463
+ const seconds = Math.floor(runningTime % 6e4 / 1e3);
464
+ const timeString = hours > 0 ? `${hours}\u5C0F\u65F6${minutes}\u5206${seconds}\u79D2` : minutes > 0 ? `${minutes}\u5206${seconds}\u79D2` : `${seconds}\u79D2`;
465
+ alert(`\u26A0\uFE0F \u5185\u5B58\u6EA2\u51FA\u9519\u8BEF (OOM)
466
+
467
+ \u{1F4CA} \u7CFB\u7EDF\u8FD0\u884C\u7EDF\u8BA1\uFF1A
468
+ \u2022 \u8FD0\u884C\u65F6\u95F4: ${timeString}
469
+ \u2022 \u6A21\u578B\u5207\u6362\u6B21\u6570: ${modelSwitchCountRef.current}
470
+ \u2022 \u542F\u52A8\u65F6\u95F4: ${new Date(startTimeRef.current).toLocaleString()}
471
+ \u2022 \u9519\u8BEF\u65F6\u95F4: ${(/* @__PURE__ */ new Date()).toLocaleString()}
472
+
473
+ \u274C \u95EE\u9898\uFF1A\u7269\u7406\u5F15\u64CE\u5185\u5B58\u4E0D\u8DB3\uFF01
474
+ \u8FD9\u901A\u5E38\u610F\u5473\u7740\u4E4B\u524D\u7684\u7269\u7406\u4E16\u754C\u6CA1\u6709\u6B63\u786E\u6E05\u7406\u3002
475
+
476
+ \u{1F50D} \u9519\u8BEF\u8BE6\u60C5\uFF1A
477
+ ${errorMessage}
478
+
479
+ \u{1F4A1} \u5EFA\u8BAE\uFF1A\u8BF7\u5237\u65B0\u9875\u9762\u6216\u8054\u7CFB\u5F00\u53D1\u8005`);
480
+ }
481
+ onError?.(error instanceof Error ? error : new Error(String(error)));
252
482
  }
253
- return void 0;
254
483
  };
255
484
  init();
256
485
  return () => {
257
- if (animationId) cancelAnimationFrame(animationId);
258
- if (renderer) {
259
- renderer.dispose();
260
- const domElement = renderer.domElement;
261
- if (domElement && domElement.parentNode) {
262
- domElement.parentNode.removeChild(domElement);
263
- }
264
- }
265
- };
266
- }, [modelUrl, vmdUrl, cameraUrl, audioUrl, physics]);
267
- return /* @__PURE__ */ React2__default.default.createElement(
268
- "div",
269
- {
270
- ref: containerRef,
271
- style: { width, height, position: "relative", overflow: "hidden" }
272
- },
273
- loading && /* @__PURE__ */ React2__default.default.createElement("div", { style: {
274
- position: "absolute",
275
- top: 0,
276
- left: 0,
277
- width: "100%",
278
- height: "100%",
279
- display: "flex",
280
- justifyContent: "center",
281
- alignItems: "center",
282
- background: "#f0f0f0",
283
- color: "#666",
284
- zIndex: 1
285
- } }, "Loading MMD..."),
286
- error && /* @__PURE__ */ React2__default.default.createElement("div", { style: {
287
- position: "absolute",
288
- top: 0,
289
- left: 0,
290
- width: "100%",
291
- height: "100%",
292
- display: "flex",
293
- justifyContent: "center",
294
- alignItems: "center",
295
- background: "#ffeeee",
296
- color: "#cc0000",
297
- zIndex: 2
298
- } }, error)
299
- );
300
- };
301
- var MMDPlayerEnhanced = React2.forwardRef(({
302
- resources,
303
- resourcesList,
304
- defaultResourceId,
305
- resourceOptions,
306
- defaultSelection,
307
- stage,
308
- autoPlay = false,
309
- loop = false,
310
- className = "",
311
- style,
312
- onLoad,
313
- onError,
314
- onResourceChange,
315
- onSelectionChange,
316
- onAudioEnded,
317
- onAnimationEnded
318
- }, ref) => {
319
- console.log("\u{1F3A8} [MMDPlayerEnhanced] \u7EC4\u4EF6\u521D\u59CB\u5316");
320
- const [selectedResourceId, setSelectedResourceId] = React2.useState(
321
- defaultResourceId || resourcesList?.[0]?.id || ""
322
- );
323
- const [selectedModelId, setSelectedModelId] = React2.useState(
324
- defaultSelection?.modelId || resourceOptions?.models?.[0]?.id || ""
325
- );
326
- const [selectedMotionId, setSelectedMotionId] = React2.useState(
327
- defaultSelection?.motionId || ""
328
- );
329
- const [selectedAudioId, setSelectedAudioId] = React2.useState(
330
- defaultSelection?.audioId || ""
331
- );
332
- const [selectedCameraId, setSelectedCameraId] = React2.useState(
333
- defaultSelection?.cameraId || ""
334
- );
335
- const [selectedStageModelId, setSelectedStageModelId] = React2.useState(
336
- defaultSelection?.stageModelId || ""
337
- );
338
- const [selectedBackgroundId, setSelectedBackgroundId] = React2.useState(
339
- defaultSelection?.backgroundId || ""
340
- );
341
- const [showSettings, setShowSettings] = React2.useState(false);
342
- const [expandedSection, setExpandedSection] = React2.useState(null);
343
- const currentResources = React2.useMemo(() => {
344
- if (resourceOptions) {
345
- const model = resourceOptions.models?.find((m) => m.id === selectedModelId);
346
- const motion = resourceOptions.motions?.find((m) => m.id === selectedMotionId);
347
- const audio = resourceOptions.audios?.find((a) => a.id === selectedAudioId);
348
- const camera = resourceOptions.cameras?.find((c) => c.id === selectedCameraId);
349
- const stageModel = resourceOptions.stageModels?.find((s) => s.id === selectedStageModelId);
350
- const background = resourceOptions.backgrounds?.find((b) => b.id === selectedBackgroundId);
351
- return {
352
- modelPath: model?.path || resourceOptions.models?.[0]?.path || "",
353
- motionPath: motion?.path,
354
- audioPath: audio?.path,
355
- cameraPath: camera?.path,
356
- stageModelPath: stageModel?.path,
357
- backgroundPath: background?.path
358
- };
359
- }
360
- if (resourcesList && resourcesList.length > 0) {
361
- const selected = resourcesList.find((r) => r.id === selectedResourceId);
362
- const resourceItem = selected || resourcesList[0];
363
- if (!resourceItem) {
364
- throw new Error("\u65E0\u6CD5\u627E\u5230\u6709\u6548\u7684\u8D44\u6E90\u914D\u7F6E");
486
+ console.log("[MMDPlayerBase] Cleanup started");
487
+ initIdRef.current++;
488
+ if (animationIdRef.current) {
489
+ cancelAnimationFrame(animationIdRef.current);
490
+ animationIdRef.current = null;
365
491
  }
366
- return resourceItem.resources;
367
- }
368
- if (!resources) {
369
- throw new Error("\u5FC5\u987B\u63D0\u4F9B resources\u3001resourcesList \u6216 resourceOptions");
370
- }
371
- return resources;
372
- }, [
373
- resources,
374
- resourcesList,
375
- selectedResourceId,
376
- resourceOptions,
377
- selectedModelId,
378
- selectedMotionId,
379
- selectedAudioId,
380
- selectedCameraId,
381
- selectedStageModelId,
382
- selectedBackgroundId
383
- ]);
384
- console.log("\u{1F4C2} [MMDPlayerEnhanced] \u5F53\u524D\u8D44\u6E90\u914D\u7F6E:", currentResources);
385
- console.log("\u{1F3AD} [MMDPlayerEnhanced] \u821E\u53F0\u914D\u7F6E:", stage);
386
- const containerRef = React2.useRef(null);
387
- const rendererRef = React2.useRef(null);
388
- const sceneRef = React2.useRef(null);
389
- const cameraRef = React2.useRef(null);
390
- const controlsRef = React2.useRef(null);
391
- const helperRef = React2.useRef(null);
392
- const clockRef = React2.useRef(new THREE2__namespace.Clock());
393
- const audioRef = React2.useRef(null);
394
- const animationIdRef = React2.useRef(null);
395
- const isPlayingRef = React2.useRef(false);
396
- const isLoadedRef = React2.useRef(false);
397
- const shouldAutoPlayAfterReloadRef = React2.useRef(false);
398
- const vmdDataRef = React2.useRef(null);
399
- const animationDurationRef = React2.useRef(0);
400
- const hasAudioRef = React2.useRef(false);
401
- const animationEndedFiredRef = React2.useRef(false);
402
- const lastAnimationTimeRef = React2.useRef(0);
403
- const animationStoppedCountRef = React2.useRef(0);
404
- const [loading, setLoading] = React2.useState(false);
405
- const [loadingProgress, setLoadingProgress] = React2.useState(0);
406
- const [error, setError] = React2.useState(null);
407
- const [isPlaying, setIsPlaying] = React2.useState(false);
408
- const [isInitialized, setIsInitialized] = React2.useState(false);
409
- const [reloadTrigger, setReloadTrigger] = React2.useState(0);
410
- const [needReset, setNeedReset] = React2.useState(false);
411
- React2.useImperativeHandle(ref, () => ({
412
- clearResources: () => {
413
- console.log("\u{1F9F9} [MMDPlayerEnhanced] \u5916\u90E8\u89E6\u53D1\u8D44\u6E90\u6E05\u7406");
414
- clearOldResources();
415
- },
416
- getIsPlaying: () => isPlayingRef.current,
417
- getIsLoaded: () => isLoadedRef.current,
418
- stopCompletely: () => {
419
- console.log("\u23F9\uFE0F [MMDPlayerEnhanced] \u5B8C\u5168\u505C\u6B62");
420
- stopCompletely();
421
- }
422
- }));
423
- const stopCompletely = () => {
424
- isPlayingRef.current = false;
425
- setIsPlaying(false);
426
- if (audioRef.current) {
427
- audioRef.current.pause();
428
- audioRef.current.currentTime = 0;
429
- }
430
- if (helperRef.current) {
431
- helperRef.current.enable("physics", false);
432
- }
433
- animationEndedFiredRef.current = false;
434
- lastAnimationTimeRef.current = 0;
435
- animationStoppedCountRef.current = 0;
436
- console.log("\u2705 [MMDPlayerEnhanced] \u5B8C\u5168\u505C\u6B62\u5B8C\u6210");
437
- };
438
- React2.useEffect(() => {
439
- const container = containerRef.current;
440
- if (!container) return;
441
- const handleCleanupResources = () => {
442
- console.log("\u{1F9F9} [MMDPlayerEnhanced] \u6536\u5230\u6E05\u7406\u8D44\u6E90\u4E8B\u4EF6");
443
- if (!isPlayingRef.current) {
444
- clearOldResources();
445
- } else {
446
- console.warn("\u26A0\uFE0F [MMDPlayerEnhanced] \u64AD\u653E\u4E2D\uFF0C\u8DF3\u8FC7\u8D44\u6E90\u6E05\u7406");
492
+ isPlayingRef.current = false;
493
+ isReadyRef.current = false;
494
+ if (resizeObserverRef.current) {
495
+ resizeObserverRef.current.disconnect();
496
+ resizeObserverRef.current = null;
447
497
  }
448
- };
449
- const handleStopCompletely = () => {
450
- console.log("\u23F9\uFE0F [MMDPlayerEnhanced] \u6536\u5230\u5B8C\u5168\u505C\u6B62\u4E8B\u4EF6");
451
- stopCompletely();
452
- };
453
- container.addEventListener("cleanupResources", handleCleanupResources);
454
- container.addEventListener("stopCompletely", handleStopCompletely);
455
- return () => {
456
- container.removeEventListener("cleanupResources", handleCleanupResources);
457
- container.removeEventListener("stopCompletely", handleStopCompletely);
458
- };
459
- }, []);
460
- React2.useEffect(() => {
461
- console.log("\u{1F3D7}\uFE0F [MMDPlayerEnhanced] \u573A\u666F\u521D\u59CB\u5316 useEffect \u89E6\u53D1");
462
- if (!containerRef.current) {
463
- console.warn("\u26A0\uFE0F [MMDPlayerEnhanced] containerRef.current \u4E0D\u5B58\u5728");
464
- return;
465
- }
466
- console.log("\u2705 [MMDPlayerEnhanced] \u5BB9\u5668\u5143\u7D20\u5B58\u5728\uFF0C\u5F00\u59CB\u521D\u59CB\u5316\u573A\u666F");
467
- const container = containerRef.current;
468
- if (container.children.length > 0) {
469
- console.log("\u26A0\uFE0F [MMDPlayerEnhanced] \u573A\u666F\u5DF2\u7ECF\u521D\u59CB\u5316\uFF0C\u8DF3\u8FC7");
470
- return;
471
- }
472
- const width = container.clientWidth;
473
- const height = container.clientHeight;
474
- const scene = new THREE2__namespace.Scene();
475
- scene.background = new THREE2__namespace.Color(stage?.backgroundColor || "#000000");
476
- sceneRef.current = scene;
477
- const camera = new THREE2__namespace.PerspectiveCamera(45, width / height, 1, 2e3);
478
- const camPos = stage?.cameraPosition || { x: 0, y: 10, z: 30 };
479
- camera.position.set(camPos.x, camPos.y, camPos.z);
480
- cameraRef.current = camera;
481
- const renderer = new THREE2__namespace.WebGLRenderer({ antialias: true });
482
- renderer.setSize(width, height);
483
- renderer.setPixelRatio(window.devicePixelRatio);
484
- container.appendChild(renderer.domElement);
485
- rendererRef.current = renderer;
486
- const ambient = new THREE2__namespace.AmbientLight(16777215, 0.6);
487
- scene.add(ambient);
488
- const directionalLight = new THREE2__namespace.DirectionalLight(16777215, 0.8);
489
- directionalLight.position.set(1, 1, 1);
490
- scene.add(directionalLight);
491
- if (stage?.showGrid !== false) {
492
- const gridHelper = new THREE2__namespace.PolarGridHelper(30, 10);
493
- scene.add(gridHelper);
494
- }
495
- const controls = new threeStdlib.OrbitControls(camera, renderer.domElement);
496
- const target = stage?.cameraTarget || { x: 0, y: 10, z: 0 };
497
- controls.target.set(target.x, target.y, target.z);
498
- controls.update();
499
- controlsRef.current = controls;
500
- const handleResize = () => {
501
- if (!container || !camera || !renderer) return;
502
- const newWidth = container.clientWidth;
503
- const newHeight = container.clientHeight;
504
- camera.aspect = newWidth / newHeight;
505
- camera.updateProjectionMatrix();
506
- renderer.setSize(newWidth, newHeight);
507
- };
508
- window.addEventListener("resize", handleResize);
509
- const animate = () => {
510
- animationIdRef.current = requestAnimationFrame(animate);
511
- if (helperRef.current && isPlayingRef.current) {
512
- const delta = clockRef.current.getDelta();
498
+ if (audioRef.current) {
513
499
  try {
514
- helperRef.current.update(delta);
515
- } catch (error2) {
516
- if (error2.message && error2.message.includes("OOM")) {
517
- console.error("\u274C \u7269\u7406\u5F15\u64CE\u5185\u5B58\u6EA2\u51FA\uFF0C\u505C\u6B62\u64AD\u653E");
518
- isPlayingRef.current = false;
519
- setIsPlaying(false);
520
- onError?.(new Error("\u7269\u7406\u5F15\u64CE\u5185\u5B58\u6EA2\u51FA"));
521
- return;
500
+ if (audioRef.current.isPlaying) {
501
+ audioRef.current.stop();
522
502
  }
523
- throw error2;
524
- }
525
- if (!hasAudioRef.current && !loop && !animationEndedFiredRef.current) {
526
- const currentTime = clockRef.current.getElapsedTime();
527
- if (animationDurationRef.current > 0) {
528
- if (currentTime >= animationDurationRef.current - 0.1) {
529
- console.log("\u{1F3AC} [MMDPlayerEnhanced] \u52A8\u753B\u64AD\u653E\u7ED3\u675F\uFF08\u65F6\u957F\u5224\u5B9A\uFF09");
530
- animationEndedFiredRef.current = true;
531
- isPlayingRef.current = false;
532
- setIsPlaying(false);
533
- onAnimationEnded?.();
534
- }
535
- } else {
536
- if (Math.abs(currentTime - lastAnimationTimeRef.current) < 1e-3) {
537
- animationStoppedCountRef.current++;
538
- if (animationStoppedCountRef.current > 30) {
539
- console.log("\u{1F3AC} [MMDPlayerEnhanced] \u52A8\u753B\u64AD\u653E\u7ED3\u675F\uFF08\u505C\u6B62\u68C0\u6D4B\uFF09");
540
- animationEndedFiredRef.current = true;
541
- isPlayingRef.current = false;
542
- setIsPlaying(false);
543
- onAnimationEnded?.();
544
- }
545
- } else {
546
- animationStoppedCountRef.current = 0;
547
- }
548
- lastAnimationTimeRef.current = currentTime;
503
+ if (audioRef.current.source) {
504
+ audioRef.current.disconnect();
549
505
  }
506
+ if (audioRef.current.buffer) {
507
+ audioRef.current.buffer = null;
508
+ }
509
+ audioRef.current = null;
510
+ } catch (e) {
511
+ console.warn("[MMDPlayerBase] Error cleaning up audio:", e);
550
512
  }
551
513
  }
552
- if (controlsRef.current) {
553
- controlsRef.current.update();
554
- }
555
- if (renderer && scene && camera) {
556
- renderer.render(scene, camera);
557
- }
558
- };
559
- animate();
560
- setIsInitialized(true);
561
- console.log("\u2705 [MMDPlayerEnhanced] \u573A\u666F\u521D\u59CB\u5316\u5B8C\u6210");
562
- return () => {
563
- window.removeEventListener("resize", handleResize);
564
- if (animationIdRef.current) {
565
- cancelAnimationFrame(animationIdRef.current);
566
- }
567
- if (renderer) {
568
- renderer.dispose();
569
- if (renderer.domElement && renderer.domElement.parentNode) {
570
- container.removeChild(renderer.domElement);
571
- }
572
- }
573
- if (controls) {
574
- controls.dispose();
575
- }
576
- };
577
- }, [stage]);
578
- const clearOldResources = () => {
579
- if (!sceneRef.current) return;
580
- if (isPlayingRef.current) {
581
- isPlayingRef.current = false;
582
- setIsPlaying(false);
583
- }
584
- if (audioRef.current) {
585
- audioRef.current.pause();
586
- audioRef.current.currentTime = 0;
587
- audioRef.current.onended = null;
588
- audioRef.current.src = "";
589
- audioRef.current.load();
590
- audioRef.current = null;
591
- }
592
- if (helperRef.current) {
593
- try {
594
- helperRef.current.enable("animation", false);
595
- helperRef.current.enable("ik", false);
596
- helperRef.current.enable("grant", false);
597
- helperRef.current.enable("physics", false);
598
- const helperObjects = helperRef.current.objects;
599
- if (helperObjects && Array.isArray(helperObjects)) {
600
- const physicsWorldsToDestroy = /* @__PURE__ */ new Set();
601
- helperObjects.forEach((obj) => {
602
- if (obj.physics) {
603
- try {
604
- const physics = obj.physics;
605
- if (physics.world) {
606
- physicsWorldsToDestroy.add(physics.world);
514
+ if (helperRef.current) {
515
+ try {
516
+ console.log("[MMDPlayerBase] Cleaning up AnimationHelper");
517
+ const helperObjects = helperRef.current.objects;
518
+ const meshes = helperRef.current.meshes || [];
519
+ console.log("[MMDPlayerBase] Found meshes count:", meshes.length);
520
+ if (meshes && Array.isArray(meshes) && meshes.length > 0) {
521
+ meshes.forEach((mesh, idx) => {
522
+ console.log(`[MMDPlayerBase] Cleaning mesh ${idx}:`, mesh.uuid);
523
+ let meshData = null;
524
+ if (helperObjects instanceof WeakMap) {
525
+ console.log("[MMDPlayerBase] Accessing WeakMap with mesh as key...");
526
+ meshData = helperObjects.get(mesh);
527
+ if (meshData) {
528
+ const meshDataKeys = Object.keys(meshData);
529
+ console.log(`[MMDPlayerBase] \u2705 Got meshData from WeakMap, keys (${meshDataKeys.length}):`, meshDataKeys);
530
+ const physicsRelatedKeys = meshDataKeys.filter((k) => k.toLowerCase().includes("phys"));
531
+ if (physicsRelatedKeys.length > 0) {
532
+ console.log(`[MMDPlayerBase] Physics-related keys:`, physicsRelatedKeys);
533
+ physicsRelatedKeys.forEach((key) => {
534
+ const value = meshData[key];
535
+ console.log(`[MMDPlayerBase] ${key}:`, typeof value, value?.constructor?.name || value);
536
+ });
537
+ }
538
+ } else {
539
+ console.log("[MMDPlayerBase] \u26A0\uFE0F No meshData found in WeakMap for this mesh");
607
540
  }
608
- if (physics.bodies && Array.isArray(physics.bodies)) {
609
- physics.bodies.forEach((body) => {
610
- if (physics.world && body) {
611
- try {
612
- physics.world.removeRigidBody(body);
613
- if (window.Ammo && body.destroy) {
614
- body.destroy();
541
+ }
542
+ if (!meshData) {
543
+ console.log("[MMDPlayerBase] Using mesh itself as meshData");
544
+ meshData = mesh;
545
+ }
546
+ const physics = meshData?.physics;
547
+ if (physics) {
548
+ try {
549
+ console.log("[MMDPlayerBase] \u{1F3AF} Starting physics cleanup for mesh", idx);
550
+ console.log("[MMDPlayerBase] Debug: physics object keys:", Object.keys(physics));
551
+ if (typeof physics.dispose === "function") {
552
+ console.log("[MMDPlayerBase] Calling MMDPhysics.dispose()...");
553
+ physics.dispose();
554
+ console.log("[MMDPlayerBase] \u2705 MMDPhysics.dispose() completed");
555
+ } else {
556
+ console.log("[MMDPlayerBase] No dispose method, manually cleaning physics components...");
557
+ const Ammo2 = window.Ammo;
558
+ if (!Ammo2 || !Ammo2.destroy) {
559
+ console.warn("[MMDPlayerBase] \u26A0\uFE0F Ammo.destroy not available");
560
+ } else {
561
+ if (physics.world && Array.isArray(physics.bodies) && physics.bodies.length > 0) {
562
+ console.log(`[MMDPlayerBase] Cleaning ${physics.bodies.length} rigid bodies...`);
563
+ for (let i = physics.bodies.length - 1; i >= 0; i--) {
564
+ try {
565
+ const body = physics.bodies[i];
566
+ if (body && body.body) {
567
+ physics.world.removeRigidBody(body.body);
568
+ }
569
+ } catch (e) {
570
+ console.warn(`[MMDPlayerBase] Error removing body ${i}:`, e);
571
+ }
615
572
  }
616
- } catch (e) {
573
+ physics.bodies.length = 0;
574
+ console.log("[MMDPlayerBase] \u2705 All rigid bodies removed");
617
575
  }
618
- }
619
- });
620
- physics.bodies.length = 0;
621
- physics.bodies = null;
622
- }
623
- if (physics.constraints && Array.isArray(physics.constraints)) {
624
- physics.constraints.forEach((constraint) => {
625
- if (physics.world && constraint) {
626
- try {
627
- physics.world.removeConstraint(constraint);
628
- if (window.Ammo && constraint.destroy) {
629
- constraint.destroy();
576
+ if (physics.world && Array.isArray(physics.constraints) && physics.constraints.length > 0) {
577
+ console.log(`[MMDPlayerBase] Cleaning ${physics.constraints.length} constraints...`);
578
+ for (let i = physics.constraints.length - 1; i >= 0; i--) {
579
+ try {
580
+ const constraint = physics.constraints[i];
581
+ if (constraint) {
582
+ physics.world.removeConstraint(constraint);
583
+ }
584
+ } catch (e) {
585
+ console.warn(`[MMDPlayerBase] Error removing constraint ${i}:`, e);
586
+ }
630
587
  }
631
- } catch (e) {
588
+ physics.constraints.length = 0;
589
+ console.log("[MMDPlayerBase] \u2705 All constraints removed");
632
590
  }
633
591
  }
634
- });
635
- physics.constraints.length = 0;
636
- physics.constraints = null;
592
+ }
593
+ meshData.physics = null;
594
+ console.log("[MMDPlayerBase] \u2705 Physics cleanup completed for mesh", idx);
595
+ } catch (physicsError) {
596
+ console.error("[MMDPlayerBase] \u274C Error cleaning up physics:", physicsError);
597
+ console.error("[MMDPlayerBase] Physics error stack:", physicsError.stack);
598
+ }
599
+ } else {
600
+ console.log("[MMDPlayerBase] \u26A0\uFE0F No physics object found for mesh", idx);
601
+ }
602
+ if (meshData?.mixer) {
603
+ meshData.mixer.stopAllAction();
604
+ meshData.mixer.uncacheRoot(meshData.mesh || mesh);
605
+ const clips = meshData.mixer._actions || [];
606
+ clips.forEach((action) => {
607
+ if (action._clip) {
608
+ action._clip = null;
609
+ }
610
+ });
611
+ meshData.mixer = null;
612
+ }
613
+ if (meshData?.audio) {
614
+ if (meshData.audio.isPlaying) {
615
+ meshData.audio.stop();
616
+ }
617
+ if (meshData.audio.source) {
618
+ meshData.audio.disconnect();
637
619
  }
638
- if (physics.reset) physics.reset();
639
- physics.world = null;
640
- obj.physics = null;
641
- } catch (e) {
642
- console.warn("\u6E05\u7406\u7269\u7406\u7CFB\u7EDF\u5931\u8D25:", e);
620
+ if (meshData.audio.buffer) {
621
+ meshData.audio.buffer = null;
622
+ }
623
+ meshData.audio = null;
643
624
  }
625
+ });
626
+ meshes.length = 0;
627
+ }
628
+ console.log("[MMDPlayerBase] \u{1F525} Starting CRITICAL physics components cleanup...");
629
+ const Ammo = window.Ammo;
630
+ if (Ammo && Ammo.destroy) {
631
+ const components = physicsComponentsRef.current;
632
+ console.log(`[MMDPlayerBase] \u{1F4CA} Physics components count:`, {
633
+ worlds: components.worlds.length,
634
+ solvers: components.solvers.length,
635
+ caches: components.caches.length,
636
+ dispatchers: components.dispatchers.length,
637
+ configs: components.configs.length
638
+ });
639
+ if (components.worlds.length > 0) {
640
+ console.log(`[MMDPlayerBase] \u{1F5D1}\uFE0F Destroying ${components.worlds.length} btDiscreteDynamicsWorld(s)...`);
641
+ for (let i = components.worlds.length - 1; i >= 0; i--) {
642
+ try {
643
+ Ammo.destroy(components.worlds[i]);
644
+ } catch (e) {
645
+ console.error(`[MMDPlayerBase] \u274C Error destroying world #${i}:`, e);
646
+ }
647
+ }
648
+ components.worlds.length = 0;
649
+ console.log("[MMDPlayerBase] \u2705 All btDiscreteDynamicsWorld destroyed");
644
650
  }
645
- });
646
- physicsWorldsToDestroy.forEach((world) => {
647
- try {
648
- while (world.getNumCollisionObjects() > 0) {
649
- const obj = world.getCollisionObjectArray().at(0);
650
- world.removeCollisionObject(obj);
651
- if (obj && obj.destroy) {
652
- obj.destroy();
651
+ if (components.solvers.length > 0) {
652
+ console.log(`[MMDPlayerBase] \u{1F5D1}\uFE0F Destroying ${components.solvers.length} btSequentialImpulseConstraintSolver(s)...`);
653
+ for (let i = components.solvers.length - 1; i >= 0; i--) {
654
+ try {
655
+ Ammo.destroy(components.solvers[i]);
656
+ } catch (e) {
657
+ console.error(`[MMDPlayerBase] \u274C Error destroying solver #${i}:`, e);
658
+ }
659
+ }
660
+ components.solvers.length = 0;
661
+ console.log("[MMDPlayerBase] \u2705 All btSequentialImpulseConstraintSolver destroyed");
662
+ }
663
+ if (components.caches.length > 0) {
664
+ console.log(`[MMDPlayerBase] \u{1F5D1}\uFE0F Destroying ${components.caches.length} btDbvtBroadphase(s)...`);
665
+ for (let i = components.caches.length - 1; i >= 0; i--) {
666
+ try {
667
+ Ammo.destroy(components.caches[i]);
668
+ } catch (e) {
669
+ console.error(`[MMDPlayerBase] \u274C Error destroying cache #${i}:`, e);
653
670
  }
654
671
  }
655
- if (world.destroy) {
656
- world.destroy();
672
+ components.caches.length = 0;
673
+ console.log("[MMDPlayerBase] \u2705 All btDbvtBroadphase destroyed");
674
+ }
675
+ if (components.dispatchers.length > 0) {
676
+ console.log(`[MMDPlayerBase] \u{1F5D1}\uFE0F Destroying ${components.dispatchers.length} btCollisionDispatcher(s)...`);
677
+ for (let i = components.dispatchers.length - 1; i >= 0; i--) {
678
+ try {
679
+ Ammo.destroy(components.dispatchers[i]);
680
+ } catch (e) {
681
+ console.error(`[MMDPlayerBase] \u274C Error destroying dispatcher #${i}:`, e);
682
+ }
657
683
  }
658
- } catch (e) {
659
- console.warn("\u9500\u6BC1\u7269\u7406\u4E16\u754C\u5931\u8D25:", e);
684
+ components.dispatchers.length = 0;
685
+ console.log("[MMDPlayerBase] \u2705 All btCollisionDispatcher destroyed");
660
686
  }
661
- });
662
- helperObjects.length = 0;
687
+ if (components.configs.length > 0) {
688
+ console.log(`[MMDPlayerBase] \u{1F5D1}\uFE0F Destroying ${components.configs.length} btDefaultCollisionConfiguration(s)...`);
689
+ for (let i = components.configs.length - 1; i >= 0; i--) {
690
+ try {
691
+ Ammo.destroy(components.configs[i]);
692
+ } catch (e) {
693
+ console.error(`[MMDPlayerBase] \u274C Error destroying config #${i}:`, e);
694
+ }
695
+ }
696
+ components.configs.length = 0;
697
+ console.log("[MMDPlayerBase] \u2705 All btDefaultCollisionConfiguration destroyed");
698
+ }
699
+ console.log("[MMDPlayerBase] \u{1F389} Physics components cleanup completed!");
700
+ } else {
701
+ console.warn("[MMDPlayerBase] \u26A0\uFE0F Ammo.destroy not available, skipping physics cleanup");
702
+ }
703
+ console.log("[MMDPlayerBase] Checking helper-level physics...");
704
+ if (helperRef.current.sharedPhysics) {
705
+ console.log("[MMDPlayerBase] Clearing sharedPhysics reference...");
706
+ helperRef.current.sharedPhysics = null;
707
+ }
708
+ if (helperRef.current.masterPhysics) {
709
+ console.log("[MMDPlayerBase] Clearing masterPhysics reference...");
710
+ helperRef.current.masterPhysics = null;
711
+ }
712
+ if (helperRef.current.dispose) {
713
+ helperRef.current.dispose();
714
+ }
715
+ } catch (e) {
716
+ console.warn("[MMDPlayerBase] Error cleaning up AnimationHelper:", e);
663
717
  }
664
- } catch (error2) {
665
- console.warn("\u6E05\u7406 helper \u5931\u8D25:", error2);
666
- }
667
- helperRef.current = null;
668
- }
669
- if (sceneRef.current.background && sceneRef.current.background.isTexture) {
670
- sceneRef.current.background.dispose();
671
- sceneRef.current.background = null;
672
- }
673
- if (sceneRef.current.environment && sceneRef.current.environment.isTexture) {
674
- sceneRef.current.environment.dispose();
675
- sceneRef.current.environment = null;
676
- }
677
- const objectsToRemove = [];
678
- sceneRef.current.traverse((child) => {
679
- if (child.type === "SkinnedMesh" || child.isSkinnedMesh) {
680
- objectsToRemove.push(child);
681
- }
682
- if (child.type === "Mesh" && child !== sceneRef.current) {
683
- objectsToRemove.push(child);
718
+ helperRef.current = null;
684
719
  }
685
- });
686
- objectsToRemove.forEach((obj) => {
687
- if (obj.parent) obj.parent.remove(obj);
688
- if (obj.geometry) {
689
- obj.geometry.dispose();
690
- }
691
- if (obj.material) {
692
- const disposeMaterial = (m) => {
693
- [
694
- "map",
695
- "emissiveMap",
696
- "normalMap",
697
- "bumpMap",
698
- "specularMap",
699
- "envMap",
700
- "lightMap",
701
- "aoMap",
702
- "alphaMap"
703
- ].forEach((prop) => {
704
- if (m[prop]) m[prop].dispose();
705
- });
706
- m.dispose();
707
- };
708
- const material = obj.material;
709
- if (Array.isArray(material)) {
710
- material.forEach(disposeMaterial);
711
- } else {
712
- disposeMaterial(material);
720
+ animationClipRef.current = null;
721
+ if (axesHelperRef.current) {
722
+ if (sceneRef.current) {
723
+ sceneRef.current.remove(axesHelperRef.current);
713
724
  }
725
+ axesHelperRef.current.dispose();
726
+ axesHelperRef.current = null;
714
727
  }
715
- if (obj.skeleton) {
716
- obj.skeleton = null;
717
- }
718
- });
719
- clockRef.current = new THREE2__namespace.Clock();
720
- vmdDataRef.current = null;
721
- setNeedReset(false);
722
- if (window.gc) {
723
- try {
724
- window.gc();
725
- } catch (e) {
726
- }
727
- }
728
- console.log(`\u2705 \u8D44\u6E90\u6E05\u7406\u5B8C\u6210 (${objectsToRemove.length} \u4E2A\u5BF9\u8C61)`);
729
- };
730
- React2.useEffect(() => {
731
- if (!sceneRef.current || !cameraRef.current) return;
732
- if (isLoadedRef.current) return;
733
- clearOldResources();
734
- isLoadedRef.current = true;
735
- const loadMMD = async () => {
736
- try {
737
- setLoading(true);
738
- setLoadingProgress(0);
739
- animationDurationRef.current = 0;
740
- hasAudioRef.current = false;
741
- animationEndedFiredRef.current = false;
742
- lastAnimationTimeRef.current = 0;
743
- animationStoppedCountRef.current = 0;
744
- if (stage?.enablePhysics !== false) {
745
- setLoadingProgress(5);
746
- await loadAmmo({
747
- scriptPath: stage?.ammoPath || "/mikutalking/libs/ammo.wasm.js",
748
- wasmBasePath: stage?.ammoWasmPath || "/mikutalking/libs/"
749
- });
750
- }
751
- const manager = new THREE2__namespace.LoadingManager();
752
- const basePath = currentResources.modelPath.substring(0, currentResources.modelPath.lastIndexOf("/") + 1);
753
- manager.setURLModifier((url) => {
754
- if (url.startsWith("http://") || url.startsWith("https://")) return url;
755
- if (url.startsWith("/")) return url;
756
- return basePath + url;
757
- });
758
- const loader = new threeStdlib.MMDLoader(manager);
759
- const helper = new threeStdlib.MMDAnimationHelper();
760
- helperRef.current = helper;
761
- setLoadingProgress(20);
762
- const modelStartTime = performance.now();
763
- const mesh = await new Promise((resolve, reject) => {
764
- loader.load(
765
- currentResources.modelPath,
766
- (object) => {
767
- const loadTime = ((performance.now() - modelStartTime) / 1e3).toFixed(2);
768
- console.log(`\u2705 \u6A21\u578B\u52A0\u8F7D\u5B8C\u6210 (${loadTime}s)`);
769
- resolve(object);
770
- },
771
- (progress) => {
772
- if (progress.total > 0) {
773
- setLoadingProgress(Math.min(progress.loaded / progress.total * 30 + 20, 50));
728
+ if (sceneRef.current) {
729
+ sceneRef.current.traverse((object) => {
730
+ if (object instanceof THREE__namespace.Mesh || object instanceof THREE__namespace.SkinnedMesh) {
731
+ if (object instanceof THREE__namespace.SkinnedMesh) {
732
+ if (object.skeleton) {
733
+ object.skeleton.dispose();
734
+ }
735
+ if (object.bindMatrix) {
736
+ object.bindMatrix = null;
737
+ }
738
+ if (object.bindMatrixInverse) {
739
+ object.bindMatrixInverse = null;
774
740
  }
775
- },
776
- (error2) => {
777
- console.error("\u274C \u6A21\u578B\u52A0\u8F7D\u5931\u8D25:", error2);
778
- reject(error2);
779
741
  }
780
- );
781
- });
782
- if (!sceneRef.current) {
783
- throw new Error("\u573A\u666F\u672A\u521D\u59CB\u5316");
784
- }
785
- sceneRef.current.add(mesh);
786
- if (currentResources.stageModelPath) {
787
- const stageMesh = await new Promise((resolve, reject) => {
788
- loader.load(currentResources.stageModelPath, resolve, void 0, reject);
789
- });
790
- sceneRef.current.add(stageMesh);
791
- }
792
- if (currentResources.backgroundPath && sceneRef.current) {
793
- const textureLoader = new THREE2__namespace.TextureLoader();
794
- const backgroundTexture = await new Promise((resolve, reject) => {
795
- textureLoader.load(currentResources.backgroundPath, resolve, void 0, reject);
796
- });
797
- backgroundTexture.colorSpace = THREE2__namespace.SRGBColorSpace;
798
- if (stage?.backgroundType === "skybox") {
799
- backgroundTexture.mapping = THREE2__namespace.EquirectangularReflectionMapping;
800
- sceneRef.current.background = backgroundTexture;
801
- sceneRef.current.environment = backgroundTexture;
802
- } else {
803
- sceneRef.current.background = backgroundTexture;
804
- }
805
- }
806
- let vmd = null;
807
- let cameraVmd = null;
808
- if (currentResources.motionPath) {
809
- setLoadingProgress(60);
810
- vmd = await new Promise((resolve, reject) => {
811
- loader.loadAnimation(
812
- currentResources.motionPath,
813
- mesh,
814
- resolve,
815
- (progress) => {
816
- if (progress.total > 0) {
817
- setLoadingProgress(Math.min(progress.loaded / progress.total * 20 + 60, 80));
818
- }
819
- },
820
- reject
821
- );
822
- });
823
- helper.add(mesh, {
824
- animation: vmd,
825
- physics: stage?.enablePhysics !== false
826
- });
827
- if (vmd) {
828
- let maxDuration = 0;
829
- if (vmd.duration !== void 0) {
830
- maxDuration = vmd.duration;
831
- } else if (Array.isArray(vmd) && vmd.length > 0 && vmd[0].duration !== void 0) {
832
- maxDuration = vmd[0].duration;
833
- } else if (vmd.clip && vmd.clip.duration !== void 0) {
834
- maxDuration = vmd.clip.duration;
742
+ if (object.geometry) {
743
+ object.geometry.dispose();
744
+ object.geometry = null;
835
745
  }
836
- if (maxDuration > 0) {
837
- animationDurationRef.current = maxDuration;
746
+ if (object.material) {
747
+ const disposeMaterial = (m) => {
748
+ const textureProps = [
749
+ "map",
750
+ "lightMap",
751
+ "bumpMap",
752
+ "normalMap",
753
+ "specularMap",
754
+ "envMap",
755
+ "alphaMap",
756
+ "emissiveMap",
757
+ "displacementMap",
758
+ "roughnessMap",
759
+ "metalnessMap",
760
+ "aoMap",
761
+ // MMD 特有纹理
762
+ "gradientMap",
763
+ "toonMap",
764
+ "sphereMap",
765
+ "matcap"
766
+ ];
767
+ textureProps.forEach((prop) => {
768
+ if (m[prop] && m[prop].dispose) {
769
+ m[prop].dispose();
770
+ m[prop] = null;
771
+ }
772
+ });
773
+ m.dispose();
774
+ };
775
+ if (Array.isArray(object.material)) {
776
+ object.material.forEach(disposeMaterial);
777
+ } else {
778
+ disposeMaterial(object.material);
779
+ }
780
+ object.material = null;
838
781
  }
839
782
  }
840
- } else {
841
- helper.add(mesh, { physics: stage?.enablePhysics !== false });
842
- }
843
- if (currentResources.cameraPath && cameraRef.current) {
844
- setLoadingProgress(80);
845
- cameraVmd = await new Promise((resolve, reject) => {
846
- loader.loadAnimation(currentResources.cameraPath, cameraRef.current, resolve, void 0, reject);
847
- });
848
- helper.add(cameraRef.current, { animation: cameraVmd });
849
- }
850
- if (currentResources.audioPath) {
851
- setLoadingProgress(90);
852
- const audio = new Audio(currentResources.audioPath);
853
- audio.volume = 0.5;
854
- audio.loop = loop;
855
- audioRef.current = audio;
856
- hasAudioRef.current = true;
857
- audio.onended = () => {
858
- if (!loop) {
859
- setIsPlaying(false);
860
- if (helperRef.current && sceneRef.current) {
861
- const mesh2 = sceneRef.current.children.find(
862
- (child) => child.type === "SkinnedMesh"
863
- );
864
- if (mesh2) {
865
- helperRef.current.pose(mesh2, {});
866
- }
783
+ if (object instanceof THREE__namespace.AudioListener) {
784
+ try {
785
+ if (object.context && object.context.state !== "closed") {
786
+ object.context.close?.();
867
787
  }
788
+ } catch (e) {
789
+ console.warn("[MMDPlayerBase] Error closing AudioContext:", e);
868
790
  }
869
- onAudioEnded?.();
870
- };
871
- }
872
- setLoadingProgress(100);
873
- setLoading(false);
874
- vmdDataRef.current = { mesh, vmd, cameraVmd };
875
- if (shouldAutoPlayAfterReloadRef.current) {
876
- shouldAutoPlayAfterReloadRef.current = false;
877
- setTimeout(() => play(), 500);
878
- } else if (autoPlay) {
879
- setTimeout(() => play(), 500);
880
- }
881
- onLoad?.();
882
- } catch (err) {
883
- console.error("\u274C MMD\u52A0\u8F7D\u5931\u8D25:", err);
884
- setError(err.message || "\u52A0\u8F7D\u5931\u8D25");
885
- setLoading(false);
886
- isLoadedRef.current = false;
887
- onError?.(err);
888
- }
889
- };
890
- loadMMD();
891
- }, [currentResources, stage?.enablePhysics, autoPlay, loop, onLoad, onError, reloadTrigger]);
892
- const play = () => {
893
- if (!helperRef.current && !needReset) return;
894
- if (needReset && vmdDataRef.current && sceneRef.current && cameraRef.current) {
895
- const { mesh, vmd, cameraVmd } = vmdDataRef.current;
896
- if (helperRef.current) {
897
- try {
898
- const helperObjects = helperRef.current.objects;
899
- if (helperObjects && Array.isArray(helperObjects)) {
900
- helperObjects.length = 0;
901
791
  }
902
- } catch (error2) {
903
- }
792
+ if (object instanceof THREE__namespace.Light) {
793
+ if (object.shadow && object.shadow.map) {
794
+ object.shadow.map.dispose();
795
+ object.shadow.map = null;
796
+ }
797
+ }
798
+ });
799
+ sceneRef.current.clear();
800
+ sceneRef.current = null;
801
+ }
802
+ if (controlsRef.current) {
803
+ controlsRef.current.dispose();
804
+ controlsRef.current = null;
904
805
  }
905
- const newHelper = new threeStdlib.MMDAnimationHelper();
906
- helperRef.current = newHelper;
907
- clockRef.current = new THREE2__namespace.Clock();
908
- if (vmd && typeof vmd === "object") {
806
+ if (rendererRef.current) {
909
807
  try {
910
- newHelper.add(mesh, {
911
- animation: vmd,
912
- physics: stage?.enablePhysics !== false
913
- });
914
- } catch (error2) {
915
- try {
916
- newHelper.add(mesh, { physics: stage?.enablePhysics !== false });
917
- } catch (innerError) {
808
+ const renderer = rendererRef.current;
809
+ if (renderer.renderLists) {
810
+ renderer.renderLists.dispose();
918
811
  }
812
+ if (renderer.info && renderer.info.programs) {
813
+ renderer.info.programs.forEach((program) => {
814
+ if (program && program.destroy) {
815
+ program.destroy();
816
+ }
817
+ });
818
+ }
819
+ if (renderer.getContext) {
820
+ const gl = renderer.getContext();
821
+ const numTextureUnits = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS);
822
+ for (let unit = 0; unit < numTextureUnits; ++unit) {
823
+ gl.activeTexture(gl.TEXTURE0 + unit);
824
+ gl.bindTexture(gl.TEXTURE_2D, null);
825
+ gl.bindTexture(gl.TEXTURE_CUBE_MAP, null);
826
+ }
827
+ }
828
+ renderer.dispose();
829
+ renderer.forceContextLoss();
830
+ if (containerRef.current && renderer.domElement) {
831
+ if (containerRef.current.contains(renderer.domElement)) {
832
+ containerRef.current.removeChild(renderer.domElement);
833
+ }
834
+ }
835
+ if (renderer.domElement) {
836
+ renderer.domElement.width = 1;
837
+ renderer.domElement.height = 1;
838
+ }
839
+ } catch (e) {
840
+ console.warn("[MMDPlayerBase] Error cleaning up renderer:", e);
919
841
  }
920
- } else {
921
- try {
922
- newHelper.add(mesh, { physics: stage?.enablePhysics !== false });
923
- } catch (error2) {
924
- }
842
+ rendererRef.current = null;
925
843
  }
926
- if (cameraVmd && typeof cameraVmd === "object") {
844
+ cameraRef.current = null;
845
+ clockRef.current = new THREE__namespace.Clock();
846
+ durationRef.current = 0;
847
+ console.log("[MMDPlayerBase] Cleanup completed");
848
+ if (typeof window !== "undefined" && "gc" in window) {
927
849
  try {
928
- newHelper.add(cameraRef.current, { animation: cameraVmd });
929
- } catch (error2) {
850
+ window.gc();
851
+ } catch (e) {
930
852
  }
931
- }
932
- if (audioRef.current) {
933
- audioRef.current.currentTime = 0;
934
- }
935
- setNeedReset(false);
936
- }
937
- if (!helperRef.current) {
938
- console.error("\u274C [play] helper \u4E0D\u5B58\u5728\uFF0C\u65E0\u6CD5\u64AD\u653E");
939
- return;
940
- }
941
- if (audioRef.current) {
942
- audioRef.current.play();
943
- }
944
- helperRef.current.enable("animation", true);
945
- helperRef.current.enable("ik", true);
946
- helperRef.current.enable("grant", true);
947
- helperRef.current.enable("physics", true);
948
- if (!isPlaying) {
949
- clockRef.current.start();
950
- }
951
- animationEndedFiredRef.current = false;
952
- lastAnimationTimeRef.current = 0;
953
- animationStoppedCountRef.current = 0;
954
- isPlayingRef.current = true;
955
- setIsPlaying(true);
956
- console.log("\u25B6\uFE0F \u5F00\u59CB\u64AD\u653E\uFF08\u5305\u62EC\u76F8\u673A\u52A8\u753B\uFF09");
957
- };
958
- const pause = () => {
959
- if (!helperRef.current) return;
960
- if (audioRef.current) {
961
- audioRef.current.pause();
962
- }
963
- clockRef.current.stop();
964
- isPlayingRef.current = false;
965
- setIsPlaying(false);
966
- console.log("\u23F8\uFE0F \u6682\u505C\u64AD\u653E\uFF08\u5305\u62EC\u76F8\u673A\u52A8\u753B\uFF09");
967
- };
968
- const stop = () => {
969
- if (!helperRef.current || !sceneRef.current) return;
970
- isPlayingRef.current = false;
971
- setIsPlaying(false);
972
- if (audioRef.current) {
973
- audioRef.current.pause();
974
- audioRef.current.currentTime = 0;
975
- }
976
- clockRef.current.stop();
977
- clockRef.current = new THREE2__namespace.Clock();
978
- const mesh = sceneRef.current.children.find(
979
- (child) => child.type === "SkinnedMesh" || child.isSkinnedMesh
980
- );
981
- if (mesh && mesh.skeleton) {
982
- mesh.skeleton.pose();
983
- }
984
- if (cameraRef.current) {
985
- const camPos = stage?.cameraPosition || { x: 0, y: 10, z: 30 };
986
- const camTarget = stage?.cameraTarget || { x: 0, y: 10, z: 0 };
987
- cameraRef.current.position.set(camPos.x, camPos.y, camPos.z);
988
- if (controlsRef.current) {
989
- controlsRef.current.target.set(camTarget.x, camTarget.y, camTarget.z);
990
- controlsRef.current.update();
991
- } else {
992
- cameraRef.current.lookAt(camTarget.x, camTarget.y, camTarget.z);
993
- }
994
- }
995
- setNeedReset(true);
996
- console.log("\u23F9\uFE0F \u505C\u6B62\u64AD\u653E\u5E76\u91CD\u7F6E\u5230\u521D\u59CB\u72B6\u6001\uFF0CneedReset = true");
997
- };
998
- const handleResourceChange = (resourceId) => {
999
- console.log("\u{1F504} [MMDPlayerEnhanced] \u5207\u6362\u8D44\u6E90:", resourceId);
1000
- if (isPlayingRef.current) {
1001
- isPlayingRef.current = false;
1002
- setIsPlaying(false);
1003
- }
1004
- if (audioRef.current) {
1005
- audioRef.current.pause();
1006
- audioRef.current.currentTime = 0;
1007
- }
1008
- setSelectedResourceId(resourceId);
1009
- isLoadedRef.current = false;
1010
- setNeedReset(false);
1011
- setReloadTrigger((prev) => prev + 1);
1012
- if (onResourceChange) {
1013
- onResourceChange(resourceId);
1014
- }
1015
- setShowSettings(false);
1016
- };
1017
- const handleSelectionChange = (type, id) => {
1018
- console.log(`\u{1F504} [MMDPlayerEnhanced] \u9009\u62E9${type}:`, id);
1019
- const wasPlaying = isPlayingRef.current;
1020
- if (isPlayingRef.current) {
1021
- isPlayingRef.current = false;
1022
- setIsPlaying(false);
1023
- }
1024
- if (audioRef.current) {
1025
- audioRef.current.pause();
1026
- audioRef.current.currentTime = 0;
853
+ }
854
+ };
855
+ }, [resources]);
856
+ React6.useEffect(() => {
857
+ if (!sceneRef.current) return;
858
+ if (showAxes && !axesHelperRef.current) {
859
+ const axesHelper = new THREE__namespace.AxesHelper(20);
860
+ sceneRef.current.add(axesHelper);
861
+ axesHelperRef.current = axesHelper;
862
+ } else if (!showAxes && axesHelperRef.current) {
863
+ sceneRef.current.remove(axesHelperRef.current);
864
+ axesHelperRef.current.dispose();
865
+ axesHelperRef.current = null;
1027
866
  }
1028
- if (type === "model") setSelectedModelId(id);
1029
- if (type === "motion") setSelectedMotionId(id);
1030
- if (type === "audio") setSelectedAudioId(id);
1031
- if (type === "camera") setSelectedCameraId(id);
1032
- if (type === "stageModel") setSelectedStageModelId(id);
1033
- if (type === "background") setSelectedBackgroundId(id);
1034
- isLoadedRef.current = false;
1035
- setNeedReset(false);
1036
- if (wasPlaying || autoPlay) {
1037
- shouldAutoPlayAfterReloadRef.current = true;
867
+ }, [showAxes]);
868
+ React6.useEffect(() => {
869
+ loopRef.current = loop;
870
+ if (audioRef.current && audioRef.current.buffer) {
871
+ audioRef.current.setLoop(loop);
1038
872
  }
1039
- setReloadTrigger((prev) => prev + 1);
1040
- if (onSelectionChange) {
1041
- const newSelection = {
1042
- modelId: type === "model" ? id : selectedModelId,
1043
- motionId: type === "motion" ? id : selectedMotionId,
1044
- audioId: type === "audio" ? id : selectedAudioId,
1045
- cameraId: type === "camera" ? id : selectedCameraId,
1046
- stageModelId: type === "stageModel" ? id : selectedStageModelId,
1047
- backgroundId: type === "background" ? id : selectedBackgroundId
1048
- };
1049
- onSelectionChange(newSelection);
873
+ }, [loop]);
874
+ const animate = () => {
875
+ animationIdRef.current = requestAnimationFrame(animate);
876
+ if (rendererRef.current && sceneRef.current && cameraRef.current) {
877
+ if (isReadyRef.current && isPlayingRef.current && helperRef.current) {
878
+ const delta = clockRef.current.getDelta();
879
+ helperRef.current.update(delta);
880
+ const elapsed = clockRef.current.elapsedTime;
881
+ const duration = durationRef.current;
882
+ const currentTime = duration > 0 && loopRef.current ? elapsed % duration : elapsed;
883
+ onTimeUpdate?.(currentTime);
884
+ if (!loopRef.current && duration > 0 && elapsed >= duration) {
885
+ isPlayingRef.current = false;
886
+ clockRef.current.stop();
887
+ onEnded?.();
888
+ }
889
+ }
890
+ rendererRef.current.render(sceneRef.current, cameraRef.current);
1050
891
  }
1051
892
  };
1052
- return /* @__PURE__ */ React2__default.default.createElement("div", { className: `relative h-full w-full ${className}`, style }, /* @__PURE__ */ React2__default.default.createElement("div", { ref: containerRef, className: "h-full w-full" }), loading && /* @__PURE__ */ React2__default.default.createElement("div", { className: "absolute inset-0 flex flex-col items-center justify-center bg-black text-white" }, /* @__PURE__ */ React2__default.default.createElement("div", { className: "mb-4 text-2xl" }, "\u{1F3AD} \u52A0\u8F7DMMD\u8D44\u6E90\u4E2D..."), /* @__PURE__ */ React2__default.default.createElement("div", { className: "h-4 w-3/4 max-w-md overflow-hidden rounded-full bg-gray-700" }, /* @__PURE__ */ React2__default.default.createElement(
893
+ return /* @__PURE__ */ React6__default.default.createElement(
1053
894
  "div",
1054
895
  {
1055
- className: "h-full bg-gradient-to-r from-blue-500 to-purple-500 transition-all duration-300",
1056
- style: { width: `${loadingProgress}%` }
896
+ ref: containerRef,
897
+ className,
898
+ style: {
899
+ width: "100%",
900
+ height: "100%",
901
+ overflow: "hidden",
902
+ position: "relative",
903
+ backgroundColor: stage.backgroundColor || "#000",
904
+ ...style
905
+ }
1057
906
  }
1058
- )), /* @__PURE__ */ React2__default.default.createElement("div", { className: "mt-2 text-sm text-gray-400" }, Math.round(loadingProgress), "%")), isInitialized && !loading && !error && /* @__PURE__ */ React2__default.default.createElement("div", { className: "absolute bottom-4 left-1/2 flex -translate-x-1/2 gap-2 rounded-full bg-black/50 px-4 py-2 backdrop-blur-md" }, !isPlaying ? /* @__PURE__ */ React2__default.default.createElement(
1059
- "button",
1060
- {
1061
- onClick: play,
1062
- className: "flex h-12 w-12 items-center justify-center rounded-full bg-green-500 text-xl text-white transition-colors hover:bg-green-600",
1063
- title: "\u64AD\u653E"
1064
- },
1065
- "\u25B6\uFE0F"
1066
- ) : /* @__PURE__ */ React2__default.default.createElement(
1067
- "button",
1068
- {
1069
- onClick: pause,
1070
- className: "flex h-12 w-12 items-center justify-center rounded-full bg-yellow-500 text-xl text-white transition-colors hover:bg-yellow-600",
1071
- title: "\u6682\u505C"
1072
- },
1073
- "\u23F8\uFE0F"
1074
- ), /* @__PURE__ */ React2__default.default.createElement(
1075
- "button",
1076
- {
1077
- onClick: stop,
1078
- className: "flex h-12 w-12 items-center justify-center rounded-full bg-red-500 text-xl text-white transition-colors hover:bg-red-600",
1079
- title: "\u505C\u6B62"
1080
- },
1081
- "\u23F9\uFE0F"
1082
- ), (resourcesList && resourcesList.length > 1 || resourceOptions) && /* @__PURE__ */ React2__default.default.createElement(
1083
- "button",
1084
- {
1085
- onClick: () => setShowSettings(true),
1086
- className: "flex h-12 w-12 items-center justify-center rounded-full bg-purple-500 text-xl text-white transition-colors hover:bg-purple-600",
1087
- title: "\u8BBE\u7F6E"
1088
- },
1089
- "\u2699\uFE0F"
1090
- )), showSettings && resourcesList && /* @__PURE__ */ React2__default.default.createElement("div", { className: "absolute inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" }, /* @__PURE__ */ React2__default.default.createElement("div", { className: "max-h-[80vh] w-full max-w-md overflow-hidden rounded-2xl bg-gradient-to-br from-gray-900 to-black shadow-2xl" }, /* @__PURE__ */ React2__default.default.createElement("div", { className: "flex items-center justify-between border-b border-white/10 px-6 py-4" }, /* @__PURE__ */ React2__default.default.createElement("h3", { className: "text-xl font-bold text-white" }, "\u9009\u62E9\u8D44\u6E90"), /* @__PURE__ */ React2__default.default.createElement(
1091
- "button",
1092
- {
1093
- onClick: () => setShowSettings(false),
1094
- className: "text-2xl text-white/60 transition-colors hover:text-white"
1095
- },
1096
- "\u2715"
1097
- )), /* @__PURE__ */ React2__default.default.createElement("div", { className: "max-h-[60vh] overflow-y-auto p-4" }, resourcesList.map((item) => /* @__PURE__ */ React2__default.default.createElement(
1098
- "button",
1099
- {
1100
- key: item.id,
1101
- onClick: () => handleResourceChange(item.id),
1102
- className: `mb-3 w-full rounded-xl p-4 text-left transition-all ${selectedResourceId === item.id ? "bg-gradient-to-r from-purple-600 to-blue-600 shadow-lg" : "bg-white/5 hover:bg-white/10"}`
1103
- },
1104
- /* @__PURE__ */ React2__default.default.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ React2__default.default.createElement("div", { className: "flex-1" }, /* @__PURE__ */ React2__default.default.createElement("h4", { className: "font-semibold text-white" }, item.name), /* @__PURE__ */ React2__default.default.createElement("div", { className: "mt-1 flex flex-wrap gap-2 text-xs text-white/60" }, item.resources.modelPath && /* @__PURE__ */ React2__default.default.createElement("span", { className: "rounded bg-white/10 px-2 py-1" }, "\u6A21\u578B"), item.resources.motionPath && /* @__PURE__ */ React2__default.default.createElement("span", { className: "rounded bg-white/10 px-2 py-1" }, "\u52A8\u4F5C"), item.resources.cameraPath && /* @__PURE__ */ React2__default.default.createElement("span", { className: "rounded bg-white/10 px-2 py-1" }, "\u76F8\u673A"), item.resources.audioPath && /* @__PURE__ */ React2__default.default.createElement("span", { className: "rounded bg-white/10 px-2 py-1" }, "\u97F3\u9891"))), selectedResourceId === item.id && /* @__PURE__ */ React2__default.default.createElement("div", { className: "ml-4 text-2xl" }, "\u2713"))
1105
- ))))), showSettings && resourceOptions && /* @__PURE__ */ React2__default.default.createElement("div", { className: "absolute top-4 right-4 z-50 w-80 rounded-xl bg-black/90 backdrop-blur-md shadow-2xl border border-white/10" }, /* @__PURE__ */ React2__default.default.createElement("div", { className: "flex items-center justify-between border-b border-white/10 px-4 py-3" }, /* @__PURE__ */ React2__default.default.createElement("h3", { className: "text-sm font-bold text-white" }, "\u8D44\u6E90\u8BBE\u7F6E"), /* @__PURE__ */ React2__default.default.createElement(
1106
- "button",
1107
- {
1108
- onClick: () => setShowSettings(false),
1109
- className: "text-lg text-white/60 transition-colors hover:text-white"
1110
- },
1111
- "\u2715"
1112
- )), /* @__PURE__ */ React2__default.default.createElement("div", { className: "max-h-[70vh] overflow-y-auto p-4 space-y-2" }, resourceOptions.models && resourceOptions.models.length > 0 && /* @__PURE__ */ React2__default.default.createElement("div", { className: "rounded-lg bg-white/5 overflow-hidden" }, /* @__PURE__ */ React2__default.default.createElement(
1113
- "button",
1114
- {
1115
- onClick: () => setExpandedSection(expandedSection === "model" ? null : "model"),
1116
- className: "w-full flex items-center justify-between px-3 py-2.5 text-left hover:bg-white/10 transition-colors"
1117
- },
1118
- /* @__PURE__ */ React2__default.default.createElement("div", { className: "flex items-center gap-2" }, /* @__PURE__ */ React2__default.default.createElement("span", { className: "text-xs font-medium text-white/70" }, "\u6A21\u578B"), /* @__PURE__ */ React2__default.default.createElement("span", { className: "text-sm text-white font-medium" }, resourceOptions.models.find((m) => m.id === selectedModelId)?.name || "\u672A\u9009\u62E9")),
1119
- /* @__PURE__ */ React2__default.default.createElement("span", { className: `text-white/60 transition-transform ${expandedSection === "model" ? "rotate-180" : ""}` }, "\u25BC")
1120
- ), expandedSection === "model" && /* @__PURE__ */ React2__default.default.createElement("div", { className: "border-t border-white/10 p-2 space-y-1 max-h-60 overflow-y-auto" }, resourceOptions.models.map((model) => /* @__PURE__ */ React2__default.default.createElement(
1121
- "button",
1122
- {
1123
- key: model.id,
1124
- onClick: () => {
1125
- handleSelectionChange("model", model.id);
1126
- setExpandedSection(null);
1127
- },
1128
- className: `w-full rounded px-3 py-2 text-left text-sm transition-all ${selectedModelId === model.id ? "bg-purple-600 text-white font-medium" : "text-white/80 hover:bg-white/10"}`
1129
- },
1130
- model.name
1131
- )))), resourceOptions.motions && resourceOptions.motions.length > 0 && /* @__PURE__ */ React2__default.default.createElement("div", { className: "rounded-lg bg-white/5 overflow-hidden" }, /* @__PURE__ */ React2__default.default.createElement(
1132
- "button",
1133
- {
1134
- onClick: () => setExpandedSection(expandedSection === "motion" ? null : "motion"),
1135
- className: "w-full flex items-center justify-between px-3 py-2.5 text-left hover:bg-white/10 transition-colors"
1136
- },
1137
- /* @__PURE__ */ React2__default.default.createElement("div", { className: "flex items-center gap-2" }, /* @__PURE__ */ React2__default.default.createElement("span", { className: "text-xs font-medium text-white/70" }, "\u52A8\u4F5C"), /* @__PURE__ */ React2__default.default.createElement("span", { className: "text-sm text-white font-medium" }, selectedMotionId ? resourceOptions.motions.find((m) => m.id === selectedMotionId)?.name : "\u65E0")),
1138
- /* @__PURE__ */ React2__default.default.createElement("span", { className: `text-white/60 transition-transform ${expandedSection === "motion" ? "rotate-180" : ""}` }, "\u25BC")
1139
- ), expandedSection === "motion" && /* @__PURE__ */ React2__default.default.createElement("div", { className: "border-t border-white/10 p-2 space-y-1 max-h-60 overflow-y-auto" }, /* @__PURE__ */ React2__default.default.createElement(
1140
- "button",
1141
- {
1142
- onClick: () => {
1143
- handleSelectionChange("motion", "");
1144
- setExpandedSection(null);
1145
- },
1146
- className: `w-full rounded px-3 py-2 text-left text-sm transition-all ${selectedMotionId === "" ? "bg-purple-600 text-white font-medium" : "text-white/80 hover:bg-white/10"}`
1147
- },
1148
- "\u65E0"
1149
- ), resourceOptions.motions.map((motion) => /* @__PURE__ */ React2__default.default.createElement(
907
+ );
908
+ });
909
+ MMDPlayerBase.displayName = "MMDPlayerBase";
910
+ var ControlPanel = ({
911
+ isPlaying,
912
+ isFullscreen,
913
+ isLooping,
914
+ isListLooping,
915
+ showSettings,
916
+ showAxes = false,
917
+ showPrevNext = false,
918
+ title,
919
+ subtitle,
920
+ onPlayPause,
921
+ onToggleFullscreen,
922
+ onToggleLoop,
923
+ onToggleListLoop,
924
+ onToggleAxes,
925
+ onOpenSettings,
926
+ onPrevious,
927
+ onNext
928
+ }) => {
929
+ return /* @__PURE__ */ React6__default.default.createElement("div", { className: "absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 to-transparent p-4 transition-opacity duration-300 hover:opacity-100" }, /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex items-center justify-between text-white" }, /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex items-center gap-2" }, showPrevNext && onPrevious && /* @__PURE__ */ React6__default.default.createElement(
1150
930
  "button",
1151
931
  {
1152
- key: motion.id,
1153
- onClick: () => {
1154
- handleSelectionChange("motion", motion.id);
1155
- setExpandedSection(null);
1156
- },
1157
- className: `w-full rounded px-3 py-2 text-left text-sm transition-all ${selectedMotionId === motion.id ? "bg-purple-600 text-white font-medium" : "text-white/80 hover:bg-white/10"}`
932
+ onClick: onPrevious,
933
+ className: "rounded-full p-2 hover:bg-white/20 transition-colors",
934
+ title: "\u4E0A\u4E00\u4E2A"
1158
935
  },
1159
- motion.name
1160
- )))), resourceOptions.audios && resourceOptions.audios.length > 0 && /* @__PURE__ */ React2__default.default.createElement("div", { className: "rounded-lg bg-white/5 overflow-hidden" }, /* @__PURE__ */ React2__default.default.createElement(
936
+ /* @__PURE__ */ React6__default.default.createElement(lucideReact.SkipBack, { size: 20 })
937
+ ), /* @__PURE__ */ React6__default.default.createElement(
1161
938
  "button",
1162
939
  {
1163
- onClick: () => setExpandedSection(expandedSection === "audio" ? null : "audio"),
1164
- className: "w-full flex items-center justify-between px-3 py-2.5 text-left hover:bg-white/10 transition-colors"
940
+ onClick: onPlayPause,
941
+ className: "rounded-full p-2 hover:bg-white/20 transition-colors",
942
+ title: isPlaying ? "\u6682\u505C" : "\u64AD\u653E"
1165
943
  },
1166
- /* @__PURE__ */ React2__default.default.createElement("div", { className: "flex items-center gap-2" }, /* @__PURE__ */ React2__default.default.createElement("span", { className: "text-xs font-medium text-white/70" }, "\u97F3\u4E50"), /* @__PURE__ */ React2__default.default.createElement("span", { className: "text-sm text-white font-medium" }, selectedAudioId ? resourceOptions.audios.find((a) => a.id === selectedAudioId)?.name : "\u65E0")),
1167
- /* @__PURE__ */ React2__default.default.createElement("span", { className: `text-white/60 transition-transform ${expandedSection === "audio" ? "rotate-180" : ""}` }, "\u25BC")
1168
- ), expandedSection === "audio" && /* @__PURE__ */ React2__default.default.createElement("div", { className: "border-t border-white/10 p-2 space-y-1 max-h-60 overflow-y-auto" }, /* @__PURE__ */ React2__default.default.createElement(
944
+ isPlaying ? /* @__PURE__ */ React6__default.default.createElement(lucideReact.Pause, { size: 24 }) : /* @__PURE__ */ React6__default.default.createElement(lucideReact.Play, { size: 24 })
945
+ ), showPrevNext && onNext && /* @__PURE__ */ React6__default.default.createElement(
1169
946
  "button",
1170
947
  {
1171
- onClick: () => {
1172
- handleSelectionChange("audio", "");
1173
- setExpandedSection(null);
1174
- },
1175
- className: `w-full rounded px-3 py-2 text-left text-sm transition-all ${selectedAudioId === "" ? "bg-purple-600 text-white font-medium" : "text-white/80 hover:bg-white/10"}`
948
+ onClick: onNext,
949
+ className: "rounded-full p-2 hover:bg-white/20 transition-colors",
950
+ title: "\u4E0B\u4E00\u4E2A"
1176
951
  },
1177
- "\u65E0"
1178
- ), resourceOptions.audios.map((audio) => /* @__PURE__ */ React2__default.default.createElement(
952
+ /* @__PURE__ */ React6__default.default.createElement(lucideReact.SkipForward, { size: 20 })
953
+ )), /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex items-center gap-4" }, (title || subtitle) && /* @__PURE__ */ React6__default.default.createElement("div", { className: "hidden text-sm font-medium opacity-80 md:block" }, title, subtitle && /* @__PURE__ */ React6__default.default.createElement("span", { className: "ml-2 text-xs opacity-60" }, subtitle)), onToggleListLoop && /* @__PURE__ */ React6__default.default.createElement(
1179
954
  "button",
1180
955
  {
1181
- key: audio.id,
1182
- onClick: () => {
1183
- handleSelectionChange("audio", audio.id);
1184
- setExpandedSection(null);
1185
- },
1186
- className: `w-full rounded px-3 py-2 text-left text-sm transition-all ${selectedAudioId === audio.id ? "bg-purple-600 text-white font-medium" : "text-white/80 hover:bg-white/10"}`
956
+ onClick: onToggleListLoop,
957
+ className: `rounded-full p-2 transition-colors ${isListLooping ? "bg-green-500/30 hover:bg-green-500/50" : "hover:bg-white/20"}`,
958
+ title: isListLooping ? "\u5217\u8868\u5FAA\u73AF\uFF1A\u5F00\u542F" : "\u5217\u8868\u5FAA\u73AF\uFF1A\u5173\u95ED"
1187
959
  },
1188
- audio.name
1189
- )))), resourceOptions.cameras && resourceOptions.cameras.length > 0 && /* @__PURE__ */ React2__default.default.createElement("div", { className: "rounded-lg bg-white/5 overflow-hidden" }, /* @__PURE__ */ React2__default.default.createElement(
960
+ /* @__PURE__ */ React6__default.default.createElement(lucideReact.Repeat, { size: 20 })
961
+ ), /* @__PURE__ */ React6__default.default.createElement(
1190
962
  "button",
1191
963
  {
1192
- onClick: () => setExpandedSection(expandedSection === "camera" ? null : "camera"),
1193
- className: "w-full flex items-center justify-between px-3 py-2.5 text-left hover:bg-white/10 transition-colors"
964
+ onClick: onToggleLoop,
965
+ className: `rounded-full p-2 transition-colors ${isLooping ? "bg-blue-500/30 hover:bg-blue-500/50" : "hover:bg-white/20"}`,
966
+ title: isLooping ? "\u5355\u66F2\u5FAA\u73AF\uFF1A\u5F00\u542F" : "\u5355\u66F2\u5FAA\u73AF\uFF1A\u5173\u95ED"
1194
967
  },
1195
- /* @__PURE__ */ React2__default.default.createElement("div", { className: "flex items-center gap-2" }, /* @__PURE__ */ React2__default.default.createElement("span", { className: "text-xs font-medium text-white/70" }, "\u76F8\u673A"), /* @__PURE__ */ React2__default.default.createElement("span", { className: "text-sm text-white font-medium" }, selectedCameraId ? resourceOptions.cameras.find((c) => c.id === selectedCameraId)?.name : "\u65E0")),
1196
- /* @__PURE__ */ React2__default.default.createElement("span", { className: `text-white/60 transition-transform ${expandedSection === "camera" ? "rotate-180" : ""}` }, "\u25BC")
1197
- ), expandedSection === "camera" && /* @__PURE__ */ React2__default.default.createElement("div", { className: "border-t border-white/10 p-2 space-y-1 max-h-60 overflow-y-auto" }, /* @__PURE__ */ React2__default.default.createElement(
968
+ /* @__PURE__ */ React6__default.default.createElement(lucideReact.Repeat1, { size: 20 })
969
+ ), onToggleAxes && /* @__PURE__ */ React6__default.default.createElement(
1198
970
  "button",
1199
971
  {
1200
- onClick: () => {
1201
- handleSelectionChange("camera", "");
1202
- setExpandedSection(null);
1203
- },
1204
- className: `w-full rounded px-3 py-2 text-left text-sm transition-all ${selectedCameraId === "" ? "bg-purple-600 text-white font-medium" : "text-white/80 hover:bg-white/10"}`
972
+ onClick: onToggleAxes,
973
+ className: `rounded-full p-2 transition-colors ${showAxes ? "bg-blue-500/30 hover:bg-blue-500/50" : "hover:bg-white/20"}`,
974
+ title: "\u663E\u793A/\u9690\u85CF\u5750\u6807\u8F74"
1205
975
  },
1206
- "\u65E0"
1207
- ), resourceOptions.cameras.map((camera) => /* @__PURE__ */ React2__default.default.createElement(
976
+ /* @__PURE__ */ React6__default.default.createElement(lucideReact.Grid3x3, { size: 20 })
977
+ ), showSettings && /* @__PURE__ */ React6__default.default.createElement(
1208
978
  "button",
1209
979
  {
1210
- key: camera.id,
1211
- onClick: () => {
1212
- handleSelectionChange("camera", camera.id);
1213
- setExpandedSection(null);
1214
- },
1215
- className: `w-full rounded px-3 py-2 text-left text-sm transition-all ${selectedCameraId === camera.id ? "bg-purple-600 text-white font-medium" : "text-white/80 hover:bg-white/10"}`
980
+ onClick: onOpenSettings,
981
+ className: "rounded-full p-2 hover:bg-white/20 transition-colors",
982
+ title: "\u8D44\u6E90\u8BBE\u7F6E"
1216
983
  },
1217
- camera.name
1218
- )))), resourceOptions.stageModels && resourceOptions.stageModels.length > 0 && /* @__PURE__ */ React2__default.default.createElement("div", { className: "rounded-lg bg-white/5 overflow-hidden" }, /* @__PURE__ */ React2__default.default.createElement(
984
+ /* @__PURE__ */ React6__default.default.createElement(lucideReact.Settings, { size: 20 })
985
+ ), /* @__PURE__ */ React6__default.default.createElement(
1219
986
  "button",
1220
987
  {
1221
- onClick: () => setExpandedSection(expandedSection === "stageModel" ? null : "stageModel"),
1222
- className: "w-full flex items-center justify-between px-3 py-2.5 text-left hover:bg-white/10 transition-colors"
988
+ onClick: onToggleFullscreen,
989
+ className: "rounded-full p-2 hover:bg-white/20 transition-colors",
990
+ title: isFullscreen ? "\u9000\u51FA\u5168\u5C4F" : "\u5168\u5C4F"
1223
991
  },
1224
- /* @__PURE__ */ React2__default.default.createElement("div", { className: "flex items-center gap-2" }, /* @__PURE__ */ React2__default.default.createElement("span", { className: "text-xs font-medium text-white/70" }, "\u573A\u666F"), /* @__PURE__ */ React2__default.default.createElement("span", { className: "text-sm text-white font-medium" }, selectedStageModelId ? resourceOptions.stageModels.find((s) => s.id === selectedStageModelId)?.name : "\u65E0")),
1225
- /* @__PURE__ */ React2__default.default.createElement("span", { className: `text-white/60 transition-transform ${expandedSection === "stageModel" ? "rotate-180" : ""}` }, "\u25BC")
1226
- ), expandedSection === "stageModel" && /* @__PURE__ */ React2__default.default.createElement("div", { className: "border-t border-white/10 p-2 space-y-1 max-h-60 overflow-y-auto" }, /* @__PURE__ */ React2__default.default.createElement(
1227
- "button",
1228
- {
1229
- onClick: () => {
1230
- handleSelectionChange("stageModel", "");
1231
- setExpandedSection(null);
992
+ isFullscreen ? /* @__PURE__ */ React6__default.default.createElement(lucideReact.Minimize, { size: 20 }) : /* @__PURE__ */ React6__default.default.createElement(lucideReact.Maximize, { size: 20 })
993
+ ))));
994
+ };
995
+ var SettingsPanel = ({
996
+ mode,
997
+ items,
998
+ currentId,
999
+ onSelectId,
1000
+ options,
1001
+ currentSelection,
1002
+ onSelectOption,
1003
+ onClose
1004
+ }) => {
1005
+ const renderListMode = () => {
1006
+ if (!items) return null;
1007
+ return /* @__PURE__ */ React6__default.default.createElement("div", { className: "grid grid-cols-1 gap-2 p-4 sm:grid-cols-2" }, items.map((item) => /* @__PURE__ */ React6__default.default.createElement(
1008
+ "button",
1009
+ {
1010
+ key: item.id,
1011
+ onClick: () => onSelectId?.(item.id),
1012
+ className: `group flex items-center gap-3 rounded-lg p-3 transition-all ${currentId === item.id ? "bg-blue-500/20 ring-1 ring-blue-500" : "bg-white/5 hover:bg-white/10"}`
1232
1013
  },
1233
- className: `w-full rounded px-3 py-2 text-left text-sm transition-all ${selectedStageModelId === "" ? "bg-purple-600 text-white font-medium" : "text-white/80 hover:bg-white/10"}`
1234
- },
1235
- "\u65E0"
1236
- ), resourceOptions.stageModels.map((stageModel) => /* @__PURE__ */ React2__default.default.createElement(
1237
- "button",
1238
- {
1239
- key: stageModel.id,
1240
- onClick: () => {
1241
- handleSelectionChange("stageModel", stageModel.id);
1242
- setExpandedSection(null);
1014
+ /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex h-12 w-12 flex-shrink-0 items-center justify-center rounded bg-black/20 overflow-hidden" }, item.thumbnail ? /* @__PURE__ */ React6__default.default.createElement("img", { src: item.thumbnail, alt: item.name, className: "h-full w-full object-cover" }) : /* @__PURE__ */ React6__default.default.createElement(lucideReact.Video, { size: 20, className: "opacity-50" })),
1015
+ /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex-1 text-left" }, /* @__PURE__ */ React6__default.default.createElement("div", { className: `font-medium ${currentId === item.id ? "text-blue-400" : "text-white"}` }, item.name), item.description && /* @__PURE__ */ React6__default.default.createElement("div", { className: "text-xs text-white/50 truncate" }, item.description)),
1016
+ currentId === item.id && /* @__PURE__ */ React6__default.default.createElement(lucideReact.Check, { size: 16, className: "text-blue-400" })
1017
+ )));
1018
+ };
1019
+ const renderOptionGroup = (title, icon, type, list = [], currentVal) => {
1020
+ if (!list || list.length === 0) return null;
1021
+ return /* @__PURE__ */ React6__default.default.createElement("div", { className: "mb-6" }, /* @__PURE__ */ React6__default.default.createElement("div", { className: "mb-3 flex items-center gap-2 text-sm font-medium text-white/70" }, icon, /* @__PURE__ */ React6__default.default.createElement("span", null, title)), /* @__PURE__ */ React6__default.default.createElement("div", { className: "grid grid-cols-2 gap-2 sm:grid-cols-3" }, list.map((opt) => /* @__PURE__ */ React6__default.default.createElement(
1022
+ "button",
1023
+ {
1024
+ key: opt.id,
1025
+ onClick: () => onSelectOption?.(type, opt.id),
1026
+ className: `relative flex flex-col items-center gap-2 rounded-lg p-2 text-center transition-all ${currentVal === opt.id ? "bg-blue-500/20 ring-1 ring-blue-500" : "bg-white/5 hover:bg-white/10"}`
1243
1027
  },
1244
- className: `w-full rounded px-3 py-2 text-left text-sm transition-all ${selectedStageModelId === stageModel.id ? "bg-purple-600 text-white font-medium" : "text-white/80 hover:bg-white/10"}`
1245
- },
1246
- stageModel.name
1247
- )))), resourceOptions.backgrounds && resourceOptions.backgrounds.length > 0 && /* @__PURE__ */ React2__default.default.createElement("div", { className: "rounded-lg bg-white/5 overflow-hidden" }, /* @__PURE__ */ React2__default.default.createElement(
1248
- "button",
1249
- {
1250
- onClick: () => setExpandedSection(expandedSection === "background" ? null : "background"),
1251
- className: "w-full flex items-center justify-between px-3 py-2.5 text-left hover:bg-white/10 transition-colors"
1252
- },
1253
- /* @__PURE__ */ React2__default.default.createElement("div", { className: "flex items-center gap-2" }, /* @__PURE__ */ React2__default.default.createElement("span", { className: "text-xs font-medium text-white/70" }, "\u80CC\u666F"), /* @__PURE__ */ React2__default.default.createElement("span", { className: "text-sm text-white font-medium" }, selectedBackgroundId ? resourceOptions.backgrounds.find((b) => b.id === selectedBackgroundId)?.name : "\u65E0")),
1254
- /* @__PURE__ */ React2__default.default.createElement("span", { className: `text-white/60 transition-transform ${expandedSection === "background" ? "rotate-180" : ""}` }, "\u25BC")
1255
- ), expandedSection === "background" && /* @__PURE__ */ React2__default.default.createElement("div", { className: "border-t border-white/10 p-2 space-y-1 max-h-60 overflow-y-auto" }, /* @__PURE__ */ React2__default.default.createElement(
1028
+ opt.thumbnail ? /* @__PURE__ */ React6__default.default.createElement("img", { src: opt.thumbnail, alt: opt.name, className: "h-16 w-full rounded object-cover bg-black/20" }) : /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex h-16 w-full items-center justify-center rounded bg-black/20" }, /* @__PURE__ */ React6__default.default.createElement("div", { className: "text-xs opacity-30" }, opt.name.slice(0, 2))),
1029
+ /* @__PURE__ */ React6__default.default.createElement("div", { className: `w-full truncate text-xs ${currentVal === opt.id ? "text-blue-400" : "text-white/80"}` }, opt.name),
1030
+ currentVal === opt.id && /* @__PURE__ */ React6__default.default.createElement("div", { className: "absolute top-1 right-1 rounded-full bg-blue-500 p-0.5" }, /* @__PURE__ */ React6__default.default.createElement(lucideReact.Check, { size: 10, className: "text-white" }))
1031
+ ))));
1032
+ };
1033
+ const renderOptionsMode = () => {
1034
+ if (!options) return null;
1035
+ return /* @__PURE__ */ React6__default.default.createElement("div", { className: "p-4" }, renderOptionGroup("\u6A21\u578B", /* @__PURE__ */ React6__default.default.createElement(lucideReact.User, { size: 16 }), "models", options.models, currentSelection?.modelId), renderOptionGroup("\u52A8\u4F5C", /* @__PURE__ */ React6__default.default.createElement(lucideReact.Video, { size: 16 }), "motions", options.motions, currentSelection?.motionId), renderOptionGroup("\u955C\u5934", /* @__PURE__ */ React6__default.default.createElement(lucideReact.Image, { size: 16 }), "cameras", options.cameras, currentSelection?.cameraId), renderOptionGroup("\u97F3\u9891", /* @__PURE__ */ React6__default.default.createElement(lucideReact.Music, { size: 16 }), "audios", options.audios, currentSelection?.audioId), renderOptionGroup("\u821E\u53F0", /* @__PURE__ */ React6__default.default.createElement(lucideReact.Image, { size: 16 }), "stages", options.stages, currentSelection?.stageId));
1036
+ };
1037
+ return /* @__PURE__ */ React6__default.default.createElement("div", { className: "absolute right-0 top-0 h-full w-full max-w-sm transform bg-[#1a1a1e]/95 backdrop-blur-md shadow-2xl transition-transform duration-300 ease-in-out overflow-y-auto z-20 border-l border-white/10" }, /* @__PURE__ */ React6__default.default.createElement("div", { className: "sticky top-0 z-10 flex items-center justify-between border-b border-white/10 bg-[#1a1a1e]/95 p-4 backdrop-blur-md" }, /* @__PURE__ */ React6__default.default.createElement("h2", { className: "text-lg font-semibold text-white" }, mode === "list" ? "\u64AD\u653E\u5217\u8868" : "\u81EA\u5B9A\u4E49\u573A\u666F"), /* @__PURE__ */ React6__default.default.createElement(
1256
1038
  "button",
1257
1039
  {
1258
- onClick: () => {
1259
- handleSelectionChange("background", "");
1260
- setExpandedSection(null);
1261
- },
1262
- className: `w-full rounded px-3 py-2 text-left text-sm transition-all ${selectedBackgroundId === "" ? "bg-purple-600 text-white font-medium" : "text-white/80 hover:bg-white/10"}`
1040
+ onClick: onClose,
1041
+ className: "rounded-full p-2 text-white/50 hover:bg-white/10 hover:text-white"
1263
1042
  },
1264
- "\u65E0"
1265
- ), resourceOptions.backgrounds.map((background) => /* @__PURE__ */ React2__default.default.createElement(
1266
- "button",
1043
+ /* @__PURE__ */ React6__default.default.createElement(lucideReact.X, { size: 20 })
1044
+ )), /* @__PURE__ */ React6__default.default.createElement("div", { className: "pb-20" }, mode === "list" ? renderListMode() : renderOptionsMode()));
1045
+ };
1046
+ var MMDPlayerEnhancedDebugInfo = ({
1047
+ isPlaying,
1048
+ isLooping,
1049
+ isFullscreen,
1050
+ showAxes,
1051
+ isLoading,
1052
+ currentResourceId,
1053
+ currentResourceName,
1054
+ mode,
1055
+ totalResources
1056
+ }) => {
1057
+ const [memoryInfo, setMemoryInfo] = React6.useState(null);
1058
+ React6.useEffect(() => {
1059
+ const timer = setInterval(() => {
1060
+ if (performance.memory) {
1061
+ const used = (performance.memory.usedJSHeapSize / 1048576).toFixed(1);
1062
+ const total = (performance.memory.totalJSHeapSize / 1048576).toFixed(1);
1063
+ const limit = (performance.memory.jsHeapSizeLimit / 1048576).toFixed(1);
1064
+ setMemoryInfo({ used, total, limit });
1065
+ }
1066
+ }, 1e3);
1067
+ return () => clearInterval(timer);
1068
+ }, []);
1069
+ return /* @__PURE__ */ React6__default.default.createElement("div", { className: "text-white text-xs font-mono" }, /* @__PURE__ */ React6__default.default.createElement("h3", { className: "text-sm font-bold mb-3 pb-2 border-b border-gray-700" }, "\u{1F3AE} MMDPlayerEnhanced Debug"), /* @__PURE__ */ React6__default.default.createElement("div", { className: "mb-4" }, /* @__PURE__ */ React6__default.default.createElement("h4", { className: "text-gray-400 mb-2" }, "\u64AD\u653E\u72B6\u6001"), /* @__PURE__ */ React6__default.default.createElement("div", { className: "space-y-1 pl-2" }, /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-gray-400" }, "\u64AD\u653E\u4E2D:"), /* @__PURE__ */ React6__default.default.createElement(StatusBadge, { active: isPlaying, label: isPlaying ? "Playing" : "Paused" })), /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-gray-400" }, "\u5FAA\u73AF:"), /* @__PURE__ */ React6__default.default.createElement(StatusBadge, { active: isLooping, label: isLooping ? "On" : "Off" })), /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-gray-400" }, "\u52A0\u8F7D\u4E2D:"), /* @__PURE__ */ React6__default.default.createElement(StatusBadge, { active: isLoading, label: isLoading ? "Loading" : "Ready" })))), /* @__PURE__ */ React6__default.default.createElement("div", { className: "mb-4" }, /* @__PURE__ */ React6__default.default.createElement("h4", { className: "text-gray-400 mb-2" }, "\u89C6\u56FE\u72B6\u6001"), /* @__PURE__ */ React6__default.default.createElement("div", { className: "space-y-1 pl-2" }, /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-gray-400" }, "\u5168\u5C4F:"), /* @__PURE__ */ React6__default.default.createElement(StatusBadge, { active: isFullscreen, label: isFullscreen ? "Yes" : "No" })), /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-gray-400" }, "\u5750\u6807\u8F74:"), /* @__PURE__ */ React6__default.default.createElement(StatusBadge, { active: showAxes, label: showAxes ? "Show" : "Hide" })))), /* @__PURE__ */ React6__default.default.createElement("div", { className: "mb-4" }, /* @__PURE__ */ React6__default.default.createElement("h4", { className: "text-gray-400 mb-2" }, "\u8D44\u6E90\u4FE1\u606F"), /* @__PURE__ */ React6__default.default.createElement("div", { className: "space-y-1 pl-2" }, /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-gray-400" }, "\u6A21\u5F0F:"), /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-blue-400 uppercase" }, mode)), mode === "list" && /* @__PURE__ */ React6__default.default.createElement(React6__default.default.Fragment, null, /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-gray-400" }, "\u603B\u6570:"), /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-green-400" }, totalResources)), currentResourceId && /* @__PURE__ */ React6__default.default.createElement("div", { className: "mt-2 p-2 bg-gray-800 rounded" }, /* @__PURE__ */ React6__default.default.createElement("div", { className: "text-gray-400 text-[10px]" }, "\u5F53\u524D\u8D44\u6E90"), /* @__PURE__ */ React6__default.default.createElement("div", { className: "text-white truncate" }, currentResourceName || currentResourceId), /* @__PURE__ */ React6__default.default.createElement("div", { className: "text-gray-500 text-[10px] mt-1 truncate" }, "ID: ", currentResourceId))))), memoryInfo && /* @__PURE__ */ React6__default.default.createElement("div", { className: "mb-4" }, /* @__PURE__ */ React6__default.default.createElement("h4", { className: "text-gray-400 mb-2" }, "\u5185\u5B58\u76D1\u63A7 (Chrome only)"), /* @__PURE__ */ React6__default.default.createElement("div", { className: "space-y-2 p-2 bg-gray-800 rounded" }, /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex items-center justify-between text-[10px]" }, /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-gray-400" }, "\u5DF2\u7528:"), /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-yellow-400 font-bold" }, memoryInfo.used, " MB")), /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex items-center justify-between text-[10px]" }, /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-gray-400" }, "\u603B\u8BA1:"), /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-blue-400" }, memoryInfo.total, " MB")), /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex items-center justify-between text-[10px]" }, /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-gray-400" }, "\u9650\u5236:"), /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-gray-400" }, memoryInfo.limit, " MB")), /* @__PURE__ */ React6__default.default.createElement("div", { className: "mt-2" }, /* @__PURE__ */ React6__default.default.createElement("div", { className: "bg-gray-700 rounded-full h-2 overflow-hidden" }, /* @__PURE__ */ React6__default.default.createElement(
1070
+ "div",
1267
1071
  {
1268
- key: background.id,
1269
- onClick: () => {
1270
- handleSelectionChange("background", background.id);
1271
- setExpandedSection(null);
1272
- },
1273
- className: `w-full rounded px-3 py-2 text-left text-sm transition-all ${selectedBackgroundId === background.id ? "bg-purple-600 text-white font-medium" : "text-white/80 hover:bg-white/10"}`
1274
- },
1275
- background.name
1276
- )))))));
1277
- });
1278
- MMDPlayerEnhanced.displayName = "MMDPlayerEnhanced";
1279
- var MMDPlaylist = ({
1280
- playlist,
1072
+ className: `h-full transition-all duration-300 ${parseFloat(memoryInfo.used) / parseFloat(memoryInfo.limit) * 100 > 80 ? "bg-red-500" : parseFloat(memoryInfo.used) / parseFloat(memoryInfo.limit) * 100 > 60 ? "bg-yellow-500" : "bg-green-500"}`,
1073
+ style: {
1074
+ width: `${Math.min(100, parseFloat(memoryInfo.used) / parseFloat(memoryInfo.limit) * 100)}%`
1075
+ }
1076
+ }
1077
+ )), /* @__PURE__ */ React6__default.default.createElement("div", { className: "text-[9px] text-gray-500 mt-1 text-center" }, (parseFloat(memoryInfo.used) / parseFloat(memoryInfo.limit) * 100).toFixed(1), "%")))), /* @__PURE__ */ React6__default.default.createElement("div", { className: "mt-auto pt-4 border-t border-gray-700" }, /* @__PURE__ */ React6__default.default.createElement("div", { className: "text-gray-500 text-[10px]" }, "Last Update: ", (/* @__PURE__ */ new Date()).toLocaleTimeString())));
1078
+ };
1079
+ var StatusBadge = ({ active, label }) => /* @__PURE__ */ React6__default.default.createElement(
1080
+ "span",
1081
+ {
1082
+ className: `px-2 py-0.5 rounded text-[10px] font-bold ${active ? "bg-green-600 text-white" : "bg-gray-700 text-gray-400"}`
1083
+ },
1084
+ label
1085
+ );
1086
+
1087
+ // src/mmd/components/MMDPlayerEnhanced.tsx
1088
+ var MMDPlayerEnhanced = ({
1089
+ resources: propResources,
1090
+ resourcesList,
1091
+ resourceOptions,
1092
+ defaultResourceId,
1093
+ defaultSelection,
1281
1094
  stage,
1282
- defaultNodeIndex = 0,
1095
+ autoPlay = false,
1096
+ loop = true,
1097
+ volume: initialVolume = 1,
1098
+ muted: initialMuted = false,
1099
+ mobileOptimization,
1100
+ showDebugInfo = false,
1283
1101
  className,
1284
1102
  style,
1285
1103
  onLoad,
1286
- onError,
1287
- onNodeChange,
1288
- onPlaylistComplete
1104
+ onPlay,
1105
+ onPause,
1106
+ onEnded,
1107
+ ...rest
1289
1108
  }) => {
1290
- console.log("\u{1F3AC} [MMDPlaylist] \u7EC4\u4EF6\u521D\u59CB\u5316");
1291
- console.log("\u{1F4CB} [MMDPlaylist] \u64AD\u653E\u5217\u8868:", playlist.name, "\u8282\u70B9\u6570:", playlist.nodes.length);
1292
- const [currentNodeIndex, setCurrentNodeIndex] = React2.useState(defaultNodeIndex);
1293
- const [showSettings, setShowSettings] = React2.useState(false);
1294
- const [preloadedNodes, setPreloadedNodes] = React2.useState(/* @__PURE__ */ new Set());
1295
- const [isPreloading, setIsPreloading] = React2.useState(true);
1296
- const [preloadProgress, setPreloadProgress] = React2.useState(0);
1297
- const [editableNodes, setEditableNodes] = React2.useState(playlist.nodes);
1298
- const currentNodeIndexRef = React2.useRef(defaultNodeIndex);
1299
- const isAutoSwitchRef = React2.useRef(false);
1300
- const playerRefsMap = React2.useRef(/* @__PURE__ */ new Map());
1301
- const playerComponentRefs = React2.useRef(/* @__PURE__ */ new Map());
1302
- const [memoryUsage, setMemoryUsage] = React2.useState(0);
1303
- React2.useEffect(() => {
1304
- currentNodeIndexRef.current = currentNodeIndex;
1305
- }, [currentNodeIndex]);
1306
- const currentNode = editableNodes[currentNodeIndex];
1307
- if (!currentNode) {
1308
- console.error("\u274C [MMDPlaylist] \u65E0\u6548\u7684\u8282\u70B9\u7D22\u5F15:", currentNodeIndex);
1309
- return /* @__PURE__ */ React2__default.default.createElement("div", { className: "flex h-full w-full items-center justify-center bg-black text-white" }, /* @__PURE__ */ React2__default.default.createElement("p", null, "\u64AD\u653E\u5217\u8868\u8282\u70B9\u7D22\u5F15\u65E0\u6548"));
1310
- }
1311
- console.log("\u{1F3AF} [MMDPlaylist] \u5F53\u524D\u8282\u70B9:", currentNode.name, "\u7D22\u5F15:", currentNodeIndex);
1312
- const stopNode = (nodeIndex) => {
1313
- const playerComponent = playerComponentRefs.current.get(nodeIndex);
1314
- if (playerComponent && playerComponent.stopCompletely) {
1315
- console.log(`\u23F9\uFE0F [MMDPlaylist] \u505C\u6B62\u8282\u70B9 ${nodeIndex}`);
1316
- playerComponent.stopCompletely();
1317
- } else {
1318
- const playerElement = playerRefsMap.current.get(nodeIndex);
1319
- if (!playerElement) return;
1320
- console.log(`\u23F9\uFE0F [MMDPlaylist] \u505C\u6B62\u8282\u70B9 ${nodeIndex} (DOM\u65B9\u5F0F)`);
1321
- const audioElement = playerElement.querySelector("audio");
1322
- if (audioElement) {
1323
- audioElement.pause();
1324
- audioElement.currentTime = 0;
1325
- console.log(` \u{1F507} \u505C\u6B62\u97F3\u9891`);
1326
- }
1327
- const stopEvent = new CustomEvent("stopCompletely");
1328
- playerElement.dispatchEvent(stopEvent);
1329
- console.log(` \u{1F4E1} \u53D1\u9001\u505C\u6B62\u4E8B\u4EF6`);
1330
- }
1331
- };
1332
- const clearNodeResources = (nodeIndex, excludeCurrent = true) => {
1333
- if (excludeCurrent && nodeIndex === currentNodeIndex) {
1334
- console.log(`\u26A0\uFE0F [MMDPlaylist] \u8DF3\u8FC7\u6E05\u7406\u5F53\u524D\u64AD\u653E\u8282\u70B9 ${nodeIndex}`);
1335
- return;
1336
- }
1337
- const playerComponent = playerComponentRefs.current.get(nodeIndex);
1338
- if (playerComponent && playerComponent.clearResources) {
1339
- console.log(`\u{1F9F9} [MMDPlaylist] \u6E05\u7406\u8282\u70B9 ${nodeIndex} \u8D44\u6E90`);
1340
- playerComponent.clearResources();
1341
- } else {
1342
- const playerElement = playerRefsMap.current.get(nodeIndex);
1343
- if (playerElement) {
1344
- const cleanupEvent = new CustomEvent("cleanupResources");
1345
- playerElement.dispatchEvent(cleanupEvent);
1109
+ const mode = resourcesList ? "list" : resourceOptions ? "options" : "single";
1110
+ const [currentResources, setCurrentResources] = React6.useState(propResources);
1111
+ const [currentId, setCurrentId] = React6.useState(defaultResourceId);
1112
+ const [selection, setSelection] = React6.useState(defaultSelection || {});
1113
+ const [isPlaying, setIsPlaying] = React6.useState(autoPlay);
1114
+ const [volume, setVolume] = React6.useState(initialVolume);
1115
+ const [isMuted, setIsMuted] = React6.useState(initialMuted);
1116
+ const [isLoading, setIsLoading] = React6.useState(true);
1117
+ const [isFullscreen, setIsFullscreen] = React6.useState(false);
1118
+ const [showSettings, setShowSettings] = React6.useState(false);
1119
+ const [showAxes, setShowAxes] = React6.useState(false);
1120
+ const [isLooping, setIsLooping] = React6.useState(loop);
1121
+ const playerRef = React6.useRef(null);
1122
+ const containerRef = React6.useRef(null);
1123
+ React6.useEffect(() => {
1124
+ if (mode === "list" && resourcesList) {
1125
+ const targetId = currentId || resourcesList[0]?.id;
1126
+ const item = resourcesList.find((i) => i.id === targetId);
1127
+ if (item) {
1128
+ setCurrentResources(item.resources);
1129
+ setCurrentId(item.id);
1346
1130
  }
1347
- }
1348
- };
1349
- const emergencyMemoryCleanup = () => {
1350
- if (window.performance?.memory) {
1351
- const memInfo = window.performance.memory;
1352
- const usage = memInfo.usedJSHeapSize / memInfo.totalJSHeapSize;
1353
- if (usage > 0.9) {
1354
- console.error(`\u{1F6A8} [MMDPlaylist] \u5185\u5B58\u4F7F\u7528\u4E25\u91CD\u8FC7\u9AD8 (${(usage * 100).toFixed(1)}%)\uFF0C\u7D27\u6025\u6E05\u7406`);
1355
- const nodesToClean = editableNodes.map((_, index) => ({
1356
- index,
1357
- distance: Math.abs(index - currentNodeIndex)
1358
- })).filter((node) => node.distance > 2).sort((a, b) => b.distance - a.distance).slice(0, 2);
1359
- nodesToClean.forEach(({ index }) => {
1360
- console.warn(`\u{1F9F9} [MMDPlaylist] \u7D27\u6025\u6E05\u7406\u8282\u70B9 ${index}`);
1361
- clearNodeResources(index, false);
1362
- });
1363
- if (window.gc) {
1364
- try {
1365
- window.gc();
1366
- } catch (e) {
1367
- }
1131
+ } else if (mode === "options" && resourceOptions) {
1132
+ const res = { modelPath: "" };
1133
+ if (selection.modelId) {
1134
+ const m = resourceOptions.models.find((o) => o.id === selection.modelId);
1135
+ if (m) res.modelPath = m.path;
1136
+ } else if (resourceOptions.models.length > 0) {
1137
+ const firstModel = resourceOptions.models[0];
1138
+ if (firstModel) {
1139
+ res.modelPath = firstModel.path;
1140
+ setSelection((s) => ({ ...s, modelId: firstModel.id }));
1368
1141
  }
1369
1142
  }
1370
- }
1371
- };
1372
- React2.useEffect(() => {
1373
- console.log(`\u{1F504} [MMDPlaylist] \u8282\u70B9\u5207\u6362: ${currentNodeIndex} - ${currentNode.name}`);
1374
- editableNodes.forEach((_, index) => {
1375
- if (index !== currentNodeIndex) {
1376
- stopNode(index);
1143
+ if (selection.motionId) {
1144
+ const m = resourceOptions.motions.find((o) => o.id === selection.motionId);
1145
+ if (m) res.motionPath = m.path;
1377
1146
  }
1378
- });
1379
- const nodesToKeep = /* @__PURE__ */ new Set();
1380
- nodesToKeep.add(currentNodeIndex);
1381
- if (playlist.loop && currentNodeIndex > 0) {
1382
- nodesToKeep.add(currentNodeIndex - 1);
1383
- }
1384
- if (currentNodeIndex < editableNodes.length - 1) {
1385
- nodesToKeep.add(currentNodeIndex + 1);
1386
- }
1387
- editableNodes.forEach((_, index) => {
1388
- if (!nodesToKeep.has(index)) {
1389
- clearNodeResources(index, false);
1147
+ if (selection.cameraId) {
1148
+ const c = resourceOptions.cameras?.find((o) => o.id === selection.cameraId);
1149
+ if (c) res.cameraPath = c.path;
1390
1150
  }
1391
- });
1392
- onNodeChange?.(currentNodeIndex, currentNode);
1393
- if (!isPreloading && (isAutoSwitchRef.current || playlist.autoPlay)) {
1394
- console.log(`\u25B6\uFE0F [MMDPlaylist] \u51C6\u5907\u64AD\u653E\u8282\u70B9 ${currentNodeIndex}`);
1395
- if (!preloadedNodes.has(currentNodeIndex)) {
1396
- console.warn(`\u26A0\uFE0F [MMDPlaylist] \u8282\u70B9 ${currentNodeIndex} \u5C1A\u672A\u9884\u52A0\u8F7D\u5B8C\u6210\uFF0C\u7B49\u5F85...`);
1397
- return;
1151
+ if (selection.audioId) {
1152
+ const a = resourceOptions.audios?.find((o) => o.id === selection.audioId);
1153
+ if (a) res.audioPath = a.path;
1398
1154
  }
1399
- requestAnimationFrame(() => {
1400
- const playerElement = playerRefsMap.current.get(currentNodeIndex);
1401
- if (playerElement) {
1402
- const playButton = playerElement.querySelector('button[title="\u64AD\u653E"]');
1403
- if (playButton) {
1404
- console.log(`\u{1F3AC} [MMDPlaylist] \u89E6\u53D1\u8282\u70B9 ${currentNodeIndex} \u64AD\u653E`);
1405
- playButton.click();
1406
- } else {
1407
- console.warn(`\u26A0\uFE0F [MMDPlaylist] \u672A\u627E\u5230\u8282\u70B9 ${currentNodeIndex} \u7684\u64AD\u653E\u6309\u94AE`);
1408
- }
1409
- } else {
1410
- console.warn(`\u26A0\uFE0F [MMDPlaylist] \u672A\u627E\u5230\u8282\u70B9 ${currentNodeIndex} \u7684 DOM \u5143\u7D20`);
1411
- }
1412
- });
1155
+ if (selection.stageId) {
1156
+ const s = resourceOptions.stages?.find((o) => o.id === selection.stageId);
1157
+ if (s) res.stageModelPath = s.path;
1158
+ }
1159
+ setCurrentResources(res);
1160
+ } else {
1161
+ setCurrentResources(propResources);
1413
1162
  }
1414
- }, [currentNodeIndex, currentNode, onNodeChange, isPreloading, playlist.autoPlay, playlist.loop, preloadedNodes, editableNodes]);
1415
- const handleNodePreloaded = (nodeIndex) => {
1416
- console.log(`\u2705 [MMDPlaylist] \u8282\u70B9 ${nodeIndex} \u9884\u52A0\u8F7D\u5B8C\u6210`);
1417
- setPreloadedNodes((prev) => {
1418
- const newSet = new Set(prev);
1419
- newSet.add(nodeIndex);
1420
- return newSet;
1421
- });
1422
- };
1423
- React2.useEffect(() => {
1424
- if (preloadedNodes.size === editableNodes.length) {
1425
- console.log("\u{1F389} [MMDPlaylist] \u6240\u6709\u8282\u70B9\u9884\u52A0\u8F7D\u5B8C\u6210");
1426
- setIsPreloading(false);
1427
- onLoad?.();
1163
+ }, [mode, resourcesList, resourceOptions, currentId, selection, propResources]);
1164
+ const toggleFullscreen = React6.useCallback(() => {
1165
+ if (!containerRef.current) return;
1166
+ if (!document.fullscreenElement) {
1167
+ containerRef.current.requestFullscreen().catch((err) => {
1168
+ console.error(`Error attempting to enable fullscreen: ${err.message}`);
1169
+ });
1170
+ setIsFullscreen(true);
1428
1171
  } else {
1429
- const progress = Math.round(preloadedNodes.size / editableNodes.length * 100);
1430
- setPreloadProgress(progress);
1172
+ document.exitFullscreen();
1173
+ setIsFullscreen(false);
1431
1174
  }
1432
- }, [preloadedNodes, editableNodes.length, onLoad]);
1433
- React2.useEffect(() => {
1434
- const checkMemory = () => {
1435
- if (window.performance?.memory) {
1436
- const memInfo = window.performance.memory;
1437
- const usage = memInfo.usedJSHeapSize / memInfo.totalJSHeapSize;
1438
- setMemoryUsage(usage);
1439
- if (usage > 0.9) {
1440
- emergencyMemoryCleanup();
1441
- }
1442
- }
1175
+ }, []);
1176
+ React6.useEffect(() => {
1177
+ const handleFsChange = () => {
1178
+ setIsFullscreen(!!document.fullscreenElement);
1443
1179
  };
1444
- const interval = setInterval(checkMemory, 15e3);
1445
- return () => clearInterval(interval);
1446
- }, [currentNodeIndex, editableNodes]);
1447
- const handlePlaybackEnded = (nodeIndex) => {
1448
- console.log(`\u{1F3B5} [MMDPlaylist] \u8282\u70B9 ${nodeIndex} \u64AD\u653E\u5B8C\u6210`);
1449
- if (nodeIndex !== currentNodeIndexRef.current) {
1450
- console.log(`\u26A0\uFE0F [MMDPlaylist] \u5FFD\u7565\u975E\u5F53\u524D\u8282\u70B9 ${nodeIndex} \u7684\u64AD\u653E\u7ED3\u675F\u4E8B\u4EF6\uFF08\u5F53\u524D: ${currentNodeIndexRef.current}\uFF09`);
1451
- return;
1452
- }
1453
- const node = editableNodes[nodeIndex];
1454
- if (!node) return;
1455
- if (node.loop) {
1456
- console.log("\u{1F501} [MMDPlaylist] \u5F53\u524D\u8282\u70B9\u5FAA\u73AF\u64AD\u653E");
1457
- return;
1458
- }
1459
- const isLastNode = nodeIndex === editableNodes.length - 1;
1460
- if (!isLastNode) {
1461
- console.log(`\u27A1\uFE0F [MMDPlaylist] \u5207\u6362\u5230\u4E0B\u4E00\u4E2A\u8282\u70B9: ${nodeIndex + 1}`);
1462
- isAutoSwitchRef.current = true;
1463
- setCurrentNodeIndex(nodeIndex + 1);
1464
- return;
1465
- }
1466
- if (playlist.loop) {
1467
- console.log("\u{1F501} [MMDPlaylist] \u64AD\u653E\u5217\u8868\u5FAA\u73AF\uFF0C\u56DE\u5230\u7B2C\u4E00\u4E2A\u8282\u70B9");
1468
- isAutoSwitchRef.current = true;
1469
- setCurrentNodeIndex(0);
1470
- return;
1471
- }
1472
- console.log("\u2705 [MMDPlaylist] \u64AD\u653E\u5217\u8868\u64AD\u653E\u5B8C\u6210");
1473
- onPlaylistComplete?.();
1474
- };
1475
- const playlistPrevious = () => {
1476
- const newIndex = currentNodeIndex > 0 ? currentNodeIndex - 1 : editableNodes.length - 1;
1477
- console.log(`\u2B05\uFE0F [MMDPlaylist] \u4E0A\u4E00\u4E2A\u8282\u70B9: ${newIndex}`);
1478
- isAutoSwitchRef.current = false;
1479
- setCurrentNodeIndex(newIndex);
1480
- };
1481
- const playlistNext = () => {
1482
- const newIndex = currentNodeIndex < editableNodes.length - 1 ? currentNodeIndex + 1 : 0;
1483
- console.log(`\u27A1\uFE0F [MMDPlaylist] \u4E0B\u4E00\u4E2A\u8282\u70B9: ${newIndex}`);
1484
- isAutoSwitchRef.current = false;
1485
- setCurrentNodeIndex(newIndex);
1486
- };
1487
- const playlistJumpTo = (index) => {
1488
- if (index < 0 || index >= editableNodes.length) return;
1489
- console.log(`\u{1F3AF} [MMDPlaylist] \u8DF3\u8F6C\u5230\u8282\u70B9: ${index}`);
1490
- isAutoSwitchRef.current = false;
1491
- setCurrentNodeIndex(index);
1492
- };
1493
- const handleDeleteNode = (index) => {
1494
- if (editableNodes.length <= 1) {
1495
- alert("\u64AD\u653E\u5217\u8868\u81F3\u5C11\u9700\u8981\u4FDD\u7559\u4E00\u4E2A\u8282\u70B9");
1496
- return;
1497
- }
1498
- const newNodes = editableNodes.filter((_, i) => i !== index);
1499
- setEditableNodes(newNodes);
1500
- if (index < currentNodeIndex) {
1501
- setCurrentNodeIndex(currentNodeIndex - 1);
1502
- } else if (index === currentNodeIndex) {
1503
- const newIndex = Math.max(0, currentNodeIndex - 1);
1504
- setCurrentNodeIndex(newIndex);
1180
+ document.addEventListener("fullscreenchange", handleFsChange);
1181
+ return () => document.removeEventListener("fullscreenchange", handleFsChange);
1182
+ }, []);
1183
+ const handlePlayPause = () => {
1184
+ if (isPlaying) {
1185
+ playerRef.current?.pause();
1186
+ } else {
1187
+ playerRef.current?.play();
1505
1188
  }
1506
- console.log(`\u{1F5D1}\uFE0F [MMDPlaylist] \u5220\u9664\u8282\u70B9 ${index}`);
1189
+ setIsPlaying(!isPlaying);
1507
1190
  };
1508
- const handleMoveNodeUp = (index) => {
1509
- if (index === 0) return;
1510
- const newNodes = [...editableNodes];
1511
- const temp = newNodes[index - 1];
1512
- newNodes[index - 1] = newNodes[index];
1513
- newNodes[index] = temp;
1514
- setEditableNodes(newNodes);
1515
- if (currentNodeIndex === index) {
1516
- setCurrentNodeIndex(index - 1);
1517
- } else if (currentNodeIndex === index - 1) {
1518
- setCurrentNodeIndex(index);
1519
- }
1520
- console.log(`\u2B06\uFE0F [MMDPlaylist] \u8282\u70B9 ${index} \u4E0A\u79FB`);
1191
+ const handleListSelect = (id) => {
1192
+ setCurrentId(id);
1193
+ setIsPlaying(true);
1194
+ setShowSettings(false);
1521
1195
  };
1522
- const handleMoveNodeDown = (index) => {
1523
- if (index === editableNodes.length - 1) return;
1524
- const newNodes = [...editableNodes];
1525
- const temp = newNodes[index];
1526
- newNodes[index] = newNodes[index + 1];
1527
- newNodes[index + 1] = temp;
1528
- setEditableNodes(newNodes);
1529
- if (currentNodeIndex === index) {
1530
- setCurrentNodeIndex(index + 1);
1531
- } else if (currentNodeIndex === index + 1) {
1532
- setCurrentNodeIndex(index);
1533
- }
1534
- console.log(`\u2B07\uFE0F [MMDPlaylist] \u8282\u70B9 ${index} \u4E0B\u79FB`);
1196
+ const handleOptionSelect = (type, id) => {
1197
+ setSelection((prev) => {
1198
+ const next = { ...prev };
1199
+ if (type === "models") next.modelId = id;
1200
+ if (type === "motions") next.motionId = id;
1201
+ if (type === "cameras") next.cameraId = id;
1202
+ if (type === "audios") next.audioId = id;
1203
+ if (type === "stages") next.stageId = id;
1204
+ return next;
1205
+ });
1535
1206
  };
1536
- const shouldAutoPlayInitial = playlist.autoPlay && currentNodeIndex === defaultNodeIndex && !isPreloading;
1537
- return /* @__PURE__ */ React2__default.default.createElement("div", { className: `relative ${className || ""}`, style }, editableNodes.map((node, index) => {
1538
- return /* @__PURE__ */ React2__default.default.createElement(
1539
- "div",
1207
+ if (!currentResources) {
1208
+ return /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex h-full w-full items-center justify-center bg-black text-white" }, "No Resources Configured");
1209
+ }
1210
+ return /* @__PURE__ */ React6__default.default.createElement(
1211
+ "div",
1212
+ {
1213
+ ref: containerRef,
1214
+ className: `relative overflow-hidden bg-black group flex ${className}`,
1215
+ style
1216
+ },
1217
+ /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex-1 relative" }, /* @__PURE__ */ React6__default.default.createElement(
1218
+ MMDPlayerBase,
1540
1219
  {
1541
- key: `player-${node.id}-${index}`,
1542
- ref: (el) => {
1543
- if (el) {
1544
- playerRefsMap.current.set(index, el);
1545
- }
1220
+ key: mode === "list" ? currentId : JSON.stringify(currentResources),
1221
+ ref: playerRef,
1222
+ resources: currentResources,
1223
+ stage,
1224
+ autoPlay,
1225
+ loop: isLooping,
1226
+ volume,
1227
+ muted: isMuted,
1228
+ showAxes,
1229
+ mobileOptimization,
1230
+ onLoad: () => {
1231
+ setIsLoading(false);
1232
+ onLoad?.();
1233
+ if (isPlaying) playerRef.current?.play();
1546
1234
  },
1547
- className: "absolute inset-0",
1548
- style: {
1549
- visibility: index === currentNodeIndex ? "visible" : "hidden",
1550
- zIndex: index === currentNodeIndex ? 1 : 0
1551
- }
1552
- },
1553
- /* @__PURE__ */ React2__default.default.createElement(
1554
- MMDPlayerEnhanced,
1555
- {
1556
- ref: (componentRef) => {
1557
- if (componentRef) {
1558
- playerComponentRefs.current.set(index, componentRef);
1559
- }
1560
- },
1561
- resources: node.resources,
1562
- stage,
1563
- autoPlay: index === currentNodeIndex && shouldAutoPlayInitial,
1564
- loop: node.loop || false,
1565
- className: "h-full w-full",
1566
- onLoad: () => {
1567
- handleNodePreloaded(index);
1568
- },
1569
- onError: (error) => {
1570
- console.error(`\u274C [MMDPlaylist] \u8282\u70B9 ${index} \u52A0\u8F7D\u5931\u8D25:`, error);
1571
- if (index === currentNodeIndex) {
1572
- onError?.(error);
1573
- }
1574
- },
1575
- onAudioEnded: () => handlePlaybackEnded(index),
1576
- onAnimationEnded: () => handlePlaybackEnded(index)
1577
- }
1578
- )
1579
- );
1580
- }), isPreloading && /* @__PURE__ */ React2__default.default.createElement("div", { className: "absolute inset-0 z-50 flex flex-col items-center justify-center bg-black/80 backdrop-blur-sm" }, /* @__PURE__ */ React2__default.default.createElement("div", { className: "text-center" }, /* @__PURE__ */ React2__default.default.createElement("div", { className: "mb-4 text-2xl font-bold text-white" }, "\u6B63\u5728\u9884\u52A0\u8F7D\u64AD\u653E\u5217\u8868"), /* @__PURE__ */ React2__default.default.createElement("div", { className: "mb-2 text-lg text-white/80" }, preloadedNodes.size, " / ", editableNodes.length, " \u8282\u70B9"), /* @__PURE__ */ React2__default.default.createElement("div", { className: "h-2 w-64 overflow-hidden rounded-full bg-white/20" }, /* @__PURE__ */ React2__default.default.createElement(
1235
+ onPlay: () => {
1236
+ setIsPlaying(true);
1237
+ onPlay?.();
1238
+ },
1239
+ onPause: () => {
1240
+ setIsPlaying(false);
1241
+ onPause?.();
1242
+ },
1243
+ onEnded: () => {
1244
+ setIsPlaying(false);
1245
+ onEnded?.();
1246
+ },
1247
+ ...rest
1248
+ }
1249
+ ), isLoading && /* @__PURE__ */ React6__default.default.createElement("div", { className: "absolute inset-0 z-10 flex items-center justify-center bg-black/50 backdrop-blur-sm" }, /* @__PURE__ */ React6__default.default.createElement("div", { className: "h-10 w-10 animate-spin rounded-full border-4 border-white/20 border-t-blue-500" })), /* @__PURE__ */ React6__default.default.createElement("div", { className: `transition-opacity duration-300 ${isPlaying && !showSettings ? "opacity-0 group-hover:opacity-100" : "opacity-100"}` }, /* @__PURE__ */ React6__default.default.createElement(
1250
+ ControlPanel,
1251
+ {
1252
+ isPlaying,
1253
+ isFullscreen,
1254
+ isLooping,
1255
+ showSettings: mode !== "single",
1256
+ showAxes,
1257
+ title: mode === "list" ? resourcesList?.find((i) => i.id === currentId)?.name : void 0,
1258
+ onPlayPause: handlePlayPause,
1259
+ onToggleFullscreen: toggleFullscreen,
1260
+ onToggleLoop: () => setIsLooping(!isLooping),
1261
+ onToggleAxes: () => setShowAxes(!showAxes),
1262
+ onOpenSettings: () => setShowSettings(true)
1263
+ }
1264
+ )), showSettings && (mode === "list" || mode === "options") && /* @__PURE__ */ React6__default.default.createElement(
1265
+ SettingsPanel,
1266
+ {
1267
+ mode,
1268
+ items: resourcesList,
1269
+ currentId,
1270
+ onSelectId: handleListSelect,
1271
+ options: resourceOptions,
1272
+ currentSelection: selection,
1273
+ onSelectOption: handleOptionSelect,
1274
+ onClose: () => setShowSettings(false)
1275
+ }
1276
+ )),
1277
+ showDebugInfo && /* @__PURE__ */ React6__default.default.createElement("div", { className: "w-96 bg-gray-900/95 border-l border-gray-700 p-4 overflow-y-auto" }, /* @__PURE__ */ React6__default.default.createElement(
1278
+ MMDPlayerEnhancedDebugInfo,
1279
+ {
1280
+ isPlaying,
1281
+ isLooping,
1282
+ isFullscreen,
1283
+ showAxes,
1284
+ isLoading,
1285
+ currentResourceId: currentId,
1286
+ currentResourceName: mode === "list" ? resourcesList?.find((i) => i.id === currentId)?.name : void 0,
1287
+ mode,
1288
+ totalResources: resourcesList?.length || 1
1289
+ }
1290
+ ))
1291
+ );
1292
+ };
1293
+ var MMDPlaylistDebugInfo = ({
1294
+ playlistName,
1295
+ currentIndex,
1296
+ currentNode,
1297
+ totalNodes,
1298
+ isPlaying,
1299
+ isListLooping,
1300
+ isNodeLooping,
1301
+ preloadStrategy,
1302
+ isLoading,
1303
+ isFullscreen,
1304
+ showAxes,
1305
+ preloadedNodes
1306
+ }) => {
1307
+ const [memoryInfo, setMemoryInfo] = React6.useState(null);
1308
+ React6.useEffect(() => {
1309
+ const timer = setInterval(() => {
1310
+ if (performance.memory) {
1311
+ const used = (performance.memory.usedJSHeapSize / 1048576).toFixed(1);
1312
+ const total = (performance.memory.totalJSHeapSize / 1048576).toFixed(1);
1313
+ const limit = (performance.memory.jsHeapSizeLimit / 1048576).toFixed(1);
1314
+ setMemoryInfo({ used, total, limit });
1315
+ }
1316
+ }, 1e3);
1317
+ return () => clearInterval(timer);
1318
+ }, []);
1319
+ return /* @__PURE__ */ React6__default.default.createElement("div", { className: "text-white text-xs font-mono" }, /* @__PURE__ */ React6__default.default.createElement("h3", { className: "text-sm font-bold mb-3 pb-2 border-b border-gray-700" }, "\u{1F3AD} MMDPlaylist Debug"), /* @__PURE__ */ React6__default.default.createElement("div", { className: "mb-4" }, /* @__PURE__ */ React6__default.default.createElement("h4", { className: "text-gray-400 mb-2" }, "\u64AD\u653E\u5217\u8868"), /* @__PURE__ */ React6__default.default.createElement("div", { className: "space-y-1 pl-2" }, /* @__PURE__ */ React6__default.default.createElement("div", { className: "text-white truncate" }, playlistName), /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-gray-400" }, "\u8FDB\u5EA6:"), /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-blue-400" }, currentIndex + 1, " / ", totalNodes)), /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-gray-400" }, "\u5217\u8868\u5FAA\u73AF:"), /* @__PURE__ */ React6__default.default.createElement(StatusBadge2, { active: isListLooping, label: isListLooping ? "On" : "Off" })))), /* @__PURE__ */ React6__default.default.createElement("div", { className: "mb-4" }, /* @__PURE__ */ React6__default.default.createElement("h4", { className: "text-gray-400 mb-2" }, "\u5F53\u524D\u8282\u70B9"), /* @__PURE__ */ React6__default.default.createElement("div", { className: "p-2 bg-gray-800 rounded space-y-1" }, /* @__PURE__ */ React6__default.default.createElement("div", { className: "text-white font-semibold truncate" }, currentNode.name), /* @__PURE__ */ React6__default.default.createElement("div", { className: "text-gray-500 text-[10px] truncate" }, "ID: ", currentNode.id), currentNode.duration && /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-gray-400" }, "\u65F6\u957F:"), /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-green-400" }, currentNode.duration, "s")), /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-gray-400" }, "\u8282\u70B9\u5FAA\u73AF:"), /* @__PURE__ */ React6__default.default.createElement(StatusBadge2, { active: isNodeLooping, label: isNodeLooping ? "On" : "Off" })))), /* @__PURE__ */ React6__default.default.createElement("div", { className: "mb-4" }, /* @__PURE__ */ React6__default.default.createElement("h4", { className: "text-gray-400 mb-2" }, "\u64AD\u653E\u72B6\u6001"), /* @__PURE__ */ React6__default.default.createElement("div", { className: "space-y-1 pl-2" }, /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-gray-400" }, "\u64AD\u653E\u4E2D:"), /* @__PURE__ */ React6__default.default.createElement(StatusBadge2, { active: isPlaying, label: isPlaying ? "Playing" : "Paused" })), /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-gray-400" }, "\u52A0\u8F7D\u4E2D:"), /* @__PURE__ */ React6__default.default.createElement(StatusBadge2, { active: isLoading, label: isLoading ? "Loading" : "Ready" })))), /* @__PURE__ */ React6__default.default.createElement("div", { className: "mb-4" }, /* @__PURE__ */ React6__default.default.createElement("h4", { className: "text-gray-400 mb-2" }, "\u9884\u52A0\u8F7D\u7B56\u7565"), /* @__PURE__ */ React6__default.default.createElement("div", { className: "space-y-1 pl-2" }, /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-gray-400" }, "\u7B56\u7565:"), /* @__PURE__ */ React6__default.default.createElement("span", { className: `px-2 py-0.5 rounded text-[10px] font-bold uppercase ${preloadStrategy === "all" ? "bg-red-600 text-white" : preloadStrategy === "next" ? "bg-yellow-600 text-white" : "bg-gray-700 text-gray-400"}` }, preloadStrategy)), /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-gray-400" }, "\u5DF2\u9884\u52A0\u8F7D:"), /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-purple-400" }, preloadedNodes.length)), preloadedNodes.length > 0 && /* @__PURE__ */ React6__default.default.createElement("div", { className: "mt-2 p-2 bg-gray-800 rounded" }, /* @__PURE__ */ React6__default.default.createElement("div", { className: "text-gray-400 text-[10px] mb-1" }, "\u9884\u52A0\u8F7D\u8282\u70B9\u7D22\u5F15"), /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex flex-wrap gap-1" }, preloadedNodes.map((idx) => /* @__PURE__ */ React6__default.default.createElement(
1320
+ "span",
1321
+ {
1322
+ key: idx,
1323
+ className: `px-1.5 py-0.5 rounded text-[10px] ${idx === currentIndex ? "bg-green-600 text-white font-bold" : "bg-gray-700 text-gray-300"}`
1324
+ },
1325
+ idx
1326
+ )))))), /* @__PURE__ */ React6__default.default.createElement("div", { className: "mb-4" }, /* @__PURE__ */ React6__default.default.createElement("h4", { className: "text-gray-400 mb-2" }, "\u89C6\u56FE\u72B6\u6001"), /* @__PURE__ */ React6__default.default.createElement("div", { className: "space-y-1 pl-2" }, /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-gray-400" }, "\u5168\u5C4F:"), /* @__PURE__ */ React6__default.default.createElement(StatusBadge2, { active: isFullscreen, label: isFullscreen ? "Yes" : "No" })), /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-gray-400" }, "\u5750\u6807\u8F74:"), /* @__PURE__ */ React6__default.default.createElement(StatusBadge2, { active: showAxes, label: showAxes ? "Show" : "Hide" })))), memoryInfo && /* @__PURE__ */ React6__default.default.createElement("div", { className: "mb-4" }, /* @__PURE__ */ React6__default.default.createElement("h4", { className: "text-gray-400 mb-2" }, "\u5185\u5B58\u76D1\u63A7 (Chrome only)"), /* @__PURE__ */ React6__default.default.createElement("div", { className: "space-y-2 p-2 bg-gray-800 rounded" }, /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex items-center justify-between text-[10px]" }, /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-gray-400" }, "\u5DF2\u7528:"), /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-yellow-400 font-bold" }, memoryInfo.used, " MB")), /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex items-center justify-between text-[10px]" }, /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-gray-400" }, "\u603B\u8BA1:"), /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-blue-400" }, memoryInfo.total, " MB")), /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex items-center justify-between text-[10px]" }, /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-gray-400" }, "\u9650\u5236:"), /* @__PURE__ */ React6__default.default.createElement("span", { className: "text-gray-400" }, memoryInfo.limit, " MB")), /* @__PURE__ */ React6__default.default.createElement("div", { className: "mt-2" }, /* @__PURE__ */ React6__default.default.createElement("div", { className: "bg-gray-700 rounded-full h-2 overflow-hidden" }, /* @__PURE__ */ React6__default.default.createElement(
1581
1327
  "div",
1582
1328
  {
1583
- className: "h-full bg-gradient-to-r from-purple-500 to-blue-500 transition-all duration-300",
1584
- style: { width: `${preloadProgress}%` }
1329
+ className: `h-full transition-all duration-300 ${parseFloat(memoryInfo.used) / parseFloat(memoryInfo.limit) * 100 > 80 ? "bg-red-500" : parseFloat(memoryInfo.used) / parseFloat(memoryInfo.limit) * 100 > 60 ? "bg-yellow-500" : "bg-green-500"}`,
1330
+ style: {
1331
+ width: `${Math.min(100, parseFloat(memoryInfo.used) / parseFloat(memoryInfo.limit) * 100)}%`
1332
+ }
1585
1333
  }
1586
- )), /* @__PURE__ */ React2__default.default.createElement("div", { className: "mt-4 text-sm text-white/60" }, "\u9884\u52A0\u8F7D\u6240\u6709\u8D44\u6E90\u540E\uFF0C\u5207\u6362\u8282\u70B9\u5C06\u65E0\u9700\u7B49\u5F85"))), !isPreloading && /* @__PURE__ */ React2__default.default.createElement("div", { className: "absolute bottom-4 right-4 z-10 flex gap-2" }, editableNodes.length > 1 && /* @__PURE__ */ React2__default.default.createElement(
1587
- "button",
1588
- {
1589
- onClick: playlistPrevious,
1590
- className: "flex h-12 w-12 items-center justify-center rounded-full bg-blue-500/90 text-xl text-white shadow-lg backdrop-blur-md transition-all hover:bg-blue-600 hover:scale-110",
1591
- title: "\u4E0A\u4E00\u4E2A\u8282\u70B9"
1592
- },
1593
- "\u23EE\uFE0F"
1594
- ), /* @__PURE__ */ React2__default.default.createElement(
1595
- "button",
1334
+ )), /* @__PURE__ */ React6__default.default.createElement("div", { className: "text-[9px] text-gray-500 mt-1 text-center" }, (parseFloat(memoryInfo.used) / parseFloat(memoryInfo.limit) * 100).toFixed(1), "%")))), /* @__PURE__ */ React6__default.default.createElement("div", { className: "mb-4" }, /* @__PURE__ */ React6__default.default.createElement("h4", { className: "text-gray-400 mb-2" }, "\u8282\u70B9\u5217\u8868"), /* @__PURE__ */ React6__default.default.createElement("div", { className: "space-y-1 max-h-40 overflow-y-auto" }, Array.from({ length: totalNodes }).map((_, idx) => /* @__PURE__ */ React6__default.default.createElement(
1335
+ "div",
1596
1336
  {
1597
- onClick: () => setShowSettings(true),
1598
- className: "flex h-12 w-12 items-center justify-center rounded-full bg-purple-500/90 text-xl text-white shadow-lg backdrop-blur-md transition-all hover:bg-purple-600 hover:scale-110",
1599
- title: "\u64AD\u653E\u5217\u8868\u8BBE\u7F6E"
1337
+ key: idx,
1338
+ className: `px-2 py-1 rounded text-[10px] flex items-center justify-between ${idx === currentIndex ? "bg-blue-600 text-white font-bold" : preloadedNodes.includes(idx) ? "bg-yellow-900/50 text-yellow-300" : "bg-gray-800 text-gray-400"}`
1600
1339
  },
1601
- "\u2699\uFE0F"
1602
- ), editableNodes.length > 1 && /* @__PURE__ */ React2__default.default.createElement(
1603
- "button",
1604
- {
1605
- onClick: playlistNext,
1606
- className: "flex h-12 w-12 items-center justify-center rounded-full bg-blue-500/90 text-xl text-white shadow-lg backdrop-blur-md transition-all hover:bg-blue-600 hover:scale-110",
1607
- title: "\u4E0B\u4E00\u4E2A\u8282\u70B9"
1340
+ /* @__PURE__ */ React6__default.default.createElement("span", null, "\u8282\u70B9 ", idx),
1341
+ idx === currentIndex && /* @__PURE__ */ React6__default.default.createElement("span", null, "\u25B6"),
1342
+ preloadedNodes.includes(idx) && idx !== currentIndex && /* @__PURE__ */ React6__default.default.createElement("span", null, "\u23F3")
1343
+ )))), /* @__PURE__ */ React6__default.default.createElement("div", { className: "mt-auto pt-4 border-t border-gray-700" }, /* @__PURE__ */ React6__default.default.createElement("div", { className: "text-gray-500 text-[10px]" }, "Last Update: ", (/* @__PURE__ */ new Date()).toLocaleTimeString())));
1344
+ };
1345
+ var StatusBadge2 = ({ active, label }) => /* @__PURE__ */ React6__default.default.createElement(
1346
+ "span",
1347
+ {
1348
+ className: `px-2 py-0.5 rounded text-[10px] font-bold ${active ? "bg-green-600 text-white" : "bg-gray-700 text-gray-400"}`
1349
+ },
1350
+ label
1351
+ );
1352
+
1353
+ // src/mmd/components/MMDPlaylist.tsx
1354
+ var MMDPlaylist = ({
1355
+ playlist,
1356
+ stage,
1357
+ mobileOptimization,
1358
+ onNodeChange,
1359
+ onPlaylistComplete,
1360
+ onError,
1361
+ showDebugInfo = false,
1362
+ className,
1363
+ style
1364
+ }) => {
1365
+ const { nodes, loop = false, preload = "none", autoPlay = false } = playlist;
1366
+ const [currentIndex, setCurrentIndex] = React6.useState(0);
1367
+ const [isPlaying, setIsPlaying] = React6.useState(autoPlay);
1368
+ const [isLoading, setIsLoading] = React6.useState(true);
1369
+ const [isFullscreen, setIsFullscreen] = React6.useState(false);
1370
+ const [showAxes, setShowAxes] = React6.useState(false);
1371
+ const [isLooping, setIsLooping] = React6.useState(false);
1372
+ const [isListLooping, setIsListLooping] = React6.useState(loop);
1373
+ const [showPlaylist, setShowPlaylist] = React6.useState(false);
1374
+ const [isTransitioning, setIsTransitioning] = React6.useState(false);
1375
+ const playerRef = React6.useRef(null);
1376
+ const containerRef = React6.useRef(null);
1377
+ const preloadedRef = React6.useRef(/* @__PURE__ */ new Set());
1378
+ const currentNode = nodes[currentIndex];
1379
+ const goToNode = React6.useCallback(
1380
+ (index) => {
1381
+ if (index < 0 || index >= nodes.length) return;
1382
+ if (isTransitioning) return;
1383
+ const node = nodes[index];
1384
+ if (!node) return;
1385
+ console.log(`[MMDPlaylist] Starting transition to node ${index}`);
1386
+ const wasPlaying = isPlaying;
1387
+ setIsPlaying(false);
1388
+ setIsTransitioning(true);
1389
+ requestAnimationFrame(() => {
1390
+ requestAnimationFrame(() => {
1391
+ setTimeout(() => {
1392
+ console.log(`[MMDPlaylist] Loading new node ${index}`);
1393
+ setCurrentIndex(index);
1394
+ setIsLoading(true);
1395
+ onNodeChange?.(node, index);
1396
+ requestAnimationFrame(() => {
1397
+ requestAnimationFrame(() => {
1398
+ setTimeout(() => {
1399
+ setIsTransitioning(false);
1400
+ if (wasPlaying) {
1401
+ setIsPlaying(true);
1402
+ }
1403
+ console.log(`[MMDPlaylist] Transition to node ${index} completed`);
1404
+ }, 100);
1405
+ });
1406
+ });
1407
+ }, 300);
1408
+ });
1409
+ });
1608
1410
  },
1609
- "\u23ED\uFE0F"
1610
- )), !isPreloading && /* @__PURE__ */ React2__default.default.createElement("div", { className: "absolute left-4 top-4 z-10 rounded-lg bg-black/50 px-4 py-2 backdrop-blur-md" }, /* @__PURE__ */ React2__default.default.createElement("div", { className: "flex items-center gap-2" }, /* @__PURE__ */ React2__default.default.createElement("span", { className: "text-sm font-bold text-white/60" }, currentNodeIndex + 1, "/", editableNodes.length), /* @__PURE__ */ React2__default.default.createElement("span", { className: "text-sm font-medium text-white" }, currentNode.name), currentNode.loop && /* @__PURE__ */ React2__default.default.createElement("span", { className: "rounded bg-white/20 px-2 py-0.5 text-xs text-white" }, "\u{1F501}"))), showSettings && /* @__PURE__ */ React2__default.default.createElement(
1411
+ [nodes, isPlaying, isTransitioning, onNodeChange]
1412
+ );
1413
+ const handlePrevious = React6.useCallback(() => {
1414
+ const prevIndex = currentIndex - 1;
1415
+ if (prevIndex >= 0) {
1416
+ goToNode(prevIndex);
1417
+ } else if (isListLooping) {
1418
+ goToNode(nodes.length - 1);
1419
+ }
1420
+ }, [currentIndex, isListLooping, nodes.length, goToNode]);
1421
+ const handleNext = React6.useCallback(() => {
1422
+ const nextIndex = currentIndex + 1;
1423
+ if (nextIndex < nodes.length) {
1424
+ goToNode(nextIndex);
1425
+ } else if (isListLooping) {
1426
+ goToNode(0);
1427
+ } else {
1428
+ setIsPlaying(false);
1429
+ onPlaylistComplete?.();
1430
+ }
1431
+ }, [currentIndex, isListLooping, nodes.length, goToNode, onPlaylistComplete]);
1432
+ const handlePlayPause = React6.useCallback(() => {
1433
+ if (isPlaying) {
1434
+ playerRef.current?.pause();
1435
+ setIsPlaying(false);
1436
+ } else {
1437
+ playerRef.current?.play();
1438
+ setIsPlaying(true);
1439
+ }
1440
+ }, [isPlaying]);
1441
+ const toggleFullscreen = React6.useCallback(() => {
1442
+ if (!containerRef.current) return;
1443
+ if (!document.fullscreenElement) {
1444
+ containerRef.current.requestFullscreen().catch((err) => {
1445
+ console.error(`Error attempting to enable fullscreen: ${err.message}`);
1446
+ });
1447
+ setIsFullscreen(true);
1448
+ } else {
1449
+ document.exitFullscreen();
1450
+ setIsFullscreen(false);
1451
+ }
1452
+ }, []);
1453
+ React6.useEffect(() => {
1454
+ const handleFsChange = () => {
1455
+ setIsFullscreen(!!document.fullscreenElement);
1456
+ };
1457
+ document.addEventListener("fullscreenchange", handleFsChange);
1458
+ return () => document.removeEventListener("fullscreenchange", handleFsChange);
1459
+ }, []);
1460
+ const handleEnded = React6.useCallback(() => {
1461
+ if (isLooping) {
1462
+ playerRef.current?.play();
1463
+ } else {
1464
+ handleNext();
1465
+ }
1466
+ }, [isLooping, handleNext]);
1467
+ React6.useEffect(() => {
1468
+ if (preload === "none") return;
1469
+ if (preload === "all") {
1470
+ nodes.forEach((node, idx) => {
1471
+ if (!preloadedRef.current.has(idx)) {
1472
+ preloadedRef.current.add(idx);
1473
+ console.log(`[MMDPlaylist] Preload strategy: all - marked node ${idx} (${node.name})`);
1474
+ }
1475
+ });
1476
+ } else if (preload === "next") {
1477
+ const nextIndex = (currentIndex + 1) % nodes.length;
1478
+ const nextNode = nodes[nextIndex];
1479
+ if (nextNode && !preloadedRef.current.has(nextIndex)) {
1480
+ preloadedRef.current.add(nextIndex);
1481
+ console.log(`[MMDPlaylist] Preload strategy: next - marked node ${nextIndex} (${nextNode.name})`);
1482
+ }
1483
+ }
1484
+ }, [currentIndex, nodes, preload]);
1485
+ React6.useEffect(() => {
1486
+ if (preload === "none" || preload === "all") return;
1487
+ if (nodes.length === 0) return;
1488
+ const nextIndex = (currentIndex + 1) % nodes.length;
1489
+ const keepIndices = /* @__PURE__ */ new Set([currentIndex, nextIndex]);
1490
+ const toRemove = [];
1491
+ preloadedRef.current.forEach((idx) => {
1492
+ if (idx >= nodes.length || !keepIndices.has(idx)) {
1493
+ toRemove.push(idx);
1494
+ }
1495
+ });
1496
+ if (toRemove.length > 0) {
1497
+ toRemove.forEach((idx) => {
1498
+ preloadedRef.current.delete(idx);
1499
+ console.log(`[MMDPlaylist] Memory cleanup: removed preload mark for node ${idx}`);
1500
+ });
1501
+ }
1502
+ }, [currentIndex, nodes.length, preload]);
1503
+ React6.useEffect(() => {
1504
+ return () => {
1505
+ console.log("[MMDPlaylist] Component unmounted, clearing all preload marks");
1506
+ preloadedRef.current.clear();
1507
+ };
1508
+ }, []);
1509
+ if (!currentNode) {
1510
+ return /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex h-full w-full items-center justify-center bg-black text-white" }, "\u64AD\u653E\u5217\u8868\u4E3A\u7A7A");
1511
+ }
1512
+ const showPrevNext = nodes.length > 1;
1513
+ return /* @__PURE__ */ React6__default.default.createElement(
1611
1514
  "div",
1612
1515
  {
1613
- className: "absolute inset-0 z-[100] flex items-start justify-end bg-black/40",
1614
- onClick: () => setShowSettings(false)
1516
+ ref: containerRef,
1517
+ className: `relative overflow-hidden bg-black group flex h-full ${className}`,
1518
+ style
1615
1519
  },
1616
- /* @__PURE__ */ React2__default.default.createElement(
1520
+ /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex-1 relative" }, !isTransitioning && /* @__PURE__ */ React6__default.default.createElement(
1521
+ MMDPlayerBase,
1522
+ {
1523
+ key: currentNode.id,
1524
+ ref: playerRef,
1525
+ resources: currentNode.resources,
1526
+ stage,
1527
+ autoPlay: autoPlay && currentIndex === 0,
1528
+ loop: isLooping,
1529
+ showAxes,
1530
+ mobileOptimization,
1531
+ onLoad: () => {
1532
+ setIsLoading(false);
1533
+ if (isPlaying && currentIndex > 0) {
1534
+ playerRef.current?.play();
1535
+ }
1536
+ },
1537
+ onPlay: () => setIsPlaying(true),
1538
+ onPause: () => setIsPlaying(false),
1539
+ onEnded: handleEnded,
1540
+ onError
1541
+ }
1542
+ ), (isLoading || isTransitioning) && /* @__PURE__ */ React6__default.default.createElement("div", { className: "absolute inset-0 z-10 flex items-center justify-center bg-black/50 backdrop-blur-sm" }, /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex flex-col items-center gap-3" }, /* @__PURE__ */ React6__default.default.createElement("div", { className: "h-10 w-10 animate-spin rounded-full border-4 border-white/20 border-t-blue-500" }), /* @__PURE__ */ React6__default.default.createElement("div", { className: "text-sm text-white/80" }, isTransitioning ? "\u5207\u6362\u4E2D..." : `\u6B63\u5728\u52A0\u8F7D ${currentIndex + 1} / ${nodes.length}`))), /* @__PURE__ */ React6__default.default.createElement(
1617
1543
  "div",
1618
1544
  {
1619
- className: "relative m-4 flex w-full max-w-md flex-col overflow-hidden rounded-xl bg-gradient-to-br from-gray-900 to-black shadow-2xl border border-white/20",
1620
- style: { maxHeight: "calc(100vh - 2rem)" },
1621
- onClick: (e) => e.stopPropagation()
1545
+ className: `transition-opacity duration-300 ${isPlaying && !showPlaylist ? "opacity-0 group-hover:opacity-100" : "opacity-100"}`
1622
1546
  },
1623
- /* @__PURE__ */ React2__default.default.createElement("div", { className: "flex items-center justify-between border-b border-white/10 bg-gradient-to-r from-purple-900/50 to-blue-900/50 px-4 py-3 flex-shrink-0" }, /* @__PURE__ */ React2__default.default.createElement("h3", { className: "flex items-center gap-2 text-base font-bold text-white" }, "\u2699\uFE0F \u64AD\u653E\u5217\u8868\u914D\u7F6E"), /* @__PURE__ */ React2__default.default.createElement(
1624
- "button",
1547
+ /* @__PURE__ */ React6__default.default.createElement(
1548
+ ControlPanel,
1625
1549
  {
1626
- onClick: () => setShowSettings(false),
1627
- className: "text-xl text-white/60 transition-colors hover:text-white"
1550
+ isPlaying,
1551
+ isFullscreen,
1552
+ isLooping,
1553
+ isListLooping,
1554
+ showSettings: true,
1555
+ showAxes,
1556
+ showPrevNext,
1557
+ title: currentNode.name,
1558
+ subtitle: `${currentIndex + 1} / ${nodes.length}`,
1559
+ onPlayPause: handlePlayPause,
1560
+ onPrevious: handlePrevious,
1561
+ onNext: handleNext,
1562
+ onToggleFullscreen: toggleFullscreen,
1563
+ onToggleLoop: () => setIsLooping(!isLooping),
1564
+ onToggleListLoop: () => setIsListLooping(!isListLooping),
1565
+ onToggleAxes: () => setShowAxes(!showAxes),
1566
+ onOpenSettings: () => setShowPlaylist(!showPlaylist)
1567
+ }
1568
+ )
1569
+ ), showPlaylist && /* @__PURE__ */ React6__default.default.createElement("div", { className: "absolute inset-0 z-20 flex items-end bg-black/80 backdrop-blur-sm" }, /* @__PURE__ */ React6__default.default.createElement("div", { className: "w-full max-h-[60vh] overflow-y-auto bg-gray-900/95 rounded-t-xl" }, /* @__PURE__ */ React6__default.default.createElement("div", { className: "sticky top-0 flex items-center justify-between bg-gray-800 px-4 py-3 border-b border-gray-700" }, /* @__PURE__ */ React6__default.default.createElement("div", null, /* @__PURE__ */ React6__default.default.createElement("h3", { className: "text-white font-semibold" }, playlist.name), /* @__PURE__ */ React6__default.default.createElement("p", { className: "text-xs text-gray-400 mt-0.5" }, "\u5171 ", nodes.length, " \u4E2A\u8282\u70B9")), /* @__PURE__ */ React6__default.default.createElement(
1570
+ "button",
1571
+ {
1572
+ onClick: () => setShowPlaylist(false),
1573
+ className: "p-2 hover:bg-white/10 rounded-lg transition-colors",
1574
+ "aria-label": "\u5173\u95ED\u64AD\u653E\u5217\u8868"
1575
+ },
1576
+ /* @__PURE__ */ React6__default.default.createElement("svg", { className: "w-5 h-5 text-white", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor" }, /* @__PURE__ */ React6__default.default.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }))
1577
+ )), /* @__PURE__ */ React6__default.default.createElement("div", { className: "p-2" }, nodes.map((node, index) => /* @__PURE__ */ React6__default.default.createElement(
1578
+ "button",
1579
+ {
1580
+ key: node.id,
1581
+ onClick: () => {
1582
+ goToNode(index);
1583
+ setShowPlaylist(false);
1628
1584
  },
1629
- "\u2715"
1630
- )),
1631
- /* @__PURE__ */ React2__default.default.createElement("div", { className: "flex-1 overflow-y-auto p-4", style: { scrollbarWidth: "thin", scrollbarColor: "rgba(255,255,255,0.2) transparent" } }, /* @__PURE__ */ React2__default.default.createElement("div", { className: "mb-3 rounded-lg bg-gradient-to-br from-indigo-900/30 to-purple-900/30 p-3 border border-white/10" }, /* @__PURE__ */ React2__default.default.createElement("h4", { className: "text-sm font-semibold text-white mb-2 flex items-center gap-2" }, "\u{1F4CB} \u64AD\u653E\u5217\u8868"), /* @__PURE__ */ React2__default.default.createElement("div", { className: "space-y-1 text-xs" }, /* @__PURE__ */ React2__default.default.createElement("div", { className: "flex justify-between" }, /* @__PURE__ */ React2__default.default.createElement("span", { className: "text-white/60" }, "\u540D\u79F0\uFF1A"), /* @__PURE__ */ React2__default.default.createElement("span", { className: "text-white font-medium" }, playlist.name)), /* @__PURE__ */ React2__default.default.createElement("div", { className: "flex justify-between" }, /* @__PURE__ */ React2__default.default.createElement("span", { className: "text-white/60" }, "\u8282\u70B9\u6570\uFF1A"), /* @__PURE__ */ React2__default.default.createElement("span", { className: "text-white font-medium" }, editableNodes.length)), /* @__PURE__ */ React2__default.default.createElement("div", { className: "flex justify-between" }, /* @__PURE__ */ React2__default.default.createElement("span", { className: "text-white/60" }, "\u5FAA\u73AF\uFF1A"), /* @__PURE__ */ React2__default.default.createElement("span", { className: "text-white font-medium" }, playlist.loop ? "\u662F" : "\u5426")))), /* @__PURE__ */ React2__default.default.createElement("div", { className: "mb-3 rounded-lg bg-gradient-to-br from-blue-900/30 to-cyan-900/30 p-3 border border-white/10" }, /* @__PURE__ */ React2__default.default.createElement("h4", { className: "text-sm font-semibold text-white mb-2 flex items-center gap-2" }, "\u{1F3AF} \u5F53\u524D\u8282\u70B9"), /* @__PURE__ */ React2__default.default.createElement("div", { className: "space-y-1 text-xs" }, /* @__PURE__ */ React2__default.default.createElement("div", { className: "flex justify-between items-center" }, /* @__PURE__ */ React2__default.default.createElement("span", { className: "text-white/60" }, "\u540D\u79F0\uFF1A"), /* @__PURE__ */ React2__default.default.createElement("span", { className: "text-white font-medium truncate ml-2" }, currentNode.name)), /* @__PURE__ */ React2__default.default.createElement("div", { className: "flex justify-between" }, /* @__PURE__ */ React2__default.default.createElement("span", { className: "text-white/60" }, "\u4F4D\u7F6E\uFF1A"), /* @__PURE__ */ React2__default.default.createElement("span", { className: "text-white font-medium" }, currentNodeIndex + 1, " / ", editableNodes.length)), currentNode.resources.audioPath && /* @__PURE__ */ React2__default.default.createElement("div", { className: "text-white/80 mt-1" }, "\u{1F3B5} \u6709\u97F3\u4E50"), currentNode.resources.cameraPath && /* @__PURE__ */ React2__default.default.createElement("div", { className: "text-white/80" }, "\u{1F4F7} \u6709\u76F8\u673A"))), /* @__PURE__ */ React2__default.default.createElement("div", { className: "rounded-lg bg-gradient-to-br from-gray-800/50 to-gray-900/50 border border-white/10 p-3" }, /* @__PURE__ */ React2__default.default.createElement("h4", { className: "mb-2 flex items-center gap-2 text-sm font-semibold text-white" }, "\u{1F4DD} \u8282\u70B9\u7BA1\u7406"), /* @__PURE__ */ React2__default.default.createElement("div", { className: "max-h-64 space-y-2 overflow-y-auto pr-1", style: { scrollbarWidth: "thin", scrollbarColor: "rgba(255,255,255,0.2) transparent" } }, editableNodes.map((node, index) => /* @__PURE__ */ React2__default.default.createElement(
1585
+ className: `w-full flex items-center gap-3 p-3 rounded-lg mb-2 transition-all ${index === currentIndex ? "bg-blue-600 text-white" : "bg-gray-800 text-gray-300 hover:bg-gray-700"}`
1586
+ },
1587
+ /* @__PURE__ */ React6__default.default.createElement(
1632
1588
  "div",
1633
1589
  {
1634
- key: `${node.id}-${index}`,
1635
- className: `rounded-md p-2 transition-all text-xs ${currentNodeIndex === index ? "bg-gradient-to-r from-purple-600/50 to-blue-600/50 border border-purple-400/50" : "bg-white/5 hover:bg-white/10 border border-white/10"}`
1590
+ className: `flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-sm font-semibold ${index === currentIndex ? "bg-white/20" : "bg-gray-700"}`
1636
1591
  },
1637
- /* @__PURE__ */ React2__default.default.createElement("div", { className: "flex items-start justify-between gap-2" }, /* @__PURE__ */ React2__default.default.createElement("div", { className: "flex-1 min-w-0" }, /* @__PURE__ */ React2__default.default.createElement("div", { className: "flex items-center gap-1 mb-1" }, /* @__PURE__ */ React2__default.default.createElement("span", { className: "text-xs font-bold text-white/40" }, "#", index + 1), /* @__PURE__ */ React2__default.default.createElement("h5", { className: "font-semibold text-white text-xs truncate" }, node.name), currentNodeIndex === index && /* @__PURE__ */ React2__default.default.createElement("span", { className: "rounded bg-green-500/30 px-1 py-0.5 text-[10px] text-green-300 flex-shrink-0" }, "\u25B6\uFE0F")), /* @__PURE__ */ React2__default.default.createElement("div", { className: "flex flex-wrap gap-1 text-[10px] text-white/60" }, node.resources.modelPath && /* @__PURE__ */ React2__default.default.createElement("span", null, "\u{1F464}"), node.resources.motionPath && /* @__PURE__ */ React2__default.default.createElement("span", null, "\u{1F483}"), node.resources.audioPath && /* @__PURE__ */ React2__default.default.createElement("span", null, "\u{1F3B5}"), node.resources.cameraPath && /* @__PURE__ */ React2__default.default.createElement("span", null, "\u{1F4F7}"))), /* @__PURE__ */ React2__default.default.createElement("div", { className: "flex flex-col gap-0.5 flex-shrink-0" }, index > 0 && /* @__PURE__ */ React2__default.default.createElement(
1638
- "button",
1639
- {
1640
- onClick: () => handleMoveNodeUp(index),
1641
- className: "p-0.5 rounded bg-white/10 hover:bg-white/20 text-white text-[10px] transition-colors",
1642
- title: "\u4E0A\u79FB"
1643
- },
1644
- "\u2B06\uFE0F"
1645
- ), index < editableNodes.length - 1 && /* @__PURE__ */ React2__default.default.createElement(
1646
- "button",
1647
- {
1648
- onClick: () => handleMoveNodeDown(index),
1649
- className: "p-0.5 rounded bg-white/10 hover:bg-white/20 text-white text-[10px] transition-colors",
1650
- title: "\u4E0B\u79FB"
1651
- },
1652
- "\u2B07\uFE0F"
1653
- ), /* @__PURE__ */ React2__default.default.createElement(
1654
- "button",
1655
- {
1656
- onClick: () => playlistJumpTo(index),
1657
- className: "p-0.5 rounded bg-blue-500/30 hover:bg-blue-500/50 text-white text-[10px] transition-colors",
1658
- title: "\u8DF3\u8F6C"
1659
- },
1660
- "\u25B6\uFE0F"
1661
- ), /* @__PURE__ */ React2__default.default.createElement(
1662
- "button",
1663
- {
1664
- onClick: () => {
1665
- if (confirm(`\u786E\u5B9A\u5220\u9664 "${node.name}"\uFF1F`)) {
1666
- handleDeleteNode(index);
1667
- }
1668
- },
1669
- className: "p-0.5 rounded bg-red-500/30 hover:bg-red-500/50 text-white text-[10px] transition-colors",
1670
- title: "\u5220\u9664"
1671
- },
1672
- "\u{1F5D1}\uFE0F"
1673
- )))
1674
- )))))
1675
- )
1676
- ));
1677
- };
1678
-
1679
- // src/mmd/presets.ts
1680
- var defaultMMDPreset = {
1681
- id: "default",
1682
- name: "\u9ED8\u8BA4\u6A21\u578B",
1683
- summary: "\u4EC5\u5C55\u793A\u6A21\u578B\uFF0C\u65E0\u52A8\u4F5C\u548C\u97F3\u9891",
1684
- badges: ["\u6A21\u578B", "\u9759\u6001"],
1685
- resources: {
1686
- modelPath: "/mikutalking/models/YYB_Z6SakuraMiku/miku.pmx"
1687
- },
1688
- stage: {
1689
- backgroundColor: "#000000",
1690
- cameraPosition: { x: 0, y: 10, z: 30 },
1691
- cameraTarget: { x: 0, y: 10, z: 0 },
1692
- enablePhysics: true,
1693
- showGrid: true,
1694
- ammoPath: "/mikutalking/libs/ammo.wasm.js",
1695
- ammoWasmPath: "/mikutalking/libs/"
1696
- }
1697
- };
1698
- var catchTheWavePreset = {
1699
- id: "catch-the-wave",
1700
- name: "Catch The Wave",
1701
- summary: "\u5B8C\u6574\u7684MMD\u8868\u6F14\uFF1A\u6A21\u578B\u3001\u52A8\u4F5C\u3001\u76F8\u673A\u8FD0\u955C\u3001\u97F3\u9891\u540C\u6B65",
1702
- badges: ["\u6A21\u578B", "\u52A8\u4F5C", "\u76F8\u673A", "\u97F3\u9891"],
1703
- resources: {
1704
- modelPath: "/mikutalking/models/YYB_Z6SakuraMiku/miku.pmx",
1705
- motionPath: "/mikutalking/actions/CatchTheWave/mmd_CatchTheWave_motion.vmd",
1706
- cameraPath: "/mikutalking/actions/CatchTheWave/camera.vmd",
1707
- audioPath: "/mikutalking/actions/CatchTheWave/pv_268.wav"
1708
- },
1709
- stage: {
1710
- backgroundColor: "#01030b",
1711
- cameraPosition: { x: 0, y: 10, z: 30 },
1712
- cameraTarget: { x: 0, y: 10, z: 0 },
1713
- enablePhysics: true,
1714
- showGrid: false,
1715
- ammoPath: "/mikutalking/libs/ammo.wasm.js",
1716
- ammoWasmPath: "/mikutalking/libs/"
1717
- }
1718
- };
1719
- var simpleModelPreset = {
1720
- id: "simple-model",
1721
- name: "\u7B80\u5355\u6A21\u578B",
1722
- summary: "\u8F7B\u91CF\u7EA7\u6D4B\u8BD5\u6A21\u578B",
1723
- badges: ["\u6A21\u578B", "\u8F7B\u91CF"],
1724
- resources: {
1725
- modelPath: "/mikutalking/models/test/v4c5.0.pmx"
1726
- },
1727
- stage: {
1728
- backgroundColor: "#ffffff",
1729
- cameraPosition: { x: 0, y: 10, z: 30 },
1730
- cameraTarget: { x: 0, y: 10, z: 0 },
1731
- enablePhysics: true,
1732
- showGrid: true,
1733
- ammoPath: "/mikutalking/libs/ammo.wasm.js",
1734
- ammoWasmPath: "/mikutalking/libs/"
1735
- }
1592
+ index + 1
1593
+ ),
1594
+ /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex-1 text-left" }, /* @__PURE__ */ React6__default.default.createElement("div", { className: "font-medium" }, node.name), node.duration && /* @__PURE__ */ React6__default.default.createElement("div", { className: "text-xs opacity-75 mt-0.5" }, Math.floor(node.duration / 60), ":", String(Math.floor(node.duration % 60)).padStart(2, "0"))),
1595
+ index === currentIndex && /* @__PURE__ */ React6__default.default.createElement("div", { className: "flex-shrink-0" }, /* @__PURE__ */ React6__default.default.createElement("svg", { className: "w-5 h-5", fill: "currentColor", viewBox: "0 0 24 24" }, /* @__PURE__ */ React6__default.default.createElement("path", { d: "M8 5v14l11-7z" })))
1596
+ )))))),
1597
+ showDebugInfo && /* @__PURE__ */ React6__default.default.createElement("div", { className: "w-96 flex-shrink-0 bg-gray-900/95 border-l border-gray-700 p-4 overflow-y-auto h-full" }, /* @__PURE__ */ React6__default.default.createElement(
1598
+ MMDPlaylistDebugInfo,
1599
+ {
1600
+ playlistName: playlist.name,
1601
+ currentIndex,
1602
+ currentNode,
1603
+ totalNodes: nodes.length,
1604
+ isPlaying,
1605
+ isListLooping,
1606
+ isNodeLooping: isLooping,
1607
+ preloadStrategy: preload,
1608
+ isLoading: isLoading || isTransitioning,
1609
+ isFullscreen,
1610
+ showAxes,
1611
+ preloadedNodes: Array.from(preloadedRef.current)
1612
+ }
1613
+ ))
1614
+ );
1736
1615
  };
1737
- var availableMMDPresets = [
1738
- catchTheWavePreset,
1739
- defaultMMDPreset,
1740
- simpleModelPreset
1741
- ];
1742
1616
 
1743
1617
  exports.MMDPlayerBase = MMDPlayerBase;
1744
1618
  exports.MMDPlayerEnhanced = MMDPlayerEnhanced;
1619
+ exports.MMDPlayerEnhancedDebugInfo = MMDPlayerEnhancedDebugInfo;
1745
1620
  exports.MMDPlaylist = MMDPlaylist;
1746
- exports.availableMMDPresets = availableMMDPresets;
1747
- exports.catchTheWavePreset = catchTheWavePreset;
1748
- exports.defaultMMDPreset = defaultMMDPreset;
1621
+ exports.MMDPlaylistDebugInfo = MMDPlaylistDebugInfo;
1749
1622
  exports.loadAmmo = loadAmmo;
1750
- exports.simpleModelPreset = simpleModelPreset;
1751
1623
  //# sourceMappingURL=index.js.map
1752
1624
  //# sourceMappingURL=index.js.map