sa2kit 1.5.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  import '../chunk-BJTO5JO5.mjs';
2
- import React6, { forwardRef, useRef, useImperativeHandle, useEffect, useState, useCallback } from 'react';
2
+ import React6, { forwardRef, useRef, useEffect, useImperativeHandle, useState, useCallback } from 'react';
3
3
  import * as THREE from 'three';
4
4
  import { OrbitControls, MMDLoader, MMDAnimationHelper } from 'three-stdlib';
5
- import { SkipBack, Pause, Play, SkipForward, Repeat, Repeat1, Shuffle, ListMusic, Music, X, Grid3x3, Settings, Minimize, Maximize, Video, Check, User, Image } from 'lucide-react';
5
+ import { SkipBack, Pause, Play, SkipForward, Camera, Repeat, Repeat1, Shuffle, ListMusic, Music, X, Grid3x3, Settings, Minimize, Maximize, Video, Check, User, Image } from 'lucide-react';
6
6
  import { createPortal } from 'react-dom';
7
7
 
8
8
  // src/mmd/utils/ammo-loader.ts
@@ -163,6 +163,7 @@ var MMDPlayerBase = forwardRef((props, ref) => {
163
163
  onPause,
164
164
  onEnded,
165
165
  onTimeUpdate,
166
+ onCameraChange,
166
167
  className,
167
168
  style
168
169
  } = props;
@@ -183,6 +184,10 @@ var MMDPlayerBase = forwardRef((props, ref) => {
183
184
  const animationClipRef = useRef(null);
184
185
  const loopRef = useRef(loop);
185
186
  const audioRef = useRef(null);
187
+ const latestCallbacks = useRef({ onPlay, onPause, onEnded, onTimeUpdate });
188
+ useEffect(() => {
189
+ latestCallbacks.current = { onPlay, onPause, onEnded, onTimeUpdate };
190
+ }, [onPlay, onPause, onEnded, onTimeUpdate]);
186
191
  const physicsComponentsRef = useRef({
187
192
  configs: [],
188
193
  dispatchers: [],
@@ -197,18 +202,18 @@ var MMDPlayerBase = forwardRef((props, ref) => {
197
202
  if (!isReadyRef.current) return;
198
203
  isPlayingRef.current = true;
199
204
  if (!clockRef.current.running) clockRef.current.start();
200
- onPlay?.();
205
+ latestCallbacks.current.onPlay?.();
201
206
  },
202
207
  pause: () => {
203
208
  if (!isPlayingRef.current) return;
204
209
  isPlayingRef.current = false;
205
210
  clockRef.current.stop();
206
- onPause?.();
211
+ latestCallbacks.current.onPause?.();
207
212
  },
208
213
  stop: () => {
209
214
  isPlayingRef.current = false;
210
215
  clockRef.current.stop();
211
- onPause?.();
216
+ latestCallbacks.current.onPause?.();
212
217
  },
213
218
  seek: (time) => {
214
219
  console.warn("Seek not fully implemented in MMDPlayerBase yet");
@@ -229,6 +234,24 @@ var MMDPlayerBase = forwardRef((props, ref) => {
229
234
  rendererRef.current.render(sceneRef.current, cameraRef.current);
230
235
  }
231
236
  return rendererRef.current.domElement.toDataURL("image/png");
237
+ },
238
+ resetCamera: () => {
239
+ if (!cameraRef.current || !controlsRef.current) return;
240
+ const { cameraPosition, cameraTarget } = stage;
241
+ if (cameraPosition) {
242
+ const pos = cameraPosition;
243
+ cameraRef.current.position.set(pos.x, pos.y, pos.z);
244
+ } else {
245
+ cameraRef.current.position.set(0, 20, 30);
246
+ }
247
+ if (cameraTarget) {
248
+ const target = cameraTarget;
249
+ controlsRef.current.target.set(target.x, target.y, target.z);
250
+ } else {
251
+ controlsRef.current.target.set(0, 10, 0);
252
+ }
253
+ controlsRef.current.update();
254
+ onCameraChange?.(false);
232
255
  }
233
256
  }));
234
257
  useEffect(() => {
@@ -315,6 +338,11 @@ var MMDPlayerBase = forwardRef((props, ref) => {
315
338
  const scene = new THREE.Scene();
316
339
  if (stage.backgroundColor) {
317
340
  scene.background = new THREE.Color(stage.backgroundColor);
341
+ } else if (stage.backgroundImage) {
342
+ const textureLoader = new THREE.TextureLoader();
343
+ textureLoader.load(stage.backgroundImage, (texture) => {
344
+ scene.background = texture;
345
+ });
318
346
  }
319
347
  sceneRef.current = scene;
320
348
  const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 2e3);
@@ -371,6 +399,9 @@ var MMDPlayerBase = forwardRef((props, ref) => {
371
399
  controls.target.set(0, 10, 0);
372
400
  }
373
401
  controls.update();
402
+ controls.addEventListener("start", () => {
403
+ onCameraChange?.(true);
404
+ });
374
405
  controlsRef.current = controls;
375
406
  if (showAxes) {
376
407
  const axesHelper = new THREE.AxesHelper(20);
@@ -589,6 +620,25 @@ var MMDPlayerBase = forwardRef((props, ref) => {
589
620
  console.log("[MMDPlayerBase] \u2705 Stage materials and textures loaded");
590
621
  scene.add(stageMesh);
591
622
  console.log("[MMDPlayerBase] \u2705 Stage added to scene (fully loaded)");
623
+ if (resources.stageMotionPath && stageMesh) {
624
+ console.log("[MMDPlayerBase] Loading stage motion:", resources.stageMotionPath);
625
+ const anyLoader = loader;
626
+ const anyHelper = helper;
627
+ const anyStage = stageMesh;
628
+ anyLoader.loadAnimation(
629
+ resources.stageMotionPath,
630
+ anyStage,
631
+ (stageAnimation) => {
632
+ if (checkCancelled()) return;
633
+ anyHelper.add(anyStage, {
634
+ animation: stageAnimation
635
+ });
636
+ console.log("[MMDPlayerBase] \u2705 Stage motion bound successfully");
637
+ },
638
+ void 0,
639
+ (err) => console.error("Failed to load stage motion:", err)
640
+ );
641
+ }
592
642
  } catch (err) {
593
643
  console.error("Failed to load stage:", err);
594
644
  }
@@ -1034,6 +1084,38 @@ ${errorMessage}
1034
1084
  audioRef.current.setLoop(loop);
1035
1085
  }
1036
1086
  }, [loop]);
1087
+ useEffect(() => {
1088
+ if (!isReadyRef.current) return;
1089
+ if (sceneRef.current) {
1090
+ if (stage.backgroundColor) {
1091
+ sceneRef.current.background = new THREE.Color(stage.backgroundColor);
1092
+ } else if (stage.backgroundImage) {
1093
+ const textureLoader = new THREE.TextureLoader();
1094
+ textureLoader.load(stage.backgroundImage, (texture) => {
1095
+ if (sceneRef.current) sceneRef.current.background = texture;
1096
+ });
1097
+ }
1098
+ }
1099
+ if (sceneRef.current) {
1100
+ sceneRef.current.traverse((obj) => {
1101
+ if (obj instanceof THREE.AmbientLight && stage.ambientLightIntensity !== void 0) {
1102
+ obj.intensity = stage.ambientLightIntensity;
1103
+ }
1104
+ if (obj instanceof THREE.DirectionalLight && stage.directionalLightIntensity !== void 0) {
1105
+ obj.intensity = stage.directionalLightIntensity;
1106
+ }
1107
+ });
1108
+ }
1109
+ if (cameraRef.current && stage.cameraPosition) {
1110
+ const pos = stage.cameraPosition;
1111
+ cameraRef.current.position.set(pos.x, pos.y, pos.z);
1112
+ }
1113
+ if (controlsRef.current && stage.cameraTarget) {
1114
+ const target = stage.cameraTarget;
1115
+ controlsRef.current.target.set(target.x, target.y, target.z);
1116
+ controlsRef.current.update();
1117
+ }
1118
+ }, [stage.backgroundColor, stage.backgroundImage, stage.ambientLightIntensity, stage.directionalLightIntensity, stage.cameraPosition, stage.cameraTarget]);
1037
1119
  const animate = () => {
1038
1120
  animationIdRef.current = requestAnimationFrame(animate);
1039
1121
  if (rendererRef.current && sceneRef.current && cameraRef.current) {
@@ -1043,11 +1125,11 @@ ${errorMessage}
1043
1125
  const elapsed = clockRef.current.elapsedTime;
1044
1126
  const duration = durationRef.current;
1045
1127
  const currentTime = duration > 0 && loopRef.current ? elapsed % duration : elapsed;
1046
- onTimeUpdate?.(currentTime);
1128
+ latestCallbacks.current.onTimeUpdate?.(currentTime);
1047
1129
  if (!loopRef.current && duration > 0 && elapsed >= duration) {
1048
1130
  isPlayingRef.current = false;
1049
1131
  clockRef.current.stop();
1050
- onEnded?.();
1132
+ latestCallbacks.current.onEnded?.();
1051
1133
  }
1052
1134
  }
1053
1135
  rendererRef.current.render(sceneRef.current, cameraRef.current);
@@ -1076,6 +1158,7 @@ var ControlPanel = ({
1076
1158
  isFullscreen,
1077
1159
  isLooping,
1078
1160
  isListLooping,
1161
+ isCameraManual = false,
1079
1162
  showSettings,
1080
1163
  showAxes = false,
1081
1164
  showPrevNext = false,
@@ -1088,7 +1171,8 @@ var ControlPanel = ({
1088
1171
  onToggleAxes,
1089
1172
  onOpenSettings,
1090
1173
  onPrevious,
1091
- onNext
1174
+ onNext,
1175
+ onResetCamera
1092
1176
  }) => {
1093
1177
  return /* @__PURE__ */ React6.createElement("div", { className: "absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 to-transparent p-4 transition-opacity duration-300 hover:opacity-100" }, /* @__PURE__ */ React6.createElement("div", { className: "flex items-center justify-between text-white" }, /* @__PURE__ */ React6.createElement("div", { className: "flex items-center gap-2" }, showPrevNext && onPrevious && /* @__PURE__ */ React6.createElement(
1094
1178
  "button",
@@ -1130,6 +1214,14 @@ var ControlPanel = ({
1130
1214
  title: isLooping ? "\u5355\u66F2\u5FAA\u73AF\uFF1A\u5F00\u542F" : "\u5355\u66F2\u5FAA\u73AF\uFF1A\u5173\u95ED"
1131
1215
  },
1132
1216
  /* @__PURE__ */ React6.createElement(Repeat1, { size: 20 })
1217
+ ), isCameraManual && onResetCamera && /* @__PURE__ */ React6.createElement(
1218
+ "button",
1219
+ {
1220
+ onClick: onResetCamera,
1221
+ className: "rounded-full p-2 bg-blue-500/30 text-blue-400 hover:bg-blue-500/50 hover:text-blue-300 transition-all animate-in zoom-in duration-300",
1222
+ title: "\u6062\u590D\u521D\u59CB\u89C6\u89D2"
1223
+ },
1224
+ /* @__PURE__ */ React6.createElement(Camera, { size: 20 })
1133
1225
  ), onToggleAxes && /* @__PURE__ */ React6.createElement(
1134
1226
  "button",
1135
1227
  {
@@ -1536,6 +1628,7 @@ var MMDPlaylist = ({
1536
1628
  const [isListLooping, setIsListLooping] = useState(loop);
1537
1629
  const [showPlaylist, setShowPlaylist] = useState(false);
1538
1630
  const [isTransitioning, setIsTransitioning] = useState(false);
1631
+ const [isCameraManual, setIsCameraManual] = useState(false);
1539
1632
  const playerRef = useRef(null);
1540
1633
  const containerRef = useRef(null);
1541
1634
  const preloadedRef = useRef(/* @__PURE__ */ new Set());
@@ -1687,7 +1780,7 @@ var MMDPlaylist = ({
1687
1780
  key: currentNode.id,
1688
1781
  ref: playerRef,
1689
1782
  resources: currentNode.resources,
1690
- stage,
1783
+ stage: { ...stage, ...currentNode.stage },
1691
1784
  autoPlay: autoPlay && currentIndex === 0,
1692
1785
  loop: isLooping,
1693
1786
  showAxes,
@@ -1700,6 +1793,7 @@ var MMDPlaylist = ({
1700
1793
  },
1701
1794
  onPlay: () => setIsPlaying(true),
1702
1795
  onPause: () => setIsPlaying(false),
1796
+ onCameraChange: (isManual) => setIsCameraManual(isManual),
1703
1797
  onEnded: handleEnded,
1704
1798
  onError
1705
1799
  }
@@ -1715,6 +1809,7 @@ var MMDPlaylist = ({
1715
1809
  isFullscreen,
1716
1810
  isLooping,
1717
1811
  isListLooping,
1812
+ isCameraManual,
1718
1813
  showSettings: true,
1719
1814
  showAxes,
1720
1815
  showPrevNext,
@@ -1727,7 +1822,11 @@ var MMDPlaylist = ({
1727
1822
  onToggleLoop: () => setIsLooping(!isLooping),
1728
1823
  onToggleListLoop: () => setIsListLooping(!isListLooping),
1729
1824
  onToggleAxes: () => setShowAxes(!showAxes),
1730
- onOpenSettings: () => setShowPlaylist(!showPlaylist)
1825
+ onOpenSettings: () => setShowPlaylist(!showPlaylist),
1826
+ onResetCamera: () => {
1827
+ playerRef.current?.resetCamera();
1828
+ setIsCameraManual(false);
1829
+ }
1731
1830
  }
1732
1831
  )
1733
1832
  ), showPlaylist && /* @__PURE__ */ React6.createElement("div", { className: "absolute inset-0 z-20 flex items-end bg-black/80 backdrop-blur-sm" }, /* @__PURE__ */ React6.createElement("div", { className: "w-full max-h-[60vh] overflow-y-auto bg-gray-900/95 rounded-t-xl" }, /* @__PURE__ */ React6.createElement("div", { className: "sticky top-0 flex items-center justify-between bg-gray-800 px-4 py-3 border-b border-gray-700" }, /* @__PURE__ */ React6.createElement("div", null, /* @__PURE__ */ React6.createElement("h3", { className: "text-white font-semibold" }, playlist.name), /* @__PURE__ */ React6.createElement("p", { className: "text-xs text-gray-400 mt-0.5" }, "\u5171 ", nodes.length, " \u4E2A\u8282\u70B9")), /* @__PURE__ */ React6.createElement(
@@ -1829,6 +1928,8 @@ var DialogueBox = ({
1829
1928
  onToggleAuto,
1830
1929
  onOpenHistory,
1831
1930
  onSkip,
1931
+ onResetCamera,
1932
+ isCameraManual = false,
1832
1933
  showControls = true,
1833
1934
  showSkipButton = true,
1834
1935
  showAutoButton = true,
@@ -1992,6 +2093,21 @@ var DialogueBox = ({
1992
2093
  title: "\u5386\u53F2\u8BB0\u5F55"
1993
2094
  },
1994
2095
  "\u{1F4DC} \u5386\u53F2"
2096
+ ), isCameraManual && /* @__PURE__ */ React6.createElement(
2097
+ "button",
2098
+ {
2099
+ onClick: (e) => {
2100
+ e.stopPropagation();
2101
+ onResetCamera?.();
2102
+ },
2103
+ className: "px-4 py-2 text-xs rounded-xl text-white font-medium hover:text-white transition-all backdrop-blur-lg border border-blue-400/50 hover:border-blue-400 hover:scale-105 active:scale-95 shadow-lg animate-in zoom-in duration-300",
2104
+ style: {
2105
+ background: "linear-gradient(135deg, rgba(59, 130, 246, 0.3), rgba(37, 99, 235, 0.2))",
2106
+ boxShadow: "0 4px 16px rgba(59, 130, 246, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.2)"
2107
+ },
2108
+ title: "\u6062\u590D\u521D\u59CB\u89C6\u89D2"
2109
+ },
2110
+ "\u{1F3A5} \u6062\u590D\u89C6\u89D2"
1995
2111
  ), showAutoButton && /* @__PURE__ */ React6.createElement(
1996
2112
  "button",
1997
2113
  {
@@ -2630,6 +2746,50 @@ var SkipConfirmDialog = ({
2630
2746
  }
2631
2747
  return createPortal(content, portalContainer);
2632
2748
  };
2749
+ var ChoiceMenu = ({
2750
+ choices,
2751
+ onSelect,
2752
+ theme
2753
+ }) => {
2754
+ const [isMounted, setIsMounted] = React6.useState(false);
2755
+ React6.useEffect(() => {
2756
+ setIsMounted(true);
2757
+ }, []);
2758
+ if (!isMounted) return null;
2759
+ const content = /* @__PURE__ */ React6.createElement(
2760
+ "div",
2761
+ {
2762
+ className: "fixed inset-0 flex flex-col items-center justify-center bg-black/20 backdrop-blur-sm pointer-events-auto transition-all animate-in fade-in duration-500",
2763
+ style: { zIndex: 1e6 }
2764
+ },
2765
+ /* @__PURE__ */ React6.createElement("div", { className: "flex flex-col gap-4 w-full max-w-md px-6" }, choices.map((choice, index) => /* @__PURE__ */ React6.createElement(
2766
+ "button",
2767
+ {
2768
+ key: index,
2769
+ onClick: () => onSelect(choice),
2770
+ className: "w-full py-4 px-8 rounded-2xl text-white font-bold text-lg transition-all border border-white/20 shadow-xl hover:scale-105 active:scale-95 group relative overflow-hidden",
2771
+ style: {
2772
+ background: `linear-gradient(135deg,
2773
+ rgba(255, 255, 255, 0.2) 0%,
2774
+ rgba(255, 255, 255, 0.1) 100%)`,
2775
+ backdropFilter: "blur(32px) saturate(200%)",
2776
+ WebkitBackdropFilter: "blur(32px) saturate(200%)"
2777
+ }
2778
+ },
2779
+ /* @__PURE__ */ React6.createElement("div", { className: "absolute inset-0 bg-white/10 opacity-0 group-hover:opacity-100 transition-opacity" }),
2780
+ /* @__PURE__ */ React6.createElement("div", { className: "absolute inset-0 bg-gradient-to-r from-transparent via-white/5 to-transparent -translate-x-full group-hover:translate-x-full transition-transform duration-1000 ease-in-out" }),
2781
+ /* @__PURE__ */ React6.createElement("span", { className: "relative z-10 drop-shadow-md" }, choice.text)
2782
+ )))
2783
+ );
2784
+ let portalContainer = document.getElementById("dialogue-portal-root");
2785
+ if (!portalContainer) {
2786
+ portalContainer = document.createElement("div");
2787
+ portalContainer.id = "dialogue-portal-root";
2788
+ portalContainer.style.cssText = "position: fixed; inset: 0; pointer-events: none; z-index: 999999;";
2789
+ document.body.appendChild(portalContainer);
2790
+ }
2791
+ return createPortal(content, portalContainer);
2792
+ };
2633
2793
 
2634
2794
  // src/mmd/visual-novel/MMDVisualNovel.tsx
2635
2795
  var MMDVisualNovel = forwardRef(
@@ -2665,12 +2825,15 @@ var MMDVisualNovel = forwardRef(
2665
2825
  const [isStarted, setIsStarted] = useState(autoStart);
2666
2826
  const [isVmdFinished, setIsVmdFinished] = useState(false);
2667
2827
  const [pendingNodeIndex, setPendingNodeIndex] = useState(null);
2828
+ const [showChoices, setShowChoices] = useState(false);
2829
+ const [isCameraManual, setIsCameraManual] = useState(false);
2668
2830
  const playerRef = useRef(null);
2669
2831
  const containerRef = useRef(null);
2670
2832
  const autoTimerRef = useRef(null);
2671
2833
  const typingCompleteRef = useRef(false);
2672
2834
  const isStartedRef = useRef(autoStart);
2673
2835
  const lastAnimationTimeRef = useRef(0);
2836
+ const isVmdFinishedRef = useRef(false);
2674
2837
  const currentNode = nodes[currentNodeIndex];
2675
2838
  const currentDialogue = currentNode?.dialogues[currentDialogueIndex] || null;
2676
2839
  const addToHistory = useCallback((dialogue, nodeIndex, dialogueIndex) => {
@@ -2698,6 +2861,8 @@ var MMDVisualNovel = forwardRef(
2698
2861
  addToHistory(nextDialogue, currentNodeIndex, nextDialogueIndex);
2699
2862
  onDialogueChange?.(nextDialogue, nextDialogueIndex, currentNodeIndex);
2700
2863
  typingCompleteRef.current = false;
2864
+ } else if (currentNode.choices && currentNode.choices.length > 0) {
2865
+ setShowChoices(true);
2701
2866
  } else {
2702
2867
  const nextNodeIndex = currentNodeIndex + 1;
2703
2868
  if (nextNodeIndex < nodes.length) {
@@ -2716,7 +2881,7 @@ var MMDVisualNovel = forwardRef(
2716
2881
  const node = nodes[nodeIndex];
2717
2882
  if (!node) return;
2718
2883
  const currentResources = nodes[currentNodeIndex]?.resources;
2719
- if (!force && currentResources?.motionPath && !isVmdFinished) {
2884
+ if (!force && currentResources?.motionPath && !isVmdFinishedRef.current) {
2720
2885
  console.log("[MMDVisualNovel] VMD not finished, showing confirmation");
2721
2886
  setPendingNodeIndex(nodeIndex);
2722
2887
  return;
@@ -2726,7 +2891,9 @@ var MMDVisualNovel = forwardRef(
2726
2891
  setIsLoading(true);
2727
2892
  setIsAnimationPlaying(false);
2728
2893
  setIsVmdFinished(false);
2894
+ isVmdFinishedRef.current = false;
2729
2895
  setPendingNodeIndex(null);
2896
+ setShowChoices(false);
2730
2897
  lastAnimationTimeRef.current = 0;
2731
2898
  setTimeout(() => {
2732
2899
  setCurrentNodeIndex(nodeIndex);
@@ -2794,6 +2961,10 @@ var MMDVisualNovel = forwardRef(
2794
2961
  setIsAutoMode((prev) => !prev);
2795
2962
  }, []);
2796
2963
  const handleSkip = useCallback(() => {
2964
+ if (currentNode?.choices && currentNode.choices.length > 0) {
2965
+ setShowChoices(true);
2966
+ return;
2967
+ }
2797
2968
  const nextNodeIndex = currentNodeIndex + 1;
2798
2969
  if (nextNodeIndex < nodes.length) {
2799
2970
  goToNode(nextNodeIndex);
@@ -2867,7 +3038,7 @@ var MMDVisualNovel = forwardRef(
2867
3038
  key: currentNode.id,
2868
3039
  ref: playerRef,
2869
3040
  resources: currentNode.resources,
2870
- stage,
3041
+ stage: { ...stage, ...currentNode.stage },
2871
3042
  autoPlay: isStarted,
2872
3043
  loop: currentNode.loopAnimation === true,
2873
3044
  mobileOptimization,
@@ -2886,9 +3057,13 @@ var MMDVisualNovel = forwardRef(
2886
3057
  setIsAnimationPlaying(true);
2887
3058
  },
2888
3059
  onTimeUpdate: (time) => {
2889
- if (time < lastAnimationTimeRef.current && lastAnimationTimeRef.current > 0) {
2890
- if (!isVmdFinished) {
2891
- console.log("[MMDVisualNovel] VMD loop detected, marking as finished");
3060
+ const duration = playerRef.current?.getDuration() || 0;
3061
+ const isNearEnd = duration > 0 && time > duration * 0.98;
3062
+ const isLooped = time < lastAnimationTimeRef.current && lastAnimationTimeRef.current > 0;
3063
+ if (isNearEnd || isLooped) {
3064
+ if (!isVmdFinishedRef.current) {
3065
+ console.log("[MMDVisualNovel] VMD finished/looped, marking as finished");
3066
+ isVmdFinishedRef.current = true;
2892
3067
  setIsVmdFinished(true);
2893
3068
  }
2894
3069
  }
@@ -2896,8 +3071,12 @@ var MMDVisualNovel = forwardRef(
2896
3071
  },
2897
3072
  onEnded: () => {
2898
3073
  console.log("[MMDVisualNovel] VMD ended, marking as finished");
3074
+ isVmdFinishedRef.current = true;
2899
3075
  setIsVmdFinished(true);
2900
3076
  },
3077
+ onCameraChange: (isManual) => {
3078
+ setIsCameraManual(isManual);
3079
+ },
2901
3080
  onError
2902
3081
  }
2903
3082
  )
@@ -2924,12 +3103,13 @@ var MMDVisualNovel = forwardRef(
2924
3103
  }
2925
3104
  ),
2926
3105
  (() => {
2927
- const shouldShow = isStarted && isAnimationPlaying && currentDialogue && !showHistory;
3106
+ const shouldShow = isStarted && isAnimationPlaying && currentDialogue && !showHistory && !showChoices;
2928
3107
  console.log("[MMDVisualNovel] DialogueBox render condition:", {
2929
3108
  isStarted,
2930
3109
  isAnimationPlaying,
2931
3110
  hasDialogue: !!currentDialogue,
2932
3111
  showHistory,
3112
+ showChoices,
2933
3113
  shouldShow,
2934
3114
  dialogue: currentDialogue
2935
3115
  });
@@ -2947,6 +3127,11 @@ var MMDVisualNovel = forwardRef(
2947
3127
  onToggleAuto: toggleAutoMode,
2948
3128
  onOpenHistory: () => setShowHistory(true),
2949
3129
  onSkip: handleSkip,
3130
+ onResetCamera: () => {
3131
+ playerRef.current?.resetCamera();
3132
+ setIsCameraManual(false);
3133
+ },
3134
+ isCameraManual,
2950
3135
  showControls: true,
2951
3136
  showSkipButton,
2952
3137
  showAutoButton,
@@ -2967,6 +3152,22 @@ var MMDVisualNovel = forwardRef(
2967
3152
  }
2968
3153
  }
2969
3154
  ),
3155
+ showChoices && currentNode.choices && /* @__PURE__ */ React6.createElement(
3156
+ ChoiceMenu,
3157
+ {
3158
+ choices: currentNode.choices,
3159
+ theme: dialogueTheme,
3160
+ onSelect: (choice) => {
3161
+ choice.onSelect?.();
3162
+ if (choice.nextNodeIndex === currentNodeIndex) {
3163
+ goToDialogue(choice.nextDialogueIndex || 0);
3164
+ setShowChoices(false);
3165
+ } else {
3166
+ goToNode(choice.nextNodeIndex, true);
3167
+ }
3168
+ }
3169
+ }
3170
+ ),
2970
3171
  showHistory && /* @__PURE__ */ React6.createElement(
2971
3172
  HistoryPanel,
2972
3173
  {
@@ -2984,12 +3185,14 @@ var MusicControls = ({
2984
3185
  currentTime,
2985
3186
  duration,
2986
3187
  loopMode,
3188
+ isCameraManual = false,
2987
3189
  onPlayPause,
2988
3190
  onNext,
2989
3191
  onPrevious,
2990
3192
  onSeek,
2991
3193
  onToggleLoop,
2992
3194
  onTogglePlaylist,
3195
+ onResetCamera,
2993
3196
  className = ""
2994
3197
  }) => {
2995
3198
  const formatTime = (seconds) => {
@@ -3041,7 +3244,15 @@ var MusicControls = ({
3041
3244
  className: "text-white/80 hover:text-white transition-colors"
3042
3245
  },
3043
3246
  /* @__PURE__ */ React6.createElement(SkipForward, { className: "w-6 h-6 fill-current" })
3044
- )), /* @__PURE__ */ React6.createElement("div", { className: "flex items-center gap-4 w-32 justify-end" }, /* @__PURE__ */ React6.createElement(
3247
+ )), /* @__PURE__ */ React6.createElement("div", { className: "flex items-center gap-4 w-32 justify-end" }, isCameraManual && /* @__PURE__ */ React6.createElement(
3248
+ "button",
3249
+ {
3250
+ onClick: onResetCamera,
3251
+ className: "text-blue-400 hover:text-blue-300 transition-colors animate-in zoom-in duration-300",
3252
+ title: "\u6062\u590D\u521D\u59CB\u89C6\u89D2"
3253
+ },
3254
+ /* @__PURE__ */ React6.createElement(Camera, { className: "w-5 h-5" })
3255
+ ), /* @__PURE__ */ React6.createElement(
3045
3256
  "button",
3046
3257
  {
3047
3258
  onClick: onToggleLoop,
@@ -3158,6 +3369,7 @@ var MMDMusicPlayer = forwardRef(
3158
3369
  const [loopMode, setLoopMode] = useState(defaultLoopMode);
3159
3370
  const [showPlaylist, setShowPlaylist] = useState(false);
3160
3371
  const [isUIVisible, setIsUIVisible] = useState(true);
3372
+ const [isCameraManual, setIsCameraManual] = useState(false);
3161
3373
  const playerRef = useRef(null);
3162
3374
  const containerRef = useRef(null);
3163
3375
  const isStartedRef = useRef(autoPlay);
@@ -3291,7 +3503,7 @@ var MMDMusicPlayer = forwardRef(
3291
3503
  key: currentTrack.id,
3292
3504
  ref: playerRef,
3293
3505
  resources: currentTrack.resources,
3294
- stage,
3506
+ stage: { ...stage, ...currentTrack.stage },
3295
3507
  autoPlay: isStartedRef.current,
3296
3508
  loop: loopMode === "single",
3297
3509
  mobileOptimization,
@@ -3311,6 +3523,9 @@ var MMDMusicPlayer = forwardRef(
3311
3523
  setIsPlaying(false);
3312
3524
  onPlayPause?.(false);
3313
3525
  },
3526
+ onCameraChange: (isManual) => {
3527
+ setIsCameraManual(isManual);
3528
+ },
3314
3529
  onTimeUpdate: handleTimeUpdate,
3315
3530
  onEnded: handleEnded,
3316
3531
  onError
@@ -3331,10 +3546,15 @@ var MMDMusicPlayer = forwardRef(
3331
3546
  currentTime,
3332
3547
  duration,
3333
3548
  loopMode,
3549
+ isCameraManual,
3334
3550
  onPlayPause: () => isPlaying ? playerRef.current?.pause() : playerRef.current?.play(),
3335
3551
  onNext: next,
3336
3552
  onPrevious: previous,
3337
3553
  onSeek: (time) => playerRef.current?.seek(time),
3554
+ onResetCamera: () => {
3555
+ playerRef.current?.resetCamera();
3556
+ setIsCameraManual(false);
3557
+ },
3338
3558
  onToggleLoop: () => {
3339
3559
  const modes = ["list", "single", "shuffle"];
3340
3560
  const nextMode = modes[(modes.indexOf(loopMode) + 1) % modes.length];