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