@luna-editor/engine 0.5.0 → 0.5.2

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.
@@ -10,115 +10,137 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  import { useEffect, useRef, useState } from "react";
11
11
  // グローバルなフォントキャッシュ(ページ内で共有)
12
12
  const fontCache = new Set();
13
+ /**
14
+ * Googleフォントの<link>タグを挿入し、CSSの読み込み完了を待つ
15
+ */
16
+ function loadGoogleFontCSS(fontFamily) {
17
+ const existingLink = document.querySelector(`link[data-font-family="${fontFamily}"]`);
18
+ if (existingLink) {
19
+ return Promise.resolve();
20
+ }
21
+ const link = document.createElement("link");
22
+ link.rel = "stylesheet";
23
+ // display=blockでフォント読み込みまでテキストを非表示にする(FOUTを防止)
24
+ link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(fontFamily)}:wght@400;700&display=block`;
25
+ link.setAttribute("data-font-family", fontFamily);
26
+ document.head.appendChild(link);
27
+ return new Promise((resolve) => {
28
+ link.onload = () => resolve();
29
+ link.onerror = () => {
30
+ console.warn(`Failed to load Google font CSS: ${fontFamily}`);
31
+ resolve();
32
+ };
33
+ });
34
+ }
35
+ /**
36
+ * Googleフォントのフォントデータを事前にダウンロード
37
+ * 400/700両方のウェイトをプリロードする
38
+ */
39
+ function preloadGoogleFont(fontFamily, textToPreload) {
40
+ return __awaiter(this, void 0, void 0, function* () {
41
+ try {
42
+ // 400と700両方のウェイトをプリロード
43
+ yield Promise.all([
44
+ document.fonts.load(`400 16px "${fontFamily}"`, textToPreload),
45
+ document.fonts.load(`700 16px "${fontFamily}"`, textToPreload),
46
+ ]);
47
+ }
48
+ catch (error) {
49
+ console.warn(`Failed to preload Google font: ${fontFamily}`, error);
50
+ }
51
+ });
52
+ }
53
+ /**
54
+ * カスタムフォントをFontFace APIで読み込み
55
+ */
56
+ function loadCustomFont(fontFamily, url, fontName) {
57
+ return __awaiter(this, void 0, void 0, function* () {
58
+ try {
59
+ // 既にdocument.fontsに登録されているかチェック
60
+ let fontExists = false;
61
+ for (const existingFont of document.fonts) {
62
+ if (existingFont.family === fontFamily) {
63
+ fontExists = true;
64
+ break;
65
+ }
66
+ }
67
+ if (!fontExists) {
68
+ const fontFace = new FontFace(fontFamily, `url(${url})`);
69
+ yield fontFace.load();
70
+ document.fonts.add(fontFace);
71
+ }
72
+ }
73
+ catch (error) {
74
+ console.warn(`Failed to load custom font: ${fontName}`, error);
75
+ }
76
+ });
77
+ }
13
78
  /**
14
79
  * フォントを読み込むカスタムフック
15
80
  * - Googleフォント: <link>タグで読み込み + document.fonts.load()でプリロード
16
81
  * - カスタムフォント: FontFace APIで読み込み
17
- * - すべてのフォントがプリロード&キャッシュされてからisLoaded=trueになる
82
+ * - すべてのフォントが完全にダウンロード&キャッシュされてからisLoaded=trueになる
83
+ * - 再生開始前に全フォントを一括ダウンロードし、再生中のフォントスワップを防止する
84
+ * - preloadText: シナリオ内の全テキストを渡すと、使用される全ての文字でフォントをプリロード
18
85
  */
19
- export function useFontLoader(fonts) {
86
+ export function useFontLoader(fonts, preloadText) {
20
87
  const [isLoaded, setIsLoaded] = useState(false);
21
88
  const [loadedFonts, setLoadedFonts] = useState([]);
22
- const loadingRef = useRef(false);
89
+ // 現在のロード処理をキャンセルするための世代カウンター
90
+ const generationRef = useRef(0);
23
91
  useEffect(() => {
24
92
  if (!fonts || fonts.length === 0) {
25
93
  setIsLoaded(true);
26
94
  return;
27
95
  }
28
- // 既に読み込み中の場合はスキップ
29
- if (loadingRef.current)
30
- return;
31
- loadingRef.current = true;
96
+ // 新しいロード処理を開始する際、前回の結果を無効化
97
+ const generation = ++generationRef.current;
98
+ setIsLoaded(false);
32
99
  const loadFonts = () => __awaiter(this, void 0, void 0, function* () {
100
+ var _a;
33
101
  const loadedFontFamilies = [];
34
- const fontLoadPromises = [];
35
- for (const font of fonts) {
36
- // キャッシュにあればスキップ
37
- if (fontCache.has(font.fontFamily)) {
102
+ const textToPreload = preloadText || "あいうえおABCabc123";
103
+ // すべてのGoogle FontsのCSS読み込みを並列で開始
104
+ const googleFonts = fonts.filter((f) => f.type === "google" && !fontCache.has(f.fontFamily));
105
+ const customFonts = fonts.filter((f) => f.type === "custom" && f.url && !fontCache.has(f.fontFamily));
106
+ const cachedFonts = fonts.filter((f) => fontCache.has(f.fontFamily));
107
+ // キャッシュ済みフォントは即座に追加
108
+ for (const font of cachedFonts) {
109
+ loadedFontFamilies.push(font.fontFamily);
110
+ }
111
+ // Google FontsのCSS読み込みを並列実行
112
+ yield Promise.all(googleFonts.map((font) => loadGoogleFontCSS(font.fontFamily)));
113
+ // キャンセルチェック
114
+ if (generation !== generationRef.current)
115
+ return;
116
+ // Google Fontsのフォントデータプリロード + カスタムフォント読み込みを並列実行
117
+ const allFontPromises = [];
118
+ for (const font of googleFonts) {
119
+ allFontPromises.push(preloadGoogleFont(font.fontFamily, textToPreload).then(() => {
120
+ fontCache.add(font.fontFamily);
38
121
  loadedFontFamilies.push(font.fontFamily);
39
- continue;
40
- }
41
- try {
42
- if (font.type === "google") {
43
- // Googleフォントを<link>タグで読み込み
44
- const existingLink = document.querySelector(`link[data-font-family="${font.fontFamily}"]`);
45
- if (!existingLink) {
46
- const link = document.createElement("link");
47
- link.rel = "stylesheet";
48
- link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(font.fontFamily)}:wght@400;700&display=swap`;
49
- link.setAttribute("data-font-family", font.fontFamily);
50
- document.head.appendChild(link);
51
- // CSSファイルの読み込み完了を待つ
52
- yield new Promise((resolve) => {
53
- link.onload = () => resolve();
54
- link.onerror = () => {
55
- console.warn(`Failed to load Google font CSS: ${font.fontFamily}`);
56
- resolve();
57
- };
58
- });
59
- }
60
- // フォントの実際の読み込み(プリロード)を開始
61
- // document.fonts.load()でフォントデータを事前にダウンロード
62
- const preloadPromise = document.fonts
63
- .load(`16px "${font.fontFamily}"`)
64
- .then(() => {
65
- fontCache.add(font.fontFamily);
66
- loadedFontFamilies.push(font.fontFamily);
67
- })
68
- .catch((error) => {
69
- console.warn(`Failed to preload Google font: ${font.fontFamily}`, error);
70
- // エラーでもキャッシュに追加(再試行防止)
71
- fontCache.add(font.fontFamily);
72
- loadedFontFamilies.push(font.fontFamily);
73
- });
74
- fontLoadPromises.push(preloadPromise);
75
- }
76
- else if (font.type === "custom" && font.url) {
77
- // カスタムフォントをFontFace APIで読み込み
78
- const loadCustomFont = () => __awaiter(this, void 0, void 0, function* () {
79
- try {
80
- // 既にdocument.fontsに登録されているかチェック
81
- let fontExists = false;
82
- for (const existingFont of document.fonts) {
83
- if (existingFont.family === font.fontFamily) {
84
- fontExists = true;
85
- break;
86
- }
87
- }
88
- if (!fontExists) {
89
- const fontFace = new FontFace(font.fontFamily, `url(${font.url})`);
90
- // フォントデータをダウンロード&パース
91
- yield fontFace.load();
92
- // document.fontsに追加(キャッシュとして機能)
93
- document.fonts.add(fontFace);
94
- }
95
- fontCache.add(font.fontFamily);
96
- loadedFontFamilies.push(font.fontFamily);
97
- }
98
- catch (error) {
99
- console.warn(`Failed to load custom font: ${font.name}`, error);
100
- // エラーでもキャッシュに追加(再試行防止)
101
- fontCache.add(font.fontFamily);
102
- loadedFontFamilies.push(font.fontFamily);
103
- }
104
- });
105
- fontLoadPromises.push(loadCustomFont());
106
- }
107
- }
108
- catch (error) {
109
- console.warn(`Failed to load font: ${font.name}`, error);
110
- }
122
+ }));
123
+ }
124
+ for (const font of customFonts) {
125
+ allFontPromises.push(loadCustomFont(font.fontFamily, (_a = font.url) !== null && _a !== void 0 ? _a : "", font.name).then(() => {
126
+ fontCache.add(font.fontFamily);
127
+ loadedFontFamilies.push(font.fontFamily);
128
+ }));
111
129
  }
112
- // すべてのフォント読み込みを並列で待つ
113
- yield Promise.all(fontLoadPromises);
130
+ yield Promise.all(allFontPromises);
131
+ // キャンセルチェック
132
+ if (generation !== generationRef.current)
133
+ return;
114
134
  // document.fonts.readyで全フォントの準備完了を確認
115
135
  yield document.fonts.ready;
136
+ // キャンセルチェック
137
+ if (generation !== generationRef.current)
138
+ return;
116
139
  setLoadedFonts(loadedFontFamilies);
117
140
  setIsLoaded(true);
118
- loadingRef.current = false;
119
141
  });
120
142
  loadFonts();
121
- }, [fonts]);
143
+ }, [fonts, preloadText]);
122
144
  return { isLoaded, loadedFonts };
123
145
  }
124
146
  /**
@@ -139,6 +161,23 @@ export function getFontFamilyStyle(selectedFontFamily, fonts) {
139
161
  // 選択されていない、または見つからない場合はデフォルト(最初の)フォントを使用
140
162
  return `"${fonts[0].fontFamily}", sans-serif`;
141
163
  }
164
+ /**
165
+ * UIフォントファミリーに基づいてCSSのfont-family値を生成
166
+ * メニュー、ボタン、ラベルなどのUI要素に使用
167
+ */
168
+ export function getUIFontFamilyStyle(selectedUIFontFamily, fonts) {
169
+ if (!fonts || fonts.length === 0) {
170
+ return "sans-serif";
171
+ }
172
+ if (selectedUIFontFamily) {
173
+ const selectedFont = fonts.find((f) => f.fontFamily === selectedUIFontFamily);
174
+ if (selectedFont) {
175
+ return `"${selectedUIFontFamily}", sans-serif`;
176
+ }
177
+ }
178
+ // 選択されていない、または見つからない場合はデフォルト(最初の)フォントを使用
179
+ return `"${fonts[0].fontFamily}", sans-serif`;
180
+ }
142
181
  /**
143
182
  * フォントがキャッシュに存在するかチェック
144
183
  */
@@ -20,6 +20,9 @@ const calculateDisplayedCharacters = (blocks, currentIndex) => {
20
20
  positionY: char.positionY,
21
21
  zIndex: char.zIndex,
22
22
  scale: char.scale,
23
+ cropLeft: char.cropLeft,
24
+ cropRight: char.cropRight,
25
+ cropFade: char.cropFade,
23
26
  object: char.object,
24
27
  entityState: char.entityState,
25
28
  // レイヤー機能用
@@ -37,6 +37,6 @@ export function useUIVisibility(type) {
37
37
  // UI 状態の変更を監視
38
38
  const unsubscribe = pluginManager.subscribeUIVisibility(type, setIsVisible);
39
39
  return unsubscribe;
40
- }, [pluginManager, type]);
40
+ }, [type]);
41
41
  return isVisible;
42
42
  }
@@ -40,13 +40,36 @@ export const useSoundPlayer = ({ soundBlocks, isFirstRenderComplete, muteAudio =
40
40
  isBGM,
41
41
  isVoice,
42
42
  ]);
43
- // BGMを停止
43
+ const bgmFadeIntervalRef = useRef(null);
44
+ // BGMをフェードアウトして停止
44
45
  const stopBGM = useCallback(() => {
45
- if (bgmRef.current) {
46
- bgmRef.current.audio.pause();
47
- bgmRef.current.audio.currentTime = 0;
48
- bgmRef.current = null;
46
+ if (!bgmRef.current)
47
+ return;
48
+ // 既にフェード中なら前回のをクリア
49
+ if (bgmFadeIntervalRef.current) {
50
+ clearInterval(bgmFadeIntervalRef.current);
49
51
  }
52
+ const audio = bgmRef.current.audio;
53
+ const fadeDuration = 1000; // 1秒フェードアウト
54
+ const fadeSteps = 20;
55
+ const stepTime = fadeDuration / fadeSteps;
56
+ const volumeStep = audio.volume / fadeSteps;
57
+ let stepsRemaining = fadeSteps;
58
+ bgmFadeIntervalRef.current = setInterval(() => {
59
+ stepsRemaining--;
60
+ if (stepsRemaining <= 0) {
61
+ if (bgmFadeIntervalRef.current) {
62
+ clearInterval(bgmFadeIntervalRef.current);
63
+ bgmFadeIntervalRef.current = null;
64
+ }
65
+ audio.pause();
66
+ audio.currentTime = 0;
67
+ bgmRef.current = null;
68
+ }
69
+ else {
70
+ audio.volume = Math.max(0, volumeStep * stepsRemaining);
71
+ }
72
+ }, stepTime);
50
73
  }, []);
51
74
  // 特定のSEを停止
52
75
  const stopSE = useCallback((soundId) => {
@@ -185,6 +208,11 @@ export const useSoundPlayer = ({ soundBlocks, isFirstRenderComplete, muteAudio =
185
208
  // クリーンアップ(直接refにアクセスして依存配列の問題を回避)
186
209
  useEffect(() => {
187
210
  return () => {
211
+ // フェードintervalをクリア
212
+ if (bgmFadeIntervalRef.current) {
213
+ clearInterval(bgmFadeIntervalRef.current);
214
+ bgmFadeIntervalRef.current = null;
215
+ }
188
216
  // BGMを停止
189
217
  if (bgmRef.current) {
190
218
  bgmRef.current.audio.pause();
package/dist/index.d.ts CHANGED
@@ -5,9 +5,10 @@ export { aspectRatio, BasisHeight, BasisWidth } from "./constants/screen-size";
5
5
  export type { AudioSettings } from "./contexts/AudioContext";
6
6
  export { useAudioSettings } from "./contexts/AudioContext";
7
7
  export { useDataAPI } from "./contexts/DataContext";
8
+ export { getFontFamilyStyle, getUIFontFamilyStyle, } from "./hooks/useFontLoader";
8
9
  export { setGlobalUIAPI, usePluginAPI, useUIVisibility, } from "./hooks/usePluginAPI";
9
10
  export { useScreenScale, useScreenSize, useToPixel, } from "./hooks/useScreenSize";
10
11
  export { Player } from "./Player";
11
- export type { BacklogData, BlockChangeContext, DataAPI, DataContext, FontsData, PlayerSettingsData, ScenarioPlaybackData, TransitionSource, } from "./sdk";
12
- export { ComponentType, renderTextWithLineBreaks } from "./sdk";
12
+ export type { BacklogData, BlockChangeContext, DataAPI, DataContext, FontsData, LunaPlugin, PlayerSettingsData, PluginAPI, ScenarioPlaybackData, TransitionSource, } from "./sdk";
13
+ export { ComponentType, definePlugin, renderTextWithLineBreaks } from "./sdk";
13
14
  export type { ActionNode, BacklogEntry, Character, DisplayedCharacter, EntityState, FontType, PlayerProps, PlayerSettings, PlayerState, PluginConfig, PublishedScenario, ScenarioBlock, ScenarioBlockAttributeValue, ScenarioBlockCharacter, ScenarioBlockEntityAttributeValue, ScenarioBlockType, WorkFont, WorkSound, } from "./types";
package/dist/index.js CHANGED
@@ -5,7 +5,8 @@ export { OverlayUI } from "./components/OverlayUI";
5
5
  export { aspectRatio, BasisHeight, BasisWidth } from "./constants/screen-size";
6
6
  export { useAudioSettings } from "./contexts/AudioContext";
7
7
  export { useDataAPI } from "./contexts/DataContext";
8
+ export { getFontFamilyStyle, getUIFontFamilyStyle, } from "./hooks/useFontLoader";
8
9
  export { setGlobalUIAPI, usePluginAPI, useUIVisibility, } from "./hooks/usePluginAPI";
9
10
  export { useScreenScale, useScreenSize, useToPixel, } from "./hooks/useScreenSize";
10
11
  export { Player } from "./Player";
11
- export { ComponentType, renderTextWithLineBreaks } from "./sdk";
12
+ export { ComponentType, definePlugin, renderTextWithLineBreaks } from "./sdk";
@@ -62,7 +62,10 @@ export declare class PluginManager {
62
62
  * 既に初期化済みの場合はスキップ
63
63
  */
64
64
  private initializeReactRuntime;
65
- loadPlugin(packageName: string, bundleUrl: string, config?: unknown): Promise<void>;
65
+ loadPlugin(packageName: string, bundleUrl: string | undefined, config?: unknown, assets?: {
66
+ filename: string;
67
+ url: string;
68
+ }[], plugin?: LunaPlugin): Promise<void>;
66
69
  private doLoadPlugin;
67
70
  private loadPluginScript;
68
71
  private createPluginAPI;
@@ -74,6 +77,11 @@ export declare class PluginManager {
74
77
  executeActionNode(nodeType: string, context: unknown): Promise<void>;
75
78
  setPluginEnabled(packageName: string, enabled: boolean): void;
76
79
  cleanup(): void;
80
+ /**
81
+ * React Strict Mode の再マウント時に呼び出す。
82
+ * componentRegistry と plugins をクリアし、プラグインが再ロード・再登録できるようにする。
83
+ */
84
+ resetForRemount(): void;
77
85
  private findPluginByComponentType;
78
86
  getComponent(type: ComponentType): React.ComponentType<{
79
87
  dataAPI?: DataAPI;
@@ -21,6 +21,7 @@ var __rest = (this && this.__rest) || function (s, e) {
21
21
  import React from "react";
22
22
  import { useScreenSizeAtom } from "../atoms/screen-size";
23
23
  import { useDataAPI } from "../contexts/DataContext";
24
+ import { getFontFamilyStyle, getUIFontFamilyStyle, } from "../hooks/useFontLoader";
24
25
  import { usePluginAPI, useUIVisibility } from "../hooks/usePluginAPI";
25
26
  import { useScreenScale, useScreenSize, useToPixel, } from "../hooks/useScreenSize";
26
27
  import { ComponentType, definePlugin, } from "../sdk";
@@ -107,18 +108,32 @@ export class PluginManager {
107
108
  useScreenScale,
108
109
  useScreenSizeAtom,
109
110
  ComponentType,
110
- definePlugin });
111
+ definePlugin,
112
+ getFontFamilyStyle,
113
+ getUIFontFamilyStyle });
111
114
  }
112
- loadPlugin(packageName, bundleUrl, config) {
115
+ loadPlugin(packageName, bundleUrl, config, assets, plugin) {
113
116
  return __awaiter(this, void 0, void 0, function* () {
114
117
  // 開発環境ではキャッシュをバイパスして常に最新コードをロード
115
118
  const isDevelopment = this.isDevelopment();
116
119
  if (isDevelopment) {
117
- // 開発環境では強制リロード(キャッシュをクリア)
120
+ // ロード中の場合はそのPromiseを待って完了させる(二重ロード防止)
121
+ const existingLoadPromise = globalLoadingPlugins.get(packageName);
122
+ if (existingLoadPromise) {
123
+ yield existingLoadPromise;
124
+ // ロード完了後、結果をローカルにコピー
125
+ const loaded = globalLoadedPlugins.get(packageName);
126
+ if (loaded) {
127
+ this.plugins.set(packageName, Object.assign(Object.assign({}, loaded), { config: config !== null && config !== void 0 ? config : loaded.config }));
128
+ for (const [type, component] of loaded.instance.components) {
129
+ this.componentRegistry.set(type, component);
130
+ }
131
+ }
132
+ return;
133
+ }
134
+ // ロード中でなければキャッシュをクリアして再ロード
118
135
  globalLoadedPlugins.delete(packageName);
119
136
  this.plugins.delete(packageName);
120
- // ロード中のPromiseもクリア(再ロードを確実にするため)
121
- globalLoadingPlugins.delete(packageName);
122
137
  }
123
138
  else {
124
139
  // 本番環境のみキャッシュを使用
@@ -160,7 +175,7 @@ export class PluginManager {
160
175
  }
161
176
  }
162
177
  // ロード処理をPromiseとして保存
163
- const loadPromise = this.doLoadPlugin(packageName, bundleUrl, config);
178
+ const loadPromise = this.doLoadPlugin(packageName, bundleUrl, config, assets, plugin);
164
179
  globalLoadingPlugins.set(packageName, loadPromise);
165
180
  try {
166
181
  yield loadPromise;
@@ -171,11 +186,13 @@ export class PluginManager {
171
186
  }
172
187
  });
173
188
  }
174
- doLoadPlugin(packageName, bundleUrl, config) {
189
+ doLoadPlugin(packageName, bundleUrl, config, assets, localPlugin) {
175
190
  return __awaiter(this, void 0, void 0, function* () {
176
191
  try {
177
- // スクリプトタグで動的にプラグインを読み込む
178
- const plugin = yield this.loadPluginScript(bundleUrl, packageName);
192
+ // ローカルプラグインが渡された場合はスクリプトロードをスキップ
193
+ const plugin = localPlugin
194
+ ? localPlugin
195
+ : yield this.loadPluginScript(bundleUrl, packageName);
179
196
  const instance = {
180
197
  actionNodes: new Map(),
181
198
  hooks: {},
@@ -185,6 +202,12 @@ export class PluginManager {
185
202
  components: new Map(),
186
203
  assetUrls: new Map(),
187
204
  };
205
+ // 外部アプリから渡されたアセットURLを事前にキャッシュ
206
+ if (assets) {
207
+ for (const asset of assets) {
208
+ instance.assetUrls.set(asset.filename, asset.url);
209
+ }
210
+ }
188
211
  // プラグインAPIを作成
189
212
  const api = this.createPluginAPI(packageName, instance);
190
213
  // プラグインをセットアップ
@@ -610,16 +633,26 @@ export class PluginManager {
610
633
  preload: (filenames) => {
611
634
  const preloadPromise = (() => __awaiter(this, void 0, void 0, function* () {
612
635
  const promises = filenames.map((filename) => __awaiter(this, void 0, void 0, function* () {
613
- // APIエンドポイントURL
614
- const encodedPackageName = encodeURIComponent(packageName);
615
- const encodedFilename = encodeURIComponent(filename);
616
- const apiUrl = `/api/plugin/${encodedPackageName}/assets/${encodedFilename}`;
617
- // fetchでリダイレクト後の最終URLを取得
636
+ // 既にキャッシュされたURLがあればAPIフェッチをスキップ
637
+ let finalUrl = instance.assetUrls.get(filename);
638
+ if (!finalUrl) {
639
+ // APIエンドポイントURL
640
+ const encodedPackageName = encodeURIComponent(packageName);
641
+ const encodedFilename = encodeURIComponent(filename);
642
+ const apiUrl = `/api/plugin/${encodedPackageName}/assets/${encodedFilename}`;
643
+ // fetchでリダイレクト後の最終URLを取得
644
+ try {
645
+ const response = yield fetch(apiUrl, { method: "HEAD" });
646
+ finalUrl = response.url; // リダイレクト後の最終URL
647
+ // 最終URLをキャッシュに保存
648
+ instance.assetUrls.set(filename, finalUrl);
649
+ }
650
+ catch (error) {
651
+ console.error(`Failed to preload asset: ${filename}`, error);
652
+ return Promise.resolve();
653
+ }
654
+ }
618
655
  try {
619
- const response = yield fetch(apiUrl, { method: "HEAD" });
620
- const finalUrl = response.url; // リダイレクト後の最終URL
621
- // 最終URLをキャッシュに保存
622
- instance.assetUrls.set(filename, finalUrl);
623
656
  // 画像をプリロード
624
657
  if (filename.match(/\.(png|jpg|jpeg|gif|webp)$/i)) {
625
658
  return new Promise((resolve, reject) => {
@@ -642,7 +675,6 @@ export class PluginManager {
642
675
  catch (error) {
643
676
  console.error(`Failed to preload asset: ${filename}`, error);
644
677
  }
645
- return Promise.resolve();
646
678
  }));
647
679
  yield Promise.all(promises);
648
680
  }))();
@@ -943,6 +975,14 @@ export class PluginManager {
943
975
  this.storage.clear();
944
976
  this.componentRegistry.clear();
945
977
  }
978
+ /**
979
+ * React Strict Mode の再マウント時に呼び出す。
980
+ * componentRegistry と plugins をクリアし、プラグインが再ロード・再登録できるようにする。
981
+ */
982
+ resetForRemount() {
983
+ this.componentRegistry.clear();
984
+ this.plugins.clear();
985
+ }
946
986
  // Component registry helper methods
947
987
  findPluginByComponentType(type) {
948
988
  for (const [packageName, plugin] of this.plugins) {
package/dist/sdk.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import React from "react";
2
+ import { useDataAPI } from "./contexts/DataContext";
2
3
  import type { DataAPI, DataContext, DataSubscriber } from "./data-api-types";
3
4
  import type { FacePosition } from "./emotion-effect-types";
4
5
  import type { Character, ConversationBranchState, EntityState, ScenarioBlock, ScenarioBlockType } from "./types";
5
- import { useDataAPI } from "./contexts/DataContext";
6
6
  /**
7
7
  * ブロックオプションの定義
8
8
  * プラグインがシナリオブロックに追加できるカスタムオプションを定義
package/dist/types.d.ts CHANGED
@@ -16,6 +16,7 @@ export interface ScenarioBlock {
16
16
  updatedAt: Date;
17
17
  /** Plugin-defined block options (key-value pairs) */
18
18
  options?: BlockOptions | null;
19
+ characterLayoutMode?: "detailed" | "simple";
19
20
  speaker?: {
20
21
  id: string;
21
22
  name: string;
@@ -100,6 +101,9 @@ export interface ScenarioBlockCharacter {
100
101
  positionY?: number | null;
101
102
  zIndex?: number | null;
102
103
  scale?: number | null;
104
+ cropLeft?: number | null;
105
+ cropRight?: number | null;
106
+ cropFade?: number | null;
103
107
  object: {
104
108
  id: string;
105
109
  name: string;
@@ -113,6 +117,9 @@ export interface ScenarioBlockCharacter {
113
117
  scale?: number;
114
118
  translateY?: number;
115
119
  translateX?: number;
120
+ cropLeft?: number | null;
121
+ cropRight?: number | null;
122
+ cropFade?: number | null;
116
123
  isDefault?: boolean;
117
124
  layers?: EntityStateLayer[];
118
125
  };
@@ -200,6 +207,9 @@ export interface DisplayedCharacter {
200
207
  positionY?: number | null;
201
208
  zIndex?: number | null;
202
209
  scale?: number | null;
210
+ cropLeft?: number | null;
211
+ cropRight?: number | null;
212
+ cropFade?: number | null;
203
213
  object: {
204
214
  id: string;
205
215
  name: string;
@@ -213,6 +223,9 @@ export interface DisplayedCharacter {
213
223
  scale?: number;
214
224
  translateY?: number;
215
225
  translateX?: number;
226
+ cropLeft?: number | null;
227
+ cropRight?: number | null;
228
+ cropFade?: number | null;
216
229
  isDefault?: boolean;
217
230
  layers?: EntityStateLayer[];
218
231
  };
@@ -311,13 +324,27 @@ export interface PlayerSettings {
311
324
  defaultBackgroundFadeDuration?: number;
312
325
  /** 全ての音声をミュートする (デフォルト: false) */
313
326
  muteAudio?: boolean;
314
- /** 選択されたフォントファミリー */
327
+ /** 選択されたフォントファミリー(テキスト用) */
315
328
  selectedFontFamily?: string;
329
+ /** 選択されたUIフォントファミリー(メニュー・ラベル等) */
330
+ selectedUIFontFamily?: string;
331
+ /** 非アクティブキャラクターの明るさ (0-1, デフォルト: 0.8) */
332
+ inactiveCharacterBrightness?: number;
333
+ /** キャラクター間隔(簡易モード用、デフォルト: 0.1) */
334
+ characterSpacing?: number;
316
335
  }
317
336
  export interface PluginConfig {
318
337
  packageName: string;
319
- bundleUrl: string;
338
+ /** リモートプラグインのバンドルURL(plugin未指定時に必須) */
339
+ bundleUrl?: string;
340
+ /** ローカルプラグイン: LunaPluginオブジェクトを直接渡す(bundleUrl不要) */
341
+ plugin?: import("./sdk").LunaPlugin;
320
342
  config?: unknown;
343
+ /** アセットのファイル名→絶対URLマッピング(外部アプリから利用時に必要) */
344
+ assets?: {
345
+ filename: string;
346
+ url: string;
347
+ }[];
321
348
  }
322
349
  /** 作品のサウンドデータ(プラグインからアクセス可能) */
323
350
  export interface WorkSound {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luna-editor/engine",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Luna Editor scenario playback engine",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",