sa2kit 1.0.9 → 1.2.0

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