sa2kit 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,76 +1,44 @@
1
1
  import '../chunk-BJTO5JO5.mjs';
2
- import React2, { forwardRef, useState, useMemo, useRef, useImperativeHandle, useEffect } from 'react';
3
- import * as THREE2 from 'three';
4
- import { OrbitControls, MMDLoader, MMDAnimationHelper, OutlineEffect } 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,1643 +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
- }) => {
93
- const containerRef = useRef(null);
94
- const [loading, setLoading] = useState(true);
95
- const [error, setError] = useState(null);
96
- useEffect(() => {
97
- 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
- const init = async () => {
106
- 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
- });
114
- }
115
- 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);
130
- container.appendChild(renderer.domElement);
131
- effect = new OutlineEffect(renderer);
132
- helper = new MMDAnimationHelper({
133
- afterglow: 2
134
- });
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);
173
- }
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);
189
- }
190
- );
191
- }
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);
202
- }
203
- );
204
- clock = new THREE2.Clock();
205
- const animate = () => {
206
- animationId = requestAnimationFrame(animate);
207
- helper.update(clock.getDelta());
208
- effect.render(scene, camera);
209
- };
210
- 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);
238
- }
239
- }
240
- };
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 = forwardRef(({
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
- }, ref) => {
294
- console.log("\u{1F3A8} [MMDPlayerEnhanced] \u7EC4\u4EF6\u521D\u59CB\u5316");
295
- const [selectedResourceId, setSelectedResourceId] = useState(
296
- defaultResourceId || resourcesList?.[0]?.id || ""
297
- );
298
- const [selectedModelId, setSelectedModelId] = useState(
299
- defaultSelection?.modelId || resourceOptions?.models?.[0]?.id || ""
300
- );
301
- const [selectedMotionId, setSelectedMotionId] = useState(
302
- defaultSelection?.motionId || ""
303
- );
304
- const [selectedAudioId, setSelectedAudioId] = useState(
305
- defaultSelection?.audioId || ""
306
- );
307
- const [selectedCameraId, setSelectedCameraId] = useState(
308
- defaultSelection?.cameraId || ""
309
- );
310
- const [selectedStageModelId, setSelectedStageModelId] = useState(
311
- defaultSelection?.stageModelId || ""
312
- );
313
- const [selectedBackgroundId, setSelectedBackgroundId] = useState(
314
- defaultSelection?.backgroundId || ""
315
- );
316
- const [showSettings, setShowSettings] = useState(false);
317
- const [expandedSection, setExpandedSection] = useState(null);
318
- const currentResources = useMemo(() => {
319
- if (resourceOptions) {
320
- const model = resourceOptions.models?.find((m) => m.id === selectedModelId);
321
- const motion = resourceOptions.motions?.find((m) => m.id === selectedMotionId);
322
- const audio = resourceOptions.audios?.find((a) => a.id === selectedAudioId);
323
- const camera = resourceOptions.cameras?.find((c) => c.id === selectedCameraId);
324
- const stageModel = resourceOptions.stageModels?.find((s) => s.id === selectedStageModelId);
325
- const background = resourceOptions.backgrounds?.find((b) => b.id === selectedBackgroundId);
326
- return {
327
- modelPath: model?.path || resourceOptions.models?.[0]?.path || "",
328
- motionPath: motion?.path,
329
- audioPath: audio?.path,
330
- cameraPath: camera?.path,
331
- stageModelPath: stageModel?.path,
332
- backgroundPath: background?.path
333
- };
334
- }
335
- if (resourcesList && resourcesList.length > 0) {
336
- const selected = resourcesList.find((r) => r.id === selectedResourceId);
337
- const resourceItem = selected || resourcesList[0];
338
- if (!resourceItem) {
339
- throw new Error("\u65E0\u6CD5\u627E\u5230\u6709\u6548\u7684\u8D44\u6E90\u914D\u7F6E");
340
- }
341
- return resourceItem.resources;
342
- }
343
- if (!resources) {
344
- throw new Error("\u5FC5\u987B\u63D0\u4F9B resources\u3001resourcesList \u6216 resourceOptions");
345
- }
346
- return resources;
347
- }, [
49
+ var MMDPlayerBase = forwardRef((props, ref) => {
50
+ const {
348
51
  resources,
349
- resourcesList,
350
- selectedResourceId,
351
- resourceOptions,
352
- selectedModelId,
353
- selectedMotionId,
354
- selectedAudioId,
355
- selectedCameraId,
356
- selectedStageModelId,
357
- selectedBackgroundId
358
- ]);
359
- console.log("\u{1F4C2} [MMDPlayerEnhanced] \u5F53\u524D\u8D44\u6E90\u914D\u7F6E:", currentResources);
360
- console.log("\u{1F3AD} [MMDPlayerEnhanced] \u821E\u53F0\u914D\u7F6E:", stage);
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;
361
69
  const containerRef = useRef(null);
362
- const rendererRef = useRef(null);
363
70
  const sceneRef = useRef(null);
364
71
  const cameraRef = useRef(null);
72
+ const rendererRef = useRef(null);
365
73
  const controlsRef = useRef(null);
366
74
  const helperRef = useRef(null);
367
- const clockRef = useRef(new THREE2.Clock());
368
- const audioRef = useRef(null);
75
+ const axesHelperRef = useRef(null);
76
+ const clockRef = useRef(new THREE.Clock());
369
77
  const animationIdRef = useRef(null);
78
+ const resizeObserverRef = useRef(null);
79
+ const isReadyRef = useRef(false);
370
80
  const isPlayingRef = useRef(false);
371
- const isLoadedRef = useRef(false);
372
- const shouldAutoPlayAfterReloadRef = useRef(false);
373
- const vmdDataRef = useRef(null);
374
- const animationDurationRef = useRef(0);
375
- const hasAudioRef = useRef(false);
376
- const animationEndedFiredRef = useRef(false);
377
- const lastAnimationTimeRef = useRef(0);
378
- const animationStoppedCountRef = useRef(0);
379
- const [loading, setLoading] = useState(false);
380
- const [loadingProgress, setLoadingProgress] = useState(0);
381
- const [error, setError] = useState(null);
382
- const [isPlaying, setIsPlaying] = useState(false);
383
- const [isInitialized, setIsInitialized] = useState(false);
384
- const [reloadTrigger, setReloadTrigger] = useState(0);
385
- const [needReset, setNeedReset] = useState(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);
386
95
  useImperativeHandle(ref, () => ({
387
- clearResources: () => {
388
- console.log("\u{1F9F9} [MMDPlayerEnhanced] \u5916\u90E8\u89E6\u53D1\u8D44\u6E90\u6E05\u7406");
389
- clearOldResources();
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;
390
123
  },
391
- getIsPlaying: () => isPlayingRef.current,
392
- getIsLoaded: () => isLoadedRef.current,
393
- stopCompletely: () => {
394
- console.log("\u23F9\uFE0F [MMDPlayerEnhanced] \u5B8C\u5168\u505C\u6B62");
395
- stopCompletely();
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");
396
132
  }
397
133
  }));
398
- const stopCompletely = () => {
399
- isPlayingRef.current = false;
400
- setIsPlaying(false);
401
- if (audioRef.current) {
402
- audioRef.current.pause();
403
- audioRef.current.currentTime = 0;
404
- }
405
- if (helperRef.current) {
406
- helperRef.current.enable("physics", false);
407
- }
408
- animationEndedFiredRef.current = false;
409
- lastAnimationTimeRef.current = 0;
410
- animationStoppedCountRef.current = 0;
411
- console.log("\u2705 [MMDPlayerEnhanced] \u5B8C\u5168\u505C\u6B62\u5B8C\u6210");
412
- };
413
134
  useEffect(() => {
414
- const container = containerRef.current;
415
- if (!container) return;
416
- const handleCleanupResources = () => {
417
- console.log("\u{1F9F9} [MMDPlayerEnhanced] \u6536\u5230\u6E05\u7406\u8D44\u6E90\u4E8B\u4EF6");
418
- if (!isPlayingRef.current) {
419
- clearOldResources();
135
+ if (!containerRef.current) return;
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());
420
155
  } else {
421
- console.warn("\u26A0\uFE0F [MMDPlayerEnhanced] \u64AD\u653E\u4E2D\uFF0C\u8DF3\u8FC7\u8D44\u6E90\u6E05\u7406");
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)`);
422
161
  }
423
- };
424
- const handleStopCompletely = () => {
425
- console.log("\u23F9\uFE0F [MMDPlayerEnhanced] \u6536\u5230\u5B8C\u5168\u505C\u6B62\u4E8B\u4EF6");
426
- stopCompletely();
427
- };
428
- container.addEventListener("cleanupResources", handleCleanupResources);
429
- container.addEventListener("stopCompletely", handleStopCompletely);
430
- return () => {
431
- container.removeEventListener("cleanupResources", handleCleanupResources);
432
- container.removeEventListener("stopCompletely", handleStopCompletely);
433
- };
434
- }, []);
435
- useEffect(() => {
436
- console.log("\u{1F3D7}\uFE0F [MMDPlayerEnhanced] \u573A\u666F\u521D\u59CB\u5316 useEffect \u89E6\u53D1");
437
- if (!containerRef.current) {
438
- console.warn("\u26A0\uFE0F [MMDPlayerEnhanced] containerRef.current \u4E0D\u5B58\u5728");
439
- return;
440
- }
441
- console.log("\u2705 [MMDPlayerEnhanced] \u5BB9\u5668\u5143\u7D20\u5B58\u5728\uFF0C\u5F00\u59CB\u521D\u59CB\u5316\u573A\u666F");
442
- const container = containerRef.current;
443
- if (container.children.length > 0) {
444
- console.log("\u26A0\uFE0F [MMDPlayerEnhanced] \u573A\u666F\u5DF2\u7ECF\u521D\u59CB\u5316\uFF0C\u8DF3\u8FC7");
445
- return;
446
- }
447
- const width = container.clientWidth;
448
- const height = container.clientHeight;
449
- const scene = new THREE2.Scene();
450
- scene.background = new THREE2.Color(stage?.backgroundColor || "#000000");
451
- sceneRef.current = scene;
452
- const camera = new THREE2.PerspectiveCamera(45, width / height, 1, 2e3);
453
- const camPos = stage?.cameraPosition || { x: 0, y: 10, z: 30 };
454
- camera.position.set(camPos.x, camPos.y, camPos.z);
455
- cameraRef.current = camera;
456
- const renderer = new THREE2.WebGLRenderer({ antialias: true });
457
- renderer.setSize(width, height);
458
- renderer.setPixelRatio(window.devicePixelRatio);
459
- container.appendChild(renderer.domElement);
460
- rendererRef.current = renderer;
461
- const ambient = new THREE2.AmbientLight(16777215, 0.6);
462
- scene.add(ambient);
463
- const directionalLight = new THREE2.DirectionalLight(16777215, 0.8);
464
- directionalLight.position.set(1, 1, 1);
465
- scene.add(directionalLight);
466
- if (stage?.showGrid !== false) {
467
- const gridHelper = new THREE2.PolarGridHelper(30, 10);
468
- scene.add(gridHelper);
469
- }
470
- const controls = new OrbitControls(camera, renderer.domElement);
471
- const target = stage?.cameraTarget || { x: 0, y: 10, z: 0 };
472
- controls.target.set(target.x, target.y, target.z);
473
- controls.update();
474
- controlsRef.current = controls;
475
- const handleResize = () => {
476
- if (!container || !camera || !renderer) return;
477
- const newWidth = container.clientWidth;
478
- const newHeight = container.clientHeight;
479
- camera.aspect = newWidth / newHeight;
480
- camera.updateProjectionMatrix();
481
- renderer.setSize(newWidth, newHeight);
482
- };
483
- window.addEventListener("resize", handleResize);
484
- const animate = () => {
485
- animationIdRef.current = requestAnimationFrame(animate);
486
- if (helperRef.current && isPlayingRef.current) {
487
- const delta = clockRef.current.getDelta();
488
- try {
489
- helperRef.current.update(delta);
490
- } catch (error2) {
491
- if (error2.message && error2.message.includes("OOM")) {
492
- console.error("\u274C \u7269\u7406\u5F15\u64CE\u5185\u5B58\u6EA2\u51FA\uFF0C\u505C\u6B62\u64AD\u653E");
493
- isPlayingRef.current = false;
494
- setIsPlaying(false);
495
- onError?.(new Error("\u7269\u7406\u5F15\u64CE\u5185\u5B58\u6EA2\u51FA"));
496
- return;
162
+ try {
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");
497
208
  }
498
- throw error2;
209
+ } else {
210
+ console.log("[MMDPlayerBase] Physics disabled");
499
211
  }
500
- if (!hasAudioRef.current && !loop && !animationEndedFiredRef.current) {
501
- const currentTime = clockRef.current.getElapsedTime();
502
- if (animationDurationRef.current > 0) {
503
- if (currentTime >= animationDurationRef.current - 0.1) {
504
- console.log("\u{1F3AC} [MMDPlayerEnhanced] \u52A8\u753B\u64AD\u653E\u7ED3\u675F\uFF08\u65F6\u957F\u5224\u5B9A\uFF09");
505
- animationEndedFiredRef.current = true;
506
- isPlayingRef.current = false;
507
- setIsPlaying(false);
508
- onAnimationEnded?.();
509
- }
212
+ const container = containerRef.current;
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
+ }
248
+ container.appendChild(renderer.domElement);
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({
293
+ afterglow: 2
294
+ });
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");
309
+ }
310
+ },
311
+ (err) => reject(err)
312
+ );
510
313
  } else {
511
- if (Math.abs(currentTime - lastAnimationTimeRef.current) < 1e-3) {
512
- animationStoppedCountRef.current++;
513
- if (animationStoppedCountRef.current > 30) {
514
- console.log("\u{1F3AC} [MMDPlayerEnhanced] \u52A8\u753B\u64AD\u653E\u7ED3\u675F\uFF08\u505C\u6B62\u68C0\u6D4B\uFF09");
515
- animationEndedFiredRef.current = true;
516
- isPlayingRef.current = false;
517
- setIsPlaying(false);
518
- onAnimationEnded?.();
519
- }
520
- } else {
521
- animationStoppedCountRef.current = 0;
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");
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 方向)
355
+ );
356
+ console.log("[MMDPlayerBase] Auto camera position:", camera.position);
522
357
  }
523
- lastAnimationTimeRef.current = currentTime;
358
+ controls.update();
524
359
  }
525
360
  }
526
- }
527
- if (controlsRef.current) {
528
- controlsRef.current.update();
529
- }
530
- if (renderer && scene && camera) {
531
- renderer.render(scene, camera);
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
+ }
429
+ animate();
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`);
455
+ }
456
+ onError?.(error instanceof Error ? error : new Error(String(error)));
532
457
  }
533
458
  };
534
- animate();
535
- setIsInitialized(true);
536
- console.log("\u2705 [MMDPlayerEnhanced] \u573A\u666F\u521D\u59CB\u5316\u5B8C\u6210");
459
+ init();
537
460
  return () => {
538
- window.removeEventListener("resize", handleResize);
461
+ console.log("[MMDPlayerBase] Cleanup started");
462
+ initIdRef.current++;
539
463
  if (animationIdRef.current) {
540
464
  cancelAnimationFrame(animationIdRef.current);
465
+ animationIdRef.current = null;
541
466
  }
542
- if (renderer) {
543
- renderer.dispose();
544
- if (renderer.domElement && renderer.domElement.parentNode) {
545
- container.removeChild(renderer.domElement);
546
- }
467
+ isPlayingRef.current = false;
468
+ isReadyRef.current = false;
469
+ if (resizeObserverRef.current) {
470
+ resizeObserverRef.current.disconnect();
471
+ resizeObserverRef.current = null;
547
472
  }
548
- if (controls) {
549
- controls.dispose();
473
+ if (audioRef.current) {
474
+ try {
475
+ if (audioRef.current.isPlaying) {
476
+ audioRef.current.stop();
477
+ }
478
+ if (audioRef.current.source) {
479
+ audioRef.current.disconnect();
480
+ }
481
+ if (audioRef.current.buffer) {
482
+ audioRef.current.buffer = null;
483
+ }
484
+ audioRef.current = null;
485
+ } catch (e) {
486
+ console.warn("[MMDPlayerBase] Error cleaning up audio:", e);
487
+ }
550
488
  }
551
- };
552
- }, [stage]);
553
- const clearOldResources = () => {
554
- if (!sceneRef.current) return;
555
- if (isPlayingRef.current) {
556
- isPlayingRef.current = false;
557
- setIsPlaying(false);
558
- }
559
- if (audioRef.current) {
560
- audioRef.current.pause();
561
- audioRef.current.currentTime = 0;
562
- audioRef.current.onended = null;
563
- audioRef.current.src = "";
564
- audioRef.current.load();
565
- audioRef.current = null;
566
- }
567
- if (helperRef.current) {
568
- try {
569
- helperRef.current.enable("animation", false);
570
- helperRef.current.enable("ik", false);
571
- helperRef.current.enable("grant", false);
572
- helperRef.current.enable("physics", false);
573
- const helperObjects = helperRef.current.objects;
574
- if (helperObjects && Array.isArray(helperObjects)) {
575
- const physicsWorldsToDestroy = /* @__PURE__ */ new Set();
576
- helperObjects.forEach((obj) => {
577
- if (obj.physics) {
578
- try {
579
- const physics = obj.physics;
580
- if (physics.world) {
581
- 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");
582
515
  }
583
- if (physics.bodies && Array.isArray(physics.bodies)) {
584
- physics.bodies.forEach((body) => {
585
- if (physics.world && body) {
586
- try {
587
- physics.world.removeRigidBody(body);
588
- if (window.Ammo && body.destroy) {
589
- 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
+ }
590
547
  }
591
- } catch (e) {
548
+ physics.bodies.length = 0;
549
+ console.log("[MMDPlayerBase] \u2705 All rigid bodies removed");
592
550
  }
593
- }
594
- });
595
- physics.bodies.length = 0;
596
- physics.bodies = null;
597
- }
598
- if (physics.constraints && Array.isArray(physics.constraints)) {
599
- physics.constraints.forEach((constraint) => {
600
- if (physics.world && constraint) {
601
- try {
602
- physics.world.removeConstraint(constraint);
603
- if (window.Ammo && constraint.destroy) {
604
- 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
+ }
605
562
  }
606
- } catch (e) {
563
+ physics.constraints.length = 0;
564
+ console.log("[MMDPlayerBase] \u2705 All constraints removed");
607
565
  }
608
566
  }
609
- });
610
- physics.constraints.length = 0;
611
- physics.constraints = null;
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);
612
573
  }
613
- if (physics.reset) physics.reset();
614
- physics.world = null;
615
- obj.physics = null;
616
- } catch (e) {
617
- console.warn("\u6E05\u7406\u7269\u7406\u7CFB\u7EDF\u5931\u8D25:", e);
574
+ } else {
575
+ console.log("[MMDPlayerBase] \u26A0\uFE0F No physics object found for mesh", idx);
618
576
  }
619
- }
620
- });
621
- physicsWorldsToDestroy.forEach((world) => {
622
- try {
623
- while (world.getNumCollisionObjects() > 0) {
624
- const obj = world.getCollisionObjectArray().at(0);
625
- world.removeCollisionObject(obj);
626
- if (obj && obj.destroy) {
627
- obj.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;
587
+ }
588
+ if (meshData?.audio) {
589
+ if (meshData.audio.isPlaying) {
590
+ meshData.audio.stop();
591
+ }
592
+ if (meshData.audio.source) {
593
+ meshData.audio.disconnect();
628
594
  }
595
+ if (meshData.audio.buffer) {
596
+ meshData.audio.buffer = null;
597
+ }
598
+ meshData.audio = null;
629
599
  }
630
- if (world.destroy) {
631
- world.destroy();
600
+ });
601
+ meshes.length = 0;
602
+ }
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);
621
+ }
632
622
  }
633
- } catch (e) {
634
- console.warn("\u9500\u6BC1\u7269\u7406\u4E16\u754C\u5931\u8D25:", e);
623
+ components.worlds.length = 0;
624
+ console.log("[MMDPlayerBase] \u2705 All btDiscreteDynamicsWorld destroyed");
635
625
  }
636
- });
637
- helperObjects.length = 0;
638
- }
639
- } catch (error2) {
640
- console.warn("\u6E05\u7406 helper \u5931\u8D25:", error2);
641
- }
642
- helperRef.current = null;
643
- }
644
- if (sceneRef.current.background && sceneRef.current.background.isTexture) {
645
- sceneRef.current.background.dispose();
646
- sceneRef.current.background = null;
647
- }
648
- if (sceneRef.current.environment && sceneRef.current.environment.isTexture) {
649
- sceneRef.current.environment.dispose();
650
- sceneRef.current.environment = null;
651
- }
652
- const objectsToRemove = [];
653
- sceneRef.current.traverse((child) => {
654
- if (child.type === "SkinnedMesh" || child.isSkinnedMesh) {
655
- objectsToRemove.push(child);
656
- }
657
- if (child.type === "Mesh" && child !== sceneRef.current) {
658
- objectsToRemove.push(child);
659
- }
660
- });
661
- objectsToRemove.forEach((obj) => {
662
- if (obj.parent) obj.parent.remove(obj);
663
- if (obj.geometry) {
664
- obj.geometry.dispose();
665
- }
666
- if (obj.material) {
667
- const disposeMaterial = (m) => {
668
- [
669
- "map",
670
- "emissiveMap",
671
- "normalMap",
672
- "bumpMap",
673
- "specularMap",
674
- "envMap",
675
- "lightMap",
676
- "aoMap",
677
- "alphaMap"
678
- ].forEach((prop) => {
679
- if (m[prop]) m[prop].dispose();
680
- });
681
- m.dispose();
682
- };
683
- const material = obj.material;
684
- if (Array.isArray(material)) {
685
- material.forEach(disposeMaterial);
686
- } else {
687
- disposeMaterial(material);
688
- }
689
- }
690
- if (obj.skeleton) {
691
- obj.skeleton = null;
692
- }
693
- });
694
- clockRef.current = new THREE2.Clock();
695
- vmdDataRef.current = null;
696
- setNeedReset(false);
697
- if (window.gc) {
698
- try {
699
- window.gc();
700
- } catch (e) {
701
- }
702
- }
703
- console.log(`\u2705 \u8D44\u6E90\u6E05\u7406\u5B8C\u6210 (${objectsToRemove.length} \u4E2A\u5BF9\u8C61)`);
704
- };
705
- useEffect(() => {
706
- if (!sceneRef.current || !cameraRef.current) return;
707
- if (isLoadedRef.current) return;
708
- clearOldResources();
709
- isLoadedRef.current = true;
710
- const loadMMD = async () => {
711
- try {
712
- setLoading(true);
713
- setLoadingProgress(0);
714
- animationDurationRef.current = 0;
715
- hasAudioRef.current = false;
716
- animationEndedFiredRef.current = false;
717
- lastAnimationTimeRef.current = 0;
718
- animationStoppedCountRef.current = 0;
719
- if (stage?.enablePhysics !== false) {
720
- setLoadingProgress(5);
721
- await loadAmmo({
722
- scriptPath: stage?.ammoPath || "/mikutalking/libs/ammo.wasm.js",
723
- wasmBasePath: stage?.ammoWasmPath || "/mikutalking/libs/"
724
- });
725
- }
726
- const manager = new THREE2.LoadingManager();
727
- const basePath = currentResources.modelPath.substring(0, currentResources.modelPath.lastIndexOf("/") + 1);
728
- manager.setURLModifier((url) => {
729
- if (url.startsWith("http://") || url.startsWith("https://")) return url;
730
- if (url.startsWith("/")) return url;
731
- return basePath + url;
732
- });
733
- const loader = new MMDLoader(manager);
734
- const helper = new MMDAnimationHelper();
735
- helperRef.current = helper;
736
- setLoadingProgress(20);
737
- const modelStartTime = performance.now();
738
- const mesh = await new Promise((resolve, reject) => {
739
- loader.load(
740
- currentResources.modelPath,
741
- (object) => {
742
- const loadTime = ((performance.now() - modelStartTime) / 1e3).toFixed(2);
743
- console.log(`\u2705 \u6A21\u578B\u52A0\u8F7D\u5B8C\u6210 (${loadTime}s)`);
744
- resolve(object);
745
- },
746
- (progress) => {
747
- if (progress.total > 0) {
748
- setLoadingProgress(Math.min(progress.loaded / progress.total * 30 + 20, 50));
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
+ }
749
634
  }
750
- },
751
- (error2) => {
752
- console.error("\u274C \u6A21\u578B\u52A0\u8F7D\u5931\u8D25:", error2);
753
- reject(error2);
635
+ components.solvers.length = 0;
636
+ console.log("[MMDPlayerBase] \u2705 All btSequentialImpulseConstraintSolver destroyed");
754
637
  }
755
- );
756
- });
757
- if (!sceneRef.current) {
758
- throw new Error("\u573A\u666F\u672A\u521D\u59CB\u5316");
759
- }
760
- sceneRef.current.add(mesh);
761
- if (currentResources.stageModelPath) {
762
- const stageMesh = await new Promise((resolve, reject) => {
763
- loader.load(currentResources.stageModelPath, resolve, void 0, reject);
764
- });
765
- sceneRef.current.add(stageMesh);
766
- }
767
- if (currentResources.backgroundPath && sceneRef.current) {
768
- const textureLoader = new THREE2.TextureLoader();
769
- const backgroundTexture = await new Promise((resolve, reject) => {
770
- textureLoader.load(currentResources.backgroundPath, resolve, void 0, reject);
771
- });
772
- backgroundTexture.colorSpace = THREE2.SRGBColorSpace;
773
- if (stage?.backgroundType === "skybox") {
774
- backgroundTexture.mapping = THREE2.EquirectangularReflectionMapping;
775
- sceneRef.current.background = backgroundTexture;
776
- sceneRef.current.environment = backgroundTexture;
777
- } else {
778
- sceneRef.current.background = backgroundTexture;
779
- }
780
- }
781
- let vmd = null;
782
- let cameraVmd = null;
783
- if (currentResources.motionPath) {
784
- setLoadingProgress(60);
785
- vmd = await new Promise((resolve, reject) => {
786
- loader.loadAnimation(
787
- currentResources.motionPath,
788
- mesh,
789
- resolve,
790
- (progress) => {
791
- if (progress.total > 0) {
792
- setLoadingProgress(Math.min(progress.loaded / progress.total * 20 + 60, 80));
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);
793
645
  }
794
- },
795
- reject
796
- );
797
- });
798
- helper.add(mesh, {
799
- animation: vmd,
800
- physics: stage?.enablePhysics !== false
801
- });
802
- if (vmd) {
803
- let maxDuration = 0;
804
- if (vmd.duration !== void 0) {
805
- maxDuration = vmd.duration;
806
- } else if (Array.isArray(vmd) && vmd.length > 0 && vmd[0].duration !== void 0) {
807
- maxDuration = vmd[0].duration;
808
- } else if (vmd.clip && vmd.clip.duration !== void 0) {
809
- maxDuration = vmd.clip.duration;
646
+ }
647
+ components.caches.length = 0;
648
+ console.log("[MMDPlayerBase] \u2705 All btDbvtBroadphase destroyed");
810
649
  }
811
- if (maxDuration > 0) {
812
- animationDurationRef.current = maxDuration;
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--) {
653
+ try {
654
+ Ammo.destroy(components.dispatchers[i]);
655
+ } catch (e) {
656
+ console.error(`[MMDPlayerBase] \u274C Error destroying dispatcher #${i}:`, e);
657
+ }
658
+ }
659
+ components.dispatchers.length = 0;
660
+ console.log("[MMDPlayerBase] \u2705 All btCollisionDispatcher destroyed");
813
661
  }
814
- }
815
- } else {
816
- helper.add(mesh, { physics: stage?.enablePhysics !== false });
817
- }
818
- if (currentResources.cameraPath && cameraRef.current) {
819
- setLoadingProgress(80);
820
- cameraVmd = await new Promise((resolve, reject) => {
821
- loader.loadAnimation(currentResources.cameraPath, cameraRef.current, resolve, void 0, reject);
822
- });
823
- helper.add(cameraRef.current, { animation: cameraVmd });
824
- }
825
- if (currentResources.audioPath) {
826
- setLoadingProgress(90);
827
- const audio = new Audio(currentResources.audioPath);
828
- audio.volume = 0.5;
829
- audio.loop = loop;
830
- audioRef.current = audio;
831
- hasAudioRef.current = true;
832
- audio.onended = () => {
833
- if (!loop) {
834
- setIsPlaying(false);
835
- if (helperRef.current && sceneRef.current) {
836
- const mesh2 = sceneRef.current.children.find(
837
- (child) => child.type === "SkinnedMesh"
838
- );
839
- if (mesh2) {
840
- helperRef.current.pose(mesh2, {});
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);
841
669
  }
842
670
  }
671
+ components.configs.length = 0;
672
+ console.log("[MMDPlayerBase] \u2705 All btDefaultCollisionConfiguration destroyed");
843
673
  }
844
- onAudioEnded?.();
845
- };
846
- }
847
- setLoadingProgress(100);
848
- setLoading(false);
849
- vmdDataRef.current = { mesh, vmd, cameraVmd };
850
- if (shouldAutoPlayAfterReloadRef.current) {
851
- shouldAutoPlayAfterReloadRef.current = false;
852
- setTimeout(() => play(), 500);
853
- } else if (autoPlay) {
854
- setTimeout(() => play(), 500);
855
- }
856
- onLoad?.();
857
- } catch (err) {
858
- console.error("\u274C MMD\u52A0\u8F7D\u5931\u8D25:", err);
859
- setError(err.message || "\u52A0\u8F7D\u5931\u8D25");
860
- setLoading(false);
861
- isLoadedRef.current = false;
862
- onError?.(err);
863
- }
864
- };
865
- loadMMD();
866
- }, [currentResources, stage?.enablePhysics, autoPlay, loop, onLoad, onError, reloadTrigger]);
867
- const play = () => {
868
- if (!helperRef.current && !needReset) return;
869
- if (needReset && vmdDataRef.current && sceneRef.current && cameraRef.current) {
870
- const { mesh, vmd, cameraVmd } = vmdDataRef.current;
871
- if (helperRef.current) {
872
- try {
873
- const helperObjects = helperRef.current.objects;
874
- if (helperObjects && Array.isArray(helperObjects)) {
875
- helperObjects.length = 0;
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");
876
677
  }
877
- } catch (error2) {
878
- }
879
- }
880
- const newHelper = new MMDAnimationHelper();
881
- helperRef.current = newHelper;
882
- clockRef.current = new THREE2.Clock();
883
- if (vmd && typeof vmd === "object") {
884
- try {
885
- newHelper.add(mesh, {
886
- animation: vmd,
887
- physics: stage?.enablePhysics !== false
888
- });
889
- } catch (error2) {
890
- try {
891
- newHelper.add(mesh, { physics: stage?.enablePhysics !== false });
892
- } 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;
893
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);
894
692
  }
895
- } else {
896
- try {
897
- newHelper.add(mesh, { physics: stage?.enablePhysics !== false });
898
- } catch (error2) {
899
- }
693
+ helperRef.current = null;
900
694
  }
901
- if (cameraVmd && typeof cameraVmd === "object") {
902
- try {
903
- newHelper.add(cameraRef.current, { animation: cameraVmd });
904
- } catch (error2) {
695
+ animationClipRef.current = null;
696
+ if (axesHelperRef.current) {
697
+ if (sceneRef.current) {
698
+ sceneRef.current.remove(axesHelperRef.current);
905
699
  }
700
+ axesHelperRef.current.dispose();
701
+ axesHelperRef.current = null;
906
702
  }
907
- if (audioRef.current) {
908
- audioRef.current.currentTime = 0;
909
- }
910
- setNeedReset(false);
911
- }
912
- if (!helperRef.current) {
913
- console.error("\u274C [play] helper \u4E0D\u5B58\u5728\uFF0C\u65E0\u6CD5\u64AD\u653E");
914
- return;
915
- }
916
- if (audioRef.current) {
917
- audioRef.current.play();
918
- }
919
- helperRef.current.enable("animation", true);
920
- helperRef.current.enable("ik", true);
921
- helperRef.current.enable("grant", true);
922
- helperRef.current.enable("physics", true);
923
- if (!isPlaying) {
924
- clockRef.current.start();
925
- }
926
- animationEndedFiredRef.current = false;
927
- lastAnimationTimeRef.current = 0;
928
- animationStoppedCountRef.current = 0;
929
- isPlayingRef.current = true;
930
- setIsPlaying(true);
931
- console.log("\u25B6\uFE0F \u5F00\u59CB\u64AD\u653E\uFF08\u5305\u62EC\u76F8\u673A\u52A8\u753B\uFF09");
932
- };
933
- const pause = () => {
934
- if (!helperRef.current) return;
935
- if (audioRef.current) {
936
- audioRef.current.pause();
937
- }
938
- clockRef.current.stop();
939
- isPlayingRef.current = false;
940
- setIsPlaying(false);
941
- console.log("\u23F8\uFE0F \u6682\u505C\u64AD\u653E\uFF08\u5305\u62EC\u76F8\u673A\u52A8\u753B\uFF09");
942
- };
943
- const stop = () => {
944
- if (!helperRef.current || !sceneRef.current) return;
945
- isPlayingRef.current = false;
946
- setIsPlaying(false);
947
- if (audioRef.current) {
948
- audioRef.current.pause();
949
- audioRef.current.currentTime = 0;
950
- }
951
- clockRef.current.stop();
952
- clockRef.current = new THREE2.Clock();
953
- const mesh = sceneRef.current.children.find(
954
- (child) => child.type === "SkinnedMesh" || child.isSkinnedMesh
955
- );
956
- if (mesh && mesh.skeleton) {
957
- mesh.skeleton.pose();
958
- }
959
- if (cameraRef.current) {
960
- const camPos = stage?.cameraPosition || { x: 0, y: 10, z: 30 };
961
- const camTarget = stage?.cameraTarget || { x: 0, y: 10, z: 0 };
962
- cameraRef.current.position.set(camPos.x, camPos.y, camPos.z);
963
- if (controlsRef.current) {
964
- controlsRef.current.target.set(camTarget.x, camTarget.y, camTarget.z);
965
- controlsRef.current.update();
966
- } else {
967
- cameraRef.current.lookAt(camTarget.x, camTarget.y, camTarget.z);
968
- }
969
- }
970
- setNeedReset(true);
971
- console.log("\u23F9\uFE0F \u505C\u6B62\u64AD\u653E\u5E76\u91CD\u7F6E\u5230\u521D\u59CB\u72B6\u6001\uFF0CneedReset = true");
972
- };
973
- const handleResourceChange = (resourceId) => {
974
- console.log("\u{1F504} [MMDPlayerEnhanced] \u5207\u6362\u8D44\u6E90:", resourceId);
975
- if (isPlayingRef.current) {
976
- isPlayingRef.current = false;
977
- setIsPlaying(false);
978
- }
979
- if (audioRef.current) {
980
- audioRef.current.pause();
981
- audioRef.current.currentTime = 0;
982
- }
983
- setSelectedResourceId(resourceId);
984
- isLoadedRef.current = false;
985
- setNeedReset(false);
986
- setReloadTrigger((prev) => prev + 1);
987
- if (onResourceChange) {
988
- onResourceChange(resourceId);
989
- }
990
- setShowSettings(false);
991
- };
992
- const handleSelectionChange = (type, id) => {
993
- console.log(`\u{1F504} [MMDPlayerEnhanced] \u9009\u62E9${type}:`, id);
994
- const wasPlaying = isPlayingRef.current;
995
- if (isPlayingRef.current) {
996
- isPlayingRef.current = false;
997
- setIsPlaying(false);
998
- }
999
- if (audioRef.current) {
1000
- audioRef.current.pause();
1001
- audioRef.current.currentTime = 0;
1002
- }
1003
- if (type === "model") setSelectedModelId(id);
1004
- if (type === "motion") setSelectedMotionId(id);
1005
- if (type === "audio") setSelectedAudioId(id);
1006
- if (type === "camera") setSelectedCameraId(id);
1007
- if (type === "stageModel") setSelectedStageModelId(id);
1008
- if (type === "background") setSelectedBackgroundId(id);
1009
- isLoadedRef.current = false;
1010
- setNeedReset(false);
1011
- if (wasPlaying || autoPlay) {
1012
- shouldAutoPlayAfterReloadRef.current = true;
1013
- }
1014
- setReloadTrigger((prev) => prev + 1);
1015
- if (onSelectionChange) {
1016
- const newSelection = {
1017
- modelId: type === "model" ? id : selectedModelId,
1018
- motionId: type === "motion" ? id : selectedMotionId,
1019
- audioId: type === "audio" ? id : selectedAudioId,
1020
- cameraId: type === "camera" ? id : selectedCameraId,
1021
- stageModelId: type === "stageModel" ? id : selectedStageModelId,
1022
- backgroundId: type === "background" ? id : selectedBackgroundId
1023
- };
1024
- onSelectionChange(newSelection);
1025
- }
1026
- };
1027
- 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(
1028
- "div",
1029
- {
1030
- className: "h-full bg-gradient-to-r from-blue-500 to-purple-500 transition-all duration-300",
1031
- style: { width: `${loadingProgress}%` }
1032
- }
1033
- )), /* @__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(
1034
- "button",
1035
- {
1036
- onClick: play,
1037
- 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",
1038
- title: "\u64AD\u653E"
1039
- },
1040
- "\u25B6\uFE0F"
1041
- ) : /* @__PURE__ */ React2.createElement(
1042
- "button",
1043
- {
1044
- onClick: pause,
1045
- 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",
1046
- title: "\u6682\u505C"
1047
- },
1048
- "\u23F8\uFE0F"
1049
- ), /* @__PURE__ */ React2.createElement(
1050
- "button",
1051
- {
1052
- onClick: stop,
1053
- 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",
1054
- title: "\u505C\u6B62"
1055
- },
1056
- "\u23F9\uFE0F"
1057
- ), (resourcesList && resourcesList.length > 1 || resourceOptions) && /* @__PURE__ */ React2.createElement(
1058
- "button",
1059
- {
1060
- onClick: () => setShowSettings(true),
1061
- 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",
1062
- title: "\u8BBE\u7F6E"
1063
- },
1064
- "\u2699\uFE0F"
1065
- )), 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(
1066
- "button",
1067
- {
1068
- onClick: () => setShowSettings(false),
1069
- className: "text-2xl text-white/60 transition-colors hover:text-white"
1070
- },
1071
- "\u2715"
1072
- )), /* @__PURE__ */ React2.createElement("div", { className: "max-h-[60vh] overflow-y-auto p-4" }, resourcesList.map((item) => /* @__PURE__ */ React2.createElement(
1073
- "button",
1074
- {
1075
- key: item.id,
1076
- onClick: () => handleResourceChange(item.id),
1077
- 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"}`
1078
- },
1079
- /* @__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"))
1080
- ))))), 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(
1081
- "button",
1082
- {
1083
- onClick: () => setShowSettings(false),
1084
- className: "text-lg text-white/60 transition-colors hover:text-white"
1085
- },
1086
- "\u2715"
1087
- )), /* @__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(
1088
- "button",
1089
- {
1090
- onClick: () => setExpandedSection(expandedSection === "model" ? null : "model"),
1091
- className: "w-full flex items-center justify-between px-3 py-2.5 text-left hover:bg-white/10 transition-colors"
1092
- },
1093
- /* @__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")),
1094
- /* @__PURE__ */ React2.createElement("span", { className: `text-white/60 transition-transform ${expandedSection === "model" ? "rotate-180" : ""}` }, "\u25BC")
1095
- ), 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(
1096
- "button",
1097
- {
1098
- key: model.id,
1099
- onClick: () => {
1100
- handleSelectionChange("model", model.id);
1101
- setExpandedSection(null);
1102
- },
1103
- 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"}`
1104
- },
1105
- model.name
1106
- )))), resourceOptions.motions && resourceOptions.motions.length > 0 && /* @__PURE__ */ React2.createElement("div", { className: "rounded-lg bg-white/5 overflow-hidden" }, /* @__PURE__ */ React2.createElement(
1107
- "button",
1108
- {
1109
- onClick: () => setExpandedSection(expandedSection === "motion" ? null : "motion"),
1110
- className: "w-full flex items-center justify-between px-3 py-2.5 text-left hover:bg-white/10 transition-colors"
1111
- },
1112
- /* @__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")),
1113
- /* @__PURE__ */ React2.createElement("span", { className: `text-white/60 transition-transform ${expandedSection === "motion" ? "rotate-180" : ""}` }, "\u25BC")
1114
- ), 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(
1115
- "button",
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();
709
+ }
710
+ if (object.bindMatrix) {
711
+ object.bindMatrix = null;
712
+ }
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;
746
+ }
747
+ });
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?.();
762
+ }
763
+ } catch (e) {
764
+ console.warn("[MMDPlayerBase] Error closing AudioContext:", e);
765
+ }
766
+ }
767
+ if (object instanceof THREE.Light) {
768
+ if (object.shadow && object.shadow.map) {
769
+ object.shadow.map.dispose();
770
+ object.shadow.map = null;
771
+ }
772
+ }
773
+ });
774
+ sceneRef.current.clear();
775
+ sceneRef.current = null;
776
+ }
777
+ if (controlsRef.current) {
778
+ controlsRef.current.dispose();
779
+ controlsRef.current = null;
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;
841
+ }
842
+ }, [showAxes]);
843
+ useEffect(() => {
844
+ loopRef.current = loop;
845
+ if (audioRef.current && audioRef.current.buffer) {
846
+ audioRef.current.setLoop(loop);
847
+ }
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);
866
+ }
867
+ };
868
+ return /* @__PURE__ */ React6.createElement(
869
+ "div",
1116
870
  {
1117
- onClick: () => {
1118
- handleSelectionChange("motion", "");
1119
- setExpandedSection(null);
1120
- },
1121
- 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"}`
1122
- },
1123
- "\u65E0"
1124
- ), resourceOptions.motions.map((motion) => /* @__PURE__ */ React2.createElement(
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
+ }
881
+ }
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(
1125
905
  "button",
1126
906
  {
1127
- key: motion.id,
1128
- onClick: () => {
1129
- handleSelectionChange("motion", motion.id);
1130
- setExpandedSection(null);
1131
- },
1132
- 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"
1133
910
  },
1134
- motion.name
1135
- )))), 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(
1136
913
  "button",
1137
914
  {
1138
- onClick: () => setExpandedSection(expandedSection === "audio" ? null : "audio"),
1139
- 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"
1140
918
  },
1141
- /* @__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")),
1142
- /* @__PURE__ */ React2.createElement("span", { className: `text-white/60 transition-transform ${expandedSection === "audio" ? "rotate-180" : ""}` }, "\u25BC")
1143
- ), 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(
1144
921
  "button",
1145
922
  {
1146
- onClick: () => {
1147
- handleSelectionChange("audio", "");
1148
- setExpandedSection(null);
1149
- },
1150
- 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"
1151
926
  },
1152
- "\u65E0"
1153
- ), 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(
1154
929
  "button",
1155
930
  {
1156
- key: audio.id,
1157
- onClick: () => {
1158
- handleSelectionChange("audio", audio.id);
1159
- setExpandedSection(null);
1160
- },
1161
- 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"
1162
934
  },
1163
- audio.name
1164
- )))), 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(
1165
937
  "button",
1166
938
  {
1167
- onClick: () => setExpandedSection(expandedSection === "camera" ? null : "camera"),
1168
- 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"
1169
942
  },
1170
- /* @__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")),
1171
- /* @__PURE__ */ React2.createElement("span", { className: `text-white/60 transition-transform ${expandedSection === "camera" ? "rotate-180" : ""}` }, "\u25BC")
1172
- ), 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(
1173
945
  "button",
1174
946
  {
1175
- onClick: () => {
1176
- handleSelectionChange("camera", "");
1177
- setExpandedSection(null);
1178
- },
1179
- 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"
1180
950
  },
1181
- "\u65E0"
1182
- ), resourceOptions.cameras.map((camera) => /* @__PURE__ */ React2.createElement(
951
+ /* @__PURE__ */ React6.createElement(Grid3x3, { size: 20 })
952
+ ), showSettings && /* @__PURE__ */ React6.createElement(
1183
953
  "button",
1184
954
  {
1185
- key: camera.id,
1186
- onClick: () => {
1187
- handleSelectionChange("camera", camera.id);
1188
- setExpandedSection(null);
1189
- },
1190
- 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"
1191
958
  },
1192
- camera.name
1193
- )))), 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(
1194
961
  "button",
1195
962
  {
1196
- onClick: () => setExpandedSection(expandedSection === "stageModel" ? null : "stageModel"),
1197
- 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"
1198
966
  },
1199
- /* @__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")),
1200
- /* @__PURE__ */ React2.createElement("span", { className: `text-white/60 transition-transform ${expandedSection === "stageModel" ? "rotate-180" : ""}` }, "\u25BC")
1201
- ), 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(
1202
- "button",
1203
- {
1204
- onClick: () => {
1205
- handleSelectionChange("stageModel", "");
1206
- 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"}`
1207
988
  },
1208
- 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"}`
1209
- },
1210
- "\u65E0"
1211
- ), resourceOptions.stageModels.map((stageModel) => /* @__PURE__ */ React2.createElement(
1212
- "button",
1213
- {
1214
- key: stageModel.id,
1215
- onClick: () => {
1216
- handleSelectionChange("stageModel", stageModel.id);
1217
- 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"}`
1218
1002
  },
1219
- 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"}`
1220
- },
1221
- stageModel.name
1222
- )))), resourceOptions.backgrounds && resourceOptions.backgrounds.length > 0 && /* @__PURE__ */ React2.createElement("div", { className: "rounded-lg bg-white/5 overflow-hidden" }, /* @__PURE__ */ React2.createElement(
1223
- "button",
1224
- {
1225
- onClick: () => setExpandedSection(expandedSection === "background" ? null : "background"),
1226
- className: "w-full flex items-center justify-between px-3 py-2.5 text-left hover:bg-white/10 transition-colors"
1227
- },
1228
- /* @__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")),
1229
- /* @__PURE__ */ React2.createElement("span", { className: `text-white/60 transition-transform ${expandedSection === "background" ? "rotate-180" : ""}` }, "\u25BC")
1230
- ), 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(
1231
1013
  "button",
1232
1014
  {
1233
- onClick: () => {
1234
- handleSelectionChange("background", "");
1235
- setExpandedSection(null);
1236
- },
1237
- 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"
1238
1017
  },
1239
- "\u65E0"
1240
- ), resourceOptions.backgrounds.map((background) => /* @__PURE__ */ React2.createElement(
1241
- "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",
1242
1046
  {
1243
- key: background.id,
1244
- onClick: () => {
1245
- handleSelectionChange("background", background.id);
1246
- setExpandedSection(null);
1247
- },
1248
- 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"}`
1249
- },
1250
- background.name
1251
- )))))));
1252
- });
1253
- MMDPlayerEnhanced.displayName = "MMDPlayerEnhanced";
1254
- var MMDPlaylist = ({
1255
- playlist,
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())));
1053
+ };
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,
1256
1069
  stage,
1257
- defaultNodeIndex = 0,
1070
+ autoPlay = false,
1071
+ loop = true,
1072
+ volume: initialVolume = 1,
1073
+ muted: initialMuted = false,
1074
+ mobileOptimization,
1075
+ showDebugInfo = false,
1258
1076
  className,
1259
1077
  style,
1260
1078
  onLoad,
1261
- onError,
1262
- onNodeChange,
1263
- onPlaylistComplete
1079
+ onPlay,
1080
+ onPause,
1081
+ onEnded,
1082
+ ...rest
1264
1083
  }) => {
1265
- console.log("\u{1F3AC} [MMDPlaylist] \u7EC4\u4EF6\u521D\u59CB\u5316");
1266
- console.log("\u{1F4CB} [MMDPlaylist] \u64AD\u653E\u5217\u8868:", playlist.name, "\u8282\u70B9\u6570:", playlist.nodes.length);
1267
- 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);
1268
1093
  const [showSettings, setShowSettings] = useState(false);
1269
- const [preloadedNodes, setPreloadedNodes] = useState(/* @__PURE__ */ new Set());
1270
- const [isPreloading, setIsPreloading] = useState(true);
1271
- const [preloadProgress, setPreloadProgress] = useState(0);
1272
- const [editableNodes, setEditableNodes] = useState(playlist.nodes);
1273
- const currentNodeIndexRef = useRef(defaultNodeIndex);
1274
- const isAutoSwitchRef = useRef(false);
1275
- const playerRefsMap = useRef(/* @__PURE__ */ new Map());
1276
- const playerComponentRefs = useRef(/* @__PURE__ */ new Map());
1277
- const [memoryUsage, setMemoryUsage] = useState(0);
1094
+ const [showAxes, setShowAxes] = useState(false);
1095
+ const [isLooping, setIsLooping] = useState(loop);
1096
+ const playerRef = useRef(null);
1097
+ const containerRef = useRef(null);
1278
1098
  useEffect(() => {
1279
- currentNodeIndexRef.current = currentNodeIndex;
1280
- }, [currentNodeIndex]);
1281
- const currentNode = editableNodes[currentNodeIndex];
1282
- if (!currentNode) {
1283
- console.error("\u274C [MMDPlaylist] \u65E0\u6548\u7684\u8282\u70B9\u7D22\u5F15:", currentNodeIndex);
1284
- 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"));
1285
- }
1286
- console.log("\u{1F3AF} [MMDPlaylist] \u5F53\u524D\u8282\u70B9:", currentNode.name, "\u7D22\u5F15:", currentNodeIndex);
1287
- const stopNode = (nodeIndex) => {
1288
- const playerComponent = playerComponentRefs.current.get(nodeIndex);
1289
- if (playerComponent && playerComponent.stopCompletely) {
1290
- console.log(`\u23F9\uFE0F [MMDPlaylist] \u505C\u6B62\u8282\u70B9 ${nodeIndex}`);
1291
- playerComponent.stopCompletely();
1292
- } else {
1293
- const playerElement = playerRefsMap.current.get(nodeIndex);
1294
- if (!playerElement) return;
1295
- console.log(`\u23F9\uFE0F [MMDPlaylist] \u505C\u6B62\u8282\u70B9 ${nodeIndex} (DOM\u65B9\u5F0F)`);
1296
- const audioElement = playerElement.querySelector("audio");
1297
- if (audioElement) {
1298
- audioElement.pause();
1299
- audioElement.currentTime = 0;
1300
- console.log(` \u{1F507} \u505C\u6B62\u97F3\u9891`);
1301
- }
1302
- const stopEvent = new CustomEvent("stopCompletely");
1303
- playerElement.dispatchEvent(stopEvent);
1304
- console.log(` \u{1F4E1} \u53D1\u9001\u505C\u6B62\u4E8B\u4EF6`);
1305
- }
1306
- };
1307
- const clearNodeResources = (nodeIndex, excludeCurrent = true) => {
1308
- if (excludeCurrent && nodeIndex === currentNodeIndex) {
1309
- console.log(`\u26A0\uFE0F [MMDPlaylist] \u8DF3\u8FC7\u6E05\u7406\u5F53\u524D\u64AD\u653E\u8282\u70B9 ${nodeIndex}`);
1310
- return;
1311
- }
1312
- const playerComponent = playerComponentRefs.current.get(nodeIndex);
1313
- if (playerComponent && playerComponent.clearResources) {
1314
- console.log(`\u{1F9F9} [MMDPlaylist] \u6E05\u7406\u8282\u70B9 ${nodeIndex} \u8D44\u6E90`);
1315
- playerComponent.clearResources();
1316
- } else {
1317
- const playerElement = playerRefsMap.current.get(nodeIndex);
1318
- if (playerElement) {
1319
- const cleanupEvent = new CustomEvent("cleanupResources");
1320
- playerElement.dispatchEvent(cleanupEvent);
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);
1321
1105
  }
1322
- }
1323
- };
1324
- const emergencyMemoryCleanup = () => {
1325
- if (window.performance?.memory) {
1326
- const memInfo = window.performance.memory;
1327
- const usage = memInfo.usedJSHeapSize / memInfo.totalJSHeapSize;
1328
- if (usage > 0.9) {
1329
- console.error(`\u{1F6A8} [MMDPlaylist] \u5185\u5B58\u4F7F\u7528\u4E25\u91CD\u8FC7\u9AD8 (${(usage * 100).toFixed(1)}%)\uFF0C\u7D27\u6025\u6E05\u7406`);
1330
- const nodesToClean = editableNodes.map((_, index) => ({
1331
- index,
1332
- distance: Math.abs(index - currentNodeIndex)
1333
- })).filter((node) => node.distance > 2).sort((a, b) => b.distance - a.distance).slice(0, 2);
1334
- nodesToClean.forEach(({ index }) => {
1335
- console.warn(`\u{1F9F9} [MMDPlaylist] \u7D27\u6025\u6E05\u7406\u8282\u70B9 ${index}`);
1336
- clearNodeResources(index, false);
1337
- });
1338
- if (window.gc) {
1339
- try {
1340
- window.gc();
1341
- } catch (e) {
1342
- }
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 }));
1343
1116
  }
1344
1117
  }
1345
- }
1346
- };
1347
- useEffect(() => {
1348
- console.log(`\u{1F504} [MMDPlaylist] \u8282\u70B9\u5207\u6362: ${currentNodeIndex} - ${currentNode.name}`);
1349
- editableNodes.forEach((_, index) => {
1350
- if (index !== currentNodeIndex) {
1351
- stopNode(index);
1118
+ if (selection.motionId) {
1119
+ const m = resourceOptions.motions.find((o) => o.id === selection.motionId);
1120
+ if (m) res.motionPath = m.path;
1352
1121
  }
1353
- });
1354
- const nodesToKeep = /* @__PURE__ */ new Set();
1355
- nodesToKeep.add(currentNodeIndex);
1356
- if (playlist.loop && currentNodeIndex > 0) {
1357
- nodesToKeep.add(currentNodeIndex - 1);
1358
- }
1359
- if (currentNodeIndex < editableNodes.length - 1) {
1360
- nodesToKeep.add(currentNodeIndex + 1);
1361
- }
1362
- editableNodes.forEach((_, index) => {
1363
- if (!nodesToKeep.has(index)) {
1364
- clearNodeResources(index, false);
1122
+ if (selection.cameraId) {
1123
+ const c = resourceOptions.cameras?.find((o) => o.id === selection.cameraId);
1124
+ if (c) res.cameraPath = c.path;
1365
1125
  }
1366
- });
1367
- onNodeChange?.(currentNodeIndex, currentNode);
1368
- if (!isPreloading && (isAutoSwitchRef.current || playlist.autoPlay)) {
1369
- console.log(`\u25B6\uFE0F [MMDPlaylist] \u51C6\u5907\u64AD\u653E\u8282\u70B9 ${currentNodeIndex}`);
1370
- if (!preloadedNodes.has(currentNodeIndex)) {
1371
- console.warn(`\u26A0\uFE0F [MMDPlaylist] \u8282\u70B9 ${currentNodeIndex} \u5C1A\u672A\u9884\u52A0\u8F7D\u5B8C\u6210\uFF0C\u7B49\u5F85...`);
1372
- return;
1126
+ if (selection.audioId) {
1127
+ const a = resourceOptions.audios?.find((o) => o.id === selection.audioId);
1128
+ if (a) res.audioPath = a.path;
1373
1129
  }
1374
- requestAnimationFrame(() => {
1375
- const playerElement = playerRefsMap.current.get(currentNodeIndex);
1376
- if (playerElement) {
1377
- const playButton = playerElement.querySelector('button[title="\u64AD\u653E"]');
1378
- if (playButton) {
1379
- console.log(`\u{1F3AC} [MMDPlaylist] \u89E6\u53D1\u8282\u70B9 ${currentNodeIndex} \u64AD\u653E`);
1380
- playButton.click();
1381
- } else {
1382
- console.warn(`\u26A0\uFE0F [MMDPlaylist] \u672A\u627E\u5230\u8282\u70B9 ${currentNodeIndex} \u7684\u64AD\u653E\u6309\u94AE`);
1383
- }
1384
- } else {
1385
- console.warn(`\u26A0\uFE0F [MMDPlaylist] \u672A\u627E\u5230\u8282\u70B9 ${currentNodeIndex} \u7684 DOM \u5143\u7D20`);
1386
- }
1387
- });
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);
1388
1137
  }
1389
- }, [currentNodeIndex, currentNode, onNodeChange, isPreloading, playlist.autoPlay, playlist.loop, preloadedNodes, editableNodes]);
1390
- const handleNodePreloaded = (nodeIndex) => {
1391
- console.log(`\u2705 [MMDPlaylist] \u8282\u70B9 ${nodeIndex} \u9884\u52A0\u8F7D\u5B8C\u6210`);
1392
- setPreloadedNodes((prev) => {
1393
- const newSet = new Set(prev);
1394
- newSet.add(nodeIndex);
1395
- return newSet;
1396
- });
1397
- };
1398
- useEffect(() => {
1399
- if (preloadedNodes.size === editableNodes.length) {
1400
- console.log("\u{1F389} [MMDPlaylist] \u6240\u6709\u8282\u70B9\u9884\u52A0\u8F7D\u5B8C\u6210");
1401
- setIsPreloading(false);
1402
- onLoad?.();
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}`);
1144
+ });
1145
+ setIsFullscreen(true);
1403
1146
  } else {
1404
- const progress = Math.round(preloadedNodes.size / editableNodes.length * 100);
1405
- setPreloadProgress(progress);
1147
+ document.exitFullscreen();
1148
+ setIsFullscreen(false);
1406
1149
  }
1407
- }, [preloadedNodes, editableNodes.length, onLoad]);
1150
+ }, []);
1408
1151
  useEffect(() => {
1409
- const checkMemory = () => {
1410
- if (window.performance?.memory) {
1411
- const memInfo = window.performance.memory;
1412
- const usage = memInfo.usedJSHeapSize / memInfo.totalJSHeapSize;
1413
- setMemoryUsage(usage);
1414
- if (usage > 0.9) {
1415
- emergencyMemoryCleanup();
1416
- }
1417
- }
1152
+ const handleFsChange = () => {
1153
+ setIsFullscreen(!!document.fullscreenElement);
1418
1154
  };
1419
- const interval = setInterval(checkMemory, 15e3);
1420
- return () => clearInterval(interval);
1421
- }, [currentNodeIndex, editableNodes]);
1422
- const handlePlaybackEnded = (nodeIndex) => {
1423
- console.log(`\u{1F3B5} [MMDPlaylist] \u8282\u70B9 ${nodeIndex} \u64AD\u653E\u5B8C\u6210`);
1424
- if (nodeIndex !== currentNodeIndexRef.current) {
1425
- 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`);
1426
- return;
1427
- }
1428
- const node = editableNodes[nodeIndex];
1429
- if (!node) return;
1430
- if (node.loop) {
1431
- console.log("\u{1F501} [MMDPlaylist] \u5F53\u524D\u8282\u70B9\u5FAA\u73AF\u64AD\u653E");
1432
- return;
1433
- }
1434
- const isLastNode = nodeIndex === editableNodes.length - 1;
1435
- if (!isLastNode) {
1436
- console.log(`\u27A1\uFE0F [MMDPlaylist] \u5207\u6362\u5230\u4E0B\u4E00\u4E2A\u8282\u70B9: ${nodeIndex + 1}`);
1437
- isAutoSwitchRef.current = true;
1438
- setCurrentNodeIndex(nodeIndex + 1);
1439
- return;
1440
- }
1441
- if (playlist.loop) {
1442
- console.log("\u{1F501} [MMDPlaylist] \u64AD\u653E\u5217\u8868\u5FAA\u73AF\uFF0C\u56DE\u5230\u7B2C\u4E00\u4E2A\u8282\u70B9");
1443
- isAutoSwitchRef.current = true;
1444
- setCurrentNodeIndex(0);
1445
- return;
1446
- }
1447
- console.log("\u2705 [MMDPlaylist] \u64AD\u653E\u5217\u8868\u64AD\u653E\u5B8C\u6210");
1448
- onPlaylistComplete?.();
1449
- };
1450
- const playlistPrevious = () => {
1451
- const newIndex = currentNodeIndex > 0 ? currentNodeIndex - 1 : editableNodes.length - 1;
1452
- console.log(`\u2B05\uFE0F [MMDPlaylist] \u4E0A\u4E00\u4E2A\u8282\u70B9: ${newIndex}`);
1453
- isAutoSwitchRef.current = false;
1454
- setCurrentNodeIndex(newIndex);
1455
- };
1456
- const playlistNext = () => {
1457
- const newIndex = currentNodeIndex < editableNodes.length - 1 ? currentNodeIndex + 1 : 0;
1458
- console.log(`\u27A1\uFE0F [MMDPlaylist] \u4E0B\u4E00\u4E2A\u8282\u70B9: ${newIndex}`);
1459
- isAutoSwitchRef.current = false;
1460
- setCurrentNodeIndex(newIndex);
1461
- };
1462
- const playlistJumpTo = (index) => {
1463
- if (index < 0 || index >= editableNodes.length) return;
1464
- console.log(`\u{1F3AF} [MMDPlaylist] \u8DF3\u8F6C\u5230\u8282\u70B9: ${index}`);
1465
- isAutoSwitchRef.current = false;
1466
- setCurrentNodeIndex(index);
1467
- };
1468
- const handleDeleteNode = (index) => {
1469
- if (editableNodes.length <= 1) {
1470
- alert("\u64AD\u653E\u5217\u8868\u81F3\u5C11\u9700\u8981\u4FDD\u7559\u4E00\u4E2A\u8282\u70B9");
1471
- return;
1472
- }
1473
- const newNodes = editableNodes.filter((_, i) => i !== index);
1474
- setEditableNodes(newNodes);
1475
- if (index < currentNodeIndex) {
1476
- setCurrentNodeIndex(currentNodeIndex - 1);
1477
- } else if (index === currentNodeIndex) {
1478
- const newIndex = Math.max(0, currentNodeIndex - 1);
1479
- setCurrentNodeIndex(newIndex);
1155
+ document.addEventListener("fullscreenchange", handleFsChange);
1156
+ return () => document.removeEventListener("fullscreenchange", handleFsChange);
1157
+ }, []);
1158
+ const handlePlayPause = () => {
1159
+ if (isPlaying) {
1160
+ playerRef.current?.pause();
1161
+ } else {
1162
+ playerRef.current?.play();
1480
1163
  }
1481
- console.log(`\u{1F5D1}\uFE0F [MMDPlaylist] \u5220\u9664\u8282\u70B9 ${index}`);
1164
+ setIsPlaying(!isPlaying);
1482
1165
  };
1483
- const handleMoveNodeUp = (index) => {
1484
- if (index === 0) return;
1485
- const newNodes = [...editableNodes];
1486
- const temp = newNodes[index - 1];
1487
- newNodes[index - 1] = newNodes[index];
1488
- newNodes[index] = temp;
1489
- setEditableNodes(newNodes);
1490
- if (currentNodeIndex === index) {
1491
- setCurrentNodeIndex(index - 1);
1492
- } else if (currentNodeIndex === index - 1) {
1493
- setCurrentNodeIndex(index);
1494
- }
1495
- console.log(`\u2B06\uFE0F [MMDPlaylist] \u8282\u70B9 ${index} \u4E0A\u79FB`);
1166
+ const handleListSelect = (id) => {
1167
+ setCurrentId(id);
1168
+ setIsPlaying(true);
1169
+ setShowSettings(false);
1496
1170
  };
1497
- const handleMoveNodeDown = (index) => {
1498
- if (index === editableNodes.length - 1) return;
1499
- const newNodes = [...editableNodes];
1500
- const temp = newNodes[index];
1501
- newNodes[index] = newNodes[index + 1];
1502
- newNodes[index + 1] = temp;
1503
- setEditableNodes(newNodes);
1504
- if (currentNodeIndex === index) {
1505
- setCurrentNodeIndex(index + 1);
1506
- } else if (currentNodeIndex === index + 1) {
1507
- setCurrentNodeIndex(index);
1508
- }
1509
- 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
+ });
1510
1181
  };
1511
- const shouldAutoPlayInitial = playlist.autoPlay && currentNodeIndex === defaultNodeIndex && !isPreloading;
1512
- return /* @__PURE__ */ React2.createElement("div", { className: `relative ${className || ""}`, style }, editableNodes.map((node, index) => {
1513
- return /* @__PURE__ */ React2.createElement(
1514
- "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,
1515
1194
  {
1516
- key: `player-${node.id}-${index}`,
1517
- ref: (el) => {
1518
- if (el) {
1519
- playerRefsMap.current.set(index, el);
1520
- }
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();
1521
1209
  },
1522
- className: "absolute inset-0",
1523
- style: {
1524
- visibility: index === currentNodeIndex ? "visible" : "hidden",
1525
- zIndex: index === currentNodeIndex ? 1 : 0
1526
- }
1527
- },
1528
- /* @__PURE__ */ React2.createElement(
1529
- MMDPlayerEnhanced,
1530
- {
1531
- ref: (componentRef) => {
1532
- if (componentRef) {
1533
- playerComponentRefs.current.set(index, componentRef);
1534
- }
1535
- },
1536
- resources: node.resources,
1537
- stage,
1538
- autoPlay: index === currentNodeIndex && shouldAutoPlayInitial,
1539
- loop: node.loop || false,
1540
- className: "h-full w-full",
1541
- onLoad: () => {
1542
- handleNodePreloaded(index);
1543
- },
1544
- onError: (error) => {
1545
- console.error(`\u274C [MMDPlaylist] \u8282\u70B9 ${index} \u52A0\u8F7D\u5931\u8D25:`, error);
1546
- if (index === currentNodeIndex) {
1547
- onError?.(error);
1548
- }
1549
- },
1550
- onAudioEnded: () => handlePlaybackEnded(index),
1551
- onAnimationEnded: () => handlePlaybackEnded(index)
1552
- }
1553
- )
1554
- );
1555
- }), 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(
1556
1302
  "div",
1557
1303
  {
1558
- className: "h-full bg-gradient-to-r from-purple-500 to-blue-500 transition-all duration-300",
1559
- 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
+ }
1560
1308
  }
1561
- )), /* @__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(
1562
- "button",
1563
- {
1564
- onClick: playlistPrevious,
1565
- 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",
1566
- title: "\u4E0A\u4E00\u4E2A\u8282\u70B9"
1567
- },
1568
- "\u23EE\uFE0F"
1569
- ), /* @__PURE__ */ React2.createElement(
1570
- "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",
1571
1311
  {
1572
- onClick: () => setShowSettings(true),
1573
- 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",
1574
- 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"}`
1575
1314
  },
1576
- "\u2699\uFE0F"
1577
- ), editableNodes.length > 1 && /* @__PURE__ */ React2.createElement(
1578
- "button",
1579
- {
1580
- onClick: playlistNext,
1581
- 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",
1582
- 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
+ });
1583
1385
  },
1584
- "\u23ED\uFE0F"
1585
- )), !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(
1586
1489
  "div",
1587
1490
  {
1588
- className: "absolute inset-0 z-[100] flex items-start justify-end bg-black/40",
1589
- onClick: () => setShowSettings(false)
1491
+ ref: containerRef,
1492
+ className: `relative overflow-hidden bg-black group flex h-full ${className}`,
1493
+ style
1590
1494
  },
1591
- /* @__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(
1592
1518
  "div",
1593
1519
  {
1594
- 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",
1595
- style: { maxHeight: "calc(100vh - 2rem)" },
1596
- onClick: (e) => e.stopPropagation()
1520
+ className: `transition-opacity duration-300 ${isPlaying && !showPlaylist ? "opacity-0 group-hover:opacity-100" : "opacity-100"}`
1597
1521
  },
1598
- /* @__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(
1599
- "button",
1522
+ /* @__PURE__ */ React6.createElement(
1523
+ ControlPanel,
1600
1524
  {
1601
- onClick: () => setShowSettings(false),
1602
- 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);
1603
1559
  },
1604
- "\u2715"
1605
- )),
1606
- /* @__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(
1607
1563
  "div",
1608
1564
  {
1609
- key: `${node.id}-${index}`,
1610
- 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"}`
1611
1566
  },
1612
- /* @__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(
1613
- "button",
1614
- {
1615
- onClick: () => handleMoveNodeUp(index),
1616
- className: "p-0.5 rounded bg-white/10 hover:bg-white/20 text-white text-[10px] transition-colors",
1617
- title: "\u4E0A\u79FB"
1618
- },
1619
- "\u2B06\uFE0F"
1620
- ), index < editableNodes.length - 1 && /* @__PURE__ */ React2.createElement(
1621
- "button",
1622
- {
1623
- onClick: () => handleMoveNodeDown(index),
1624
- className: "p-0.5 rounded bg-white/10 hover:bg-white/20 text-white text-[10px] transition-colors",
1625
- title: "\u4E0B\u79FB"
1626
- },
1627
- "\u2B07\uFE0F"
1628
- ), /* @__PURE__ */ React2.createElement(
1629
- "button",
1630
- {
1631
- onClick: () => playlistJumpTo(index),
1632
- className: "p-0.5 rounded bg-blue-500/30 hover:bg-blue-500/50 text-white text-[10px] transition-colors",
1633
- title: "\u8DF3\u8F6C"
1634
- },
1635
- "\u25B6\uFE0F"
1636
- ), /* @__PURE__ */ React2.createElement(
1637
- "button",
1638
- {
1639
- onClick: () => {
1640
- if (confirm(`\u786E\u5B9A\u5220\u9664 "${node.name}"\uFF1F`)) {
1641
- handleDeleteNode(index);
1642
- }
1643
- },
1644
- className: "p-0.5 rounded bg-red-500/30 hover:bg-red-500/50 text-white text-[10px] transition-colors",
1645
- title: "\u5220\u9664"
1646
- },
1647
- "\u{1F5D1}\uFE0F"
1648
- )))
1649
- )))))
1650
- )
1651
- ));
1652
- };
1653
-
1654
- // src/mmd/presets.ts
1655
- var defaultMMDPreset = {
1656
- id: "default",
1657
- name: "\u9ED8\u8BA4\u6A21\u578B",
1658
- summary: "\u4EC5\u5C55\u793A\u6A21\u578B\uFF0C\u65E0\u52A8\u4F5C\u548C\u97F3\u9891",
1659
- badges: ["\u6A21\u578B", "\u9759\u6001"],
1660
- resources: {
1661
- modelPath: "/mikutalking/models/YYB_Z6SakuraMiku/miku.pmx"
1662
- },
1663
- stage: {
1664
- backgroundColor: "#000000",
1665
- cameraPosition: { x: 0, y: 10, z: 30 },
1666
- cameraTarget: { x: 0, y: 10, z: 0 },
1667
- enablePhysics: true,
1668
- showGrid: true,
1669
- ammoPath: "/mikutalking/libs/ammo.wasm.js",
1670
- ammoWasmPath: "/mikutalking/libs/"
1671
- }
1672
- };
1673
- var catchTheWavePreset = {
1674
- id: "catch-the-wave",
1675
- name: "Catch The Wave",
1676
- summary: "\u5B8C\u6574\u7684MMD\u8868\u6F14\uFF1A\u6A21\u578B\u3001\u52A8\u4F5C\u3001\u76F8\u673A\u8FD0\u955C\u3001\u97F3\u9891\u540C\u6B65",
1677
- badges: ["\u6A21\u578B", "\u52A8\u4F5C", "\u76F8\u673A", "\u97F3\u9891"],
1678
- resources: {
1679
- modelPath: "/mikutalking/models/YYB_Z6SakuraMiku/miku.pmx",
1680
- motionPath: "/mikutalking/actions/CatchTheWave/mmd_CatchTheWave_motion.vmd",
1681
- cameraPath: "/mikutalking/actions/CatchTheWave/camera.vmd",
1682
- audioPath: "/mikutalking/actions/CatchTheWave/pv_268.wav"
1683
- },
1684
- stage: {
1685
- backgroundColor: "#01030b",
1686
- cameraPosition: { x: 0, y: 10, z: 30 },
1687
- cameraTarget: { x: 0, y: 10, z: 0 },
1688
- enablePhysics: true,
1689
- showGrid: false,
1690
- ammoPath: "/mikutalking/libs/ammo.wasm.js",
1691
- ammoWasmPath: "/mikutalking/libs/"
1692
- }
1693
- };
1694
- var simpleModelPreset = {
1695
- id: "simple-model",
1696
- name: "\u7B80\u5355\u6A21\u578B",
1697
- summary: "\u8F7B\u91CF\u7EA7\u6D4B\u8BD5\u6A21\u578B",
1698
- badges: ["\u6A21\u578B", "\u8F7B\u91CF"],
1699
- resources: {
1700
- modelPath: "/mikutalking/models/test/v4c5.0.pmx"
1701
- },
1702
- stage: {
1703
- backgroundColor: "#ffffff",
1704
- cameraPosition: { x: 0, y: 10, z: 30 },
1705
- cameraTarget: { x: 0, y: 10, z: 0 },
1706
- enablePhysics: true,
1707
- showGrid: true,
1708
- ammoPath: "/mikutalking/libs/ammo.wasm.js",
1709
- ammoWasmPath: "/mikutalking/libs/"
1710
- }
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
+ );
1711
1590
  };
1712
- var availableMMDPresets = [
1713
- catchTheWavePreset,
1714
- defaultMMDPreset,
1715
- simpleModelPreset
1716
- ];
1717
1591
 
1718
- export { MMDPlayerBase, MMDPlayerEnhanced, MMDPlaylist, availableMMDPresets, catchTheWavePreset, defaultMMDPreset, loadAmmo, simpleModelPreset };
1592
+ export { MMDPlayerBase, MMDPlayerEnhanced, MMDPlayerEnhancedDebugInfo, MMDPlaylist, MMDPlaylistDebugInfo, loadAmmo };
1719
1593
  //# sourceMappingURL=index.mjs.map
1720
1594
  //# sourceMappingURL=index.mjs.map