akari-video 1.3.0-beta.3 → 1.3.0-beta.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "1.3.0-beta.3",
3
+ "version": "1.3.0-beta.4",
4
4
  "description": "AKARI Video launcher CLI — start an AI-edited video project from any directory: scaffold, connection check, then hand over to Claude Code (or opencode). AKARI Video を opencode や Claude Code で、どのディレクトリからでも始めるための `akari` ランチャー CLI。接続確認(doctor)→ 未セットアップならプロジェクト雛形を作成 → AI エージェントを起動する。外部 npm 依存ゼロ(Node.js 組み込みモジュールのみ)。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "1.3.0-beta.3",
3
+ "version": "1.3.0-beta.4",
4
4
  "description": "AKARI Video launcher CLI — start an AI-edited video project from any directory: scaffold, connection check, then hand over to Claude Code (or opencode). AKARI Video を opencode や Claude Code で、どのディレクトリからでも始めるための `akari` ランチャー CLI。接続確認(doctor)→ 未セットアップならプロジェクト雛形を作成 → AI エージェントを起動する。外部 npm 依存ゼロ(Node.js 組み込みモジュールのみ)。 [akari-video npm vendor: bin/akari.mjs is reference-only. These CLI entrypoints are not included in the akari-video npm package. Use `akari doctor --json` and run the path reported in `render_cut.path`. Full installations provide it in a monorepo checkout, ~/.akari/app, /Applications/AKARI Video.app/Contents/Resources/packages, or %LOCALAPPDATA%\\Programs\\@akari-videoshell\\resources\\packages.]",
5
5
  "type": "module",
6
6
  "files": [
@@ -43,6 +43,7 @@ window.akari.threeRuntime = (() => {
43
43
  "lights",
44
44
  "animationClip",
45
45
  "materialOverrides",
46
+ "finishes",
46
47
  "shadows",
47
48
  "texts",
48
49
  "physics",
@@ -136,6 +137,56 @@ window.akari.threeRuntime = (() => {
136
137
  const VIDEO_TEXTURE_PATTERN = /^data:video\/|\.(?:mp4|m4v|mov|webm)(?:[?#]|$)/i;
137
138
  const CSS_VAR_REFERENCE_PATTERN = /^var\(\s*(--[\w-]+)\s*\)$/;
138
139
  const CSS_CUSTOM_PROPERTY_PATTERN = /^--[A-Za-z_][A-Za-z0-9_-]*$/;
140
+ const FINISH_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_-]*$/;
141
+ const FINISH_COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
142
+ const FINISH_PROPERTIES = new Set([
143
+ "color", "metalness", "roughness", "clearcoat", "clearcoatRoughness", "emissive",
144
+ ]);
145
+
146
+ function validateFinishes(finishes) {
147
+ const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
148
+ if (!isObject(finishes) || Object.keys(finishes).some((key) => !["var", "default", "options"].includes(key))) {
149
+ throw new TypeError("finishes は var / default / options を指定する object である必要があります");
150
+ }
151
+ if (finishes.var !== undefined
152
+ && (typeof finishes.var !== "string" || !CSS_CUSTOM_PROPERTY_PATTERN.test(finishes.var))) {
153
+ throw new TypeError("finishes.var は -- で始まる CSS カスタムプロパティ名である必要があります");
154
+ }
155
+ if (typeof finishes.default !== "string" || !FINISH_NAME_PATTERN.test(finishes.default)) {
156
+ throw new TypeError("finishes.default は有効な選択名である必要があります");
157
+ }
158
+ if (!isObject(finishes.options) || Object.keys(finishes.options).length === 0) {
159
+ throw new TypeError("finishes.options は非空の object である必要があります");
160
+ }
161
+ for (const [name, materials] of Object.entries(finishes.options)) {
162
+ if (!FINISH_NAME_PATTERN.test(name)) {
163
+ throw new TypeError(`finishes.options の選択名が不正です: ${name}`);
164
+ }
165
+ if (!isObject(materials)) {
166
+ throw new TypeError(`finishes.options.${name} は材質名をキーとする object である必要があります`);
167
+ }
168
+ for (const [materialName, values] of Object.entries(materials)) {
169
+ if (!materialName.trim() || !isObject(values)) {
170
+ throw new TypeError(`finishes.options.${name} の材質指定が不正です: ${materialName}`);
171
+ }
172
+ for (const [key, value] of Object.entries(values)) {
173
+ if (!FINISH_PROPERTIES.has(key)) {
174
+ throw new TypeError(`finishes.options.${name}.${materialName} の未対応キーです: ${key}`);
175
+ }
176
+ if (key === "color" || key === "emissive") {
177
+ if (typeof value !== "string" || !FINISH_COLOR_PATTERN.test(value)) {
178
+ throw new TypeError(`finishes.options.${name}.${materialName}.${key} は 6 桁 hex 色である必要があります`);
179
+ }
180
+ } else if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
181
+ throw new TypeError(`finishes.options.${name}.${materialName}.${key} は 0〜1 の有限数である必要があります`);
182
+ }
183
+ }
184
+ }
185
+ }
186
+ if (!Object.hasOwn(finishes.options, finishes.default)) {
187
+ throw new TypeError("finishes.default は finishes.options 内の選択名である必要があります");
188
+ }
189
+ }
139
190
 
140
191
  function finiteNumber(value, fallback) {
141
192
  const number = Number(value);
@@ -570,6 +621,7 @@ window.akari.threeRuntime = (() => {
570
621
  throw new TypeError(`data-akari-3d-scene の未対応キーです: ${key}`);
571
622
  }
572
623
  }
624
+ if (descriptor.finishes !== undefined) validateFinishes(descriptor.finishes);
573
625
  // texts[] があれば model は任意化される(両方あれば併存。§3.1)
574
626
  const hasTexts = descriptor.texts !== undefined;
575
627
  if (hasTexts) {
@@ -946,6 +998,102 @@ window.akari.threeRuntime = (() => {
946
998
  }));
947
999
  }
948
1000
 
1001
+ function initializeFinishes(instance, root) {
1002
+ if (!instance.finish) return;
1003
+ const materialsByName = new Map();
1004
+ root.traverse((object) => {
1005
+ const materials = Array.isArray(object.material) ? object.material : object.material ? [object.material] : [];
1006
+ for (const material of materials) {
1007
+ if (!materialsByName.has(material.name)) materialsByName.set(material.name, new Set());
1008
+ materialsByName.get(material.name).add(material);
1009
+ }
1010
+ });
1011
+ // materialOverrides 適用後の値を素の値として記録し、finishes は指定キーだけ上書きする。
1012
+ // emissiveMap / emissiveIntensity は記録対象にも上書き対象にも含めず保持する。
1013
+ for (const materials of materialsByName.values()) {
1014
+ for (const material of materials) {
1015
+ instance.finish.base.set(material, {
1016
+ color: material.color?.clone(),
1017
+ metalness: material.metalness,
1018
+ roughness: material.roughness,
1019
+ clearcoat: material.clearcoat,
1020
+ clearcoatRoughness: material.clearcoatRoughness,
1021
+ emissive: material.emissive?.clone(),
1022
+ });
1023
+ }
1024
+ }
1025
+ instance.finish.materialsByName = materialsByName;
1026
+ applyFinish(instance);
1027
+ }
1028
+
1029
+ function applyFinish(instance) {
1030
+ const state = instance.finish;
1031
+ if (!state || !state.materialsByName) return;
1032
+ const { descriptor } = state;
1033
+ const raw = getComputedStyle(instance.canvas).getPropertyValue(descriptor.var ?? "--akari-3d-finish").trim();
1034
+ const requested = raw.replace(/^(['"])(.*)\1$/, "$2");
1035
+ const selected = requested === "" ? descriptor.default
1036
+ : Object.hasOwn(descriptor.options, requested) ? requested : descriptor.default;
1037
+ if (requested !== "" && !Object.hasOwn(descriptor.options, requested)
1038
+ && !state.warnedValues.has(requested)) {
1039
+ state.warnedValues.add(requested);
1040
+ console.warn(`[akari-three] finishes の未知の選択値です: ${requested}。${descriptor.default} を使います`);
1041
+ }
1042
+ if (state.selected === selected) return;
1043
+
1044
+ const oldCoats = new Map();
1045
+ for (const [material, base] of state.base) {
1046
+ oldCoats.set(material, material.clearcoat);
1047
+ if (base.color && material.color) material.color.copy(base.color);
1048
+ if (base.emissive && material.emissive) material.emissive.copy(base.emissive);
1049
+ for (const key of ["metalness", "roughness", "clearcoat", "clearcoatRoughness"]) {
1050
+ if (base[key] !== undefined && key in material) material[key] = base[key];
1051
+ }
1052
+ }
1053
+ const applied = [];
1054
+ for (const [name, values] of Object.entries(descriptor.options[selected])) {
1055
+ const materials = state.materialsByName.get(name);
1056
+ if (!materials?.size) {
1057
+ const warningKey = JSON.stringify([selected, name]);
1058
+ if (!state.warnedMaterials.has(warningKey)) {
1059
+ state.warnedMaterials.add(warningKey);
1060
+ console.warn(`[akari-three] finishes の対象が見つかりません: ${name}(選択: ${selected})`);
1061
+ }
1062
+ applied.push({ material: name, applied: false });
1063
+ continue;
1064
+ }
1065
+ for (const material of materials) {
1066
+ for (const [key, value] of Object.entries(values)) {
1067
+ if (!(key in material) || ((key === "color" || key === "emissive") && !material[key]?.set)) {
1068
+ const warningKey = `${material.uuid}:${key}`;
1069
+ if (!state.warnedProperties.has(warningKey)) {
1070
+ state.warnedProperties.add(warningKey);
1071
+ console.warn(`[akari-three] finishes の ${name}.${key} はこの材質に適用できません`);
1072
+ }
1073
+ continue;
1074
+ }
1075
+ if (key === "color" || key === "emissive") material[key].set(value);
1076
+ else material[key] = value;
1077
+ }
1078
+ applied.push({
1079
+ material: name, applied: true,
1080
+ color: material.color ? `#${material.color.getHexString()}` : null,
1081
+ metalness: material.metalness ?? null,
1082
+ roughness: material.roughness ?? null,
1083
+ clearcoat: material.clearcoat ?? null,
1084
+ clearcoatRoughness: material.clearcoatRoughness ?? null,
1085
+ emissive: material.emissive ? `#${material.emissive.getHexString()}` : null,
1086
+ });
1087
+ }
1088
+ }
1089
+ // clearcoat の有無だけがシェーダ定義を変えうる。色・係数の通常変更では再コンパイルしない。
1090
+ for (const [material, oldCoat] of oldCoats) {
1091
+ if ((oldCoat > 0) !== (material.clearcoat > 0)) material.needsUpdate = true;
1092
+ }
1093
+ state.selected = selected;
1094
+ state.applied = applied;
1095
+ }
1096
+
949
1097
  function createCamera(THREE, descriptor) {
950
1098
  const cameraDescriptor = descriptor.camera ?? {};
951
1099
  const camera = new THREE.PerspectiveCamera(
@@ -2017,6 +2165,7 @@ window.akari.threeRuntime = (() => {
2017
2165
  if (!instance.active || !instance.contentReady) return;
2018
2166
  rendererSize(instance, instance.maxRenderSize);
2019
2167
  applyProjectionKnobs(instance);
2168
+ applyFinish(instance);
2020
2169
  if (instance.mixer) instance.mixer.setTime(Math.max(0, localSeconds));
2021
2170
  if (instance.textAnimEntries.length > 0) updateTextVisibility(instance, localSeconds);
2022
2171
  if (instance.textAnimEntries.length > 0) updateTextAnimations(instance, localSeconds);
@@ -2379,6 +2528,16 @@ window.akari.threeRuntime = (() => {
2379
2528
  physicsPresimMs: null,
2380
2529
  // materialOverrides の解決結果(inspect() から検証・証跡に使う)。
2381
2530
  materialOverrideReport: [],
2531
+ finish: descriptor.finishes ? {
2532
+ descriptor: descriptor.finishes,
2533
+ selected: null,
2534
+ applied: [],
2535
+ base: new Map(),
2536
+ materialsByName: null,
2537
+ warnedValues: new Set(),
2538
+ warnedMaterials: new Set(),
2539
+ warnedProperties: new Set(),
2540
+ } : null,
2382
2541
  // model 読み込み(宣言時のみ)+ 全 texts sync() 完了の両方が揃うまで false。
2383
2542
  // draw() はこれを ready 条件に使う(読み込み中フレームが書き出しに混入しないため)
2384
2543
  contentReady: false,
@@ -2410,6 +2569,9 @@ window.akari.threeRuntime = (() => {
2410
2569
  }
2411
2570
  instance.model = gltf.scene;
2412
2571
  await applyMaterialOverrides(THREE, instance, gltf.scene, descriptor.materialOverrides);
2572
+ // materialOverrides を先に適用し、その結果を素の値として finishes が指定キーだけ上書きする。
2573
+ // emissiveMap / emissiveIntensity は materialOverrides、emissive 色は finishes の担当。
2574
+ initializeFinishes(instance, gltf.scene);
2413
2575
  if (descriptor.environment?.map) {
2414
2576
  await applyEnvironmentMap(THREE, instance, descriptor.environment.map);
2415
2577
  }
@@ -2623,6 +2785,8 @@ window.akari.threeRuntime = (() => {
2623
2785
  if (rows.has(material.uuid)) continue;
2624
2786
  rows.set(material.uuid, {
2625
2787
  name: material.name, type: material.type, metalness: material.metalness,
2788
+ color: material.color ? `#${material.color.getHexString()}` : null,
2789
+ emissive: material.emissive ? `#${material.emissive.getHexString()}` : null,
2626
2790
  roughness: material.roughness, specularIntensity: material.specularIntensity,
2627
2791
  clearcoat: material.clearcoat, clearcoatRoughness: material.clearcoatRoughness,
2628
2792
  emissiveIntensity: material.emissiveIntensity,
@@ -2633,6 +2797,12 @@ window.akari.threeRuntime = (() => {
2633
2797
  })(),
2634
2798
  // materialOverrides の CSS 変数解決・適用結果(検証・証跡用)。
2635
2799
  materialOverrides: instance.materialOverrideReport.map((entry) => ({ ...entry })),
2800
+ finish: instance.finish ? {
2801
+ var: instance.finish.descriptor.var ?? "--akari-3d-finish",
2802
+ selected: instance.finish.selected,
2803
+ default: instance.finish.descriptor.default,
2804
+ applied: instance.finish.applied.map((entry) => ({ ...entry })),
2805
+ } : null,
2636
2806
  // texts[] の per-char 展開数(検証・証跡用。flat モードの読み込み完了を絵の比較なしに確認する)
2637
2807
  textNodes: instance.textNodes.length,
2638
2808
  textBlocks: instance.textAnimEntries.length,
@@ -87,7 +87,7 @@ fragment は単一ルートとし、透明 canvas、任意の静的 fallback、
87
87
  </div>
88
88
  ```
89
89
 
90
- - top-level key は `model` / `camera` / `environment` / `fog` / `background` / `lights` / `animationClip` / `materialOverrides` / `shadows` / `texts` / `physics` だけ。`texts[]` があれば `model` は任意(詳細は後述「texts[] — 3D テキスト」節)。`physics` は `texts[]` の物理落下(後述「physics — 3D テキストの物理落下」節)。
90
+ - top-level key は `model` / `camera` / `environment` / `fog` / `background` / `lights` / `animationClip` / `materialOverrides` / `finishes` / `shadows` / `texts` / `physics` だけ。`texts[]` があれば `model` は任意(詳細は後述「texts[] — 3D テキスト」節)。`physics` は `texts[]` の物理落下(後述「physics — 3D テキストの物理落下」節)。
91
91
  - `model` は edit.json のあるディレクトリからの相対 `.glb` パス。URL や CDN を書かない。
92
92
  - camera は perspective camera。`position` / `lookAt` は `[x, y, z]`、`fov` / `near` / `far` は数値。
93
93
  または `{ "fromModel": "<glb 内カメラノード名>" }` で glb 内カメラを描画に使う(§カメラを動かす)。
@@ -120,7 +120,7 @@ fragment は単一ルートとし、透明 canvas、任意の静的 fallback、
120
120
  `"*"` も指定できる。省略時は静的シーンを描画する。Blender の glTF 書き出しはオブジェクトごとに
121
121
  clip を分けるので、1 個のモデルに複数の動きがあるときは `"*"` で束ねる(1 本しか再生しないと
122
122
  片方しか動かない)。存在しない clip 名を書いた場合はエラーになる。
123
- - `materialOverrides` は `{ "<material 名>": { "texture": "<画像または動画の相対パス>" } }` の形で、名前が一致するマテリアルの `emissiveMap` を差し替える。**差し替え先が `emissiveMap` である以上、貼り先の材質が発光しない(glTF の `emissiveFactor` が未設定 = 黒)と「0 × テクスチャ」で何も出ない**(2026-08-14 実害。詳細は後述「発光しない材質には貼れない」)。edit.json のあるディレクトリからの相対 PNG / JPEG / WebP 等、または MP4 / MOV / WebM を指定し、URL や CDN を書かない。該当するマテリアル名がモデル内にない場合は警告して無視される。
123
+ - `materialOverrides` は `{ "<material 名>": { "texture": "<画像または動画の相対パス>" } }` の形で、名前が一致するマテリアルの `emissiveMap` を差し替える。**貼り先の材質が発光しない(glTF の `emissiveFactor` が未設定 = 黒)と、`materialOverrides` 単独では「0 × テクスチャ」で何も出ない**(2026-08-14 実害。詳細は後述「発光しない材質の emissiveMap」)。edit.json のあるディレクトリからの相対 PNG / JPEG / WebP 等、または MP4 / MOV / WebM を指定し、URL や CDN を書かない。該当するマテリアル名がモデル内にない場合は警告して無視される。
124
124
  - `textureVar` は画面に映すものをツマミ(CSS カスタムプロパティ)にする任意キー。`"--screen-src"` のような変数名を書き、overlay の `vars` でその変数に相対パスを入れると `texture` の代わりに差し込まれる。空・未設定なら `texture` がそのまま使われる。
125
125
  - `textureVar` を使う場合も `texture` は実在する相対パスのままにする。書き出し前の宣言済み入力検査が実ファイルを要求するため、`texture` に `var(--screen-src)` を直接書く形はライブプレビューでは動くが書き出しでは失敗する。
126
126
  - `brightness` は任意の発光倍率(0〜4、既定 1)で、差し替えたテクスチャの `emissiveIntensity` に掛かる。`var(--screen-brightness)` のような CSS 変数も書き出しを含めて使える。既定の 1 のときは `emissiveIntensity` に触れず、既存宣言の見た目を維持する。
@@ -448,7 +448,51 @@ texts[] anim と同じ「render は localSeconds の純関数」原則をその
448
448
  (人物セグメンテーション → 輪郭抽出 → 簡略化の自動生成は将来スキル化予定。現時点では手動)。
449
449
  被写体の直上を狙うだけなら `spawn` の矩形を人物の頭・肩幅に合わせて宣言すればよい。
450
450
 
451
- ## 標準ツマミ(camera 投影 — pan / zoom。2026-08-06 導入)
451
+ ## 標準ツマミ(camera 投影と材質 finish)
452
+
453
+ ### 材質 finish の選択
454
+
455
+ `finishes` は glb 内の `material.name` ごとに色と表面係数を宣言し、CSS カスタムプロパティ
456
+ 1 個で選択する。`var` の省略値は `--akari-3d-finish`。`default` は `options` 内に必須。
457
+ 選択名は英字または `_` で始まり、以降は英数字・`_`・`-` のみを使う。
458
+
459
+ ```json
460
+ "finishes": {
461
+ "var": "--akari-3d-finish",
462
+ "default": "natural-titanium",
463
+ "options": {
464
+ "natural-titanium": { "FrameMat": { "color": "#c5c2b9", "metalness": 1, "roughness": 0.35 } },
465
+ "cosmic-orange": { "FrameMat": { "color": "#d66b33", "metalness": 1, "roughness": 0.4 } }
466
+ }
467
+ }
468
+ ```
469
+
470
+ 各材質に指定できるキーは `color` / `metalness` / `roughness` / `clearcoat` /
471
+ `clearcoatRoughness` / `emissive`。色は `#rrggbb` の 6 桁 hex、数値は 0〜1。
472
+ モデルにない材質名や材質にないプロパティは警告して無視する。選択を変えるたび、各材質を
473
+ `materialOverrides` 適用後の値へ戻してから指定キーだけ上書きするため、前の選択の値は残らない。
474
+ テクスチャには一切触れない。
475
+
476
+ ```css
477
+ .phone-live { --akari-3d-finish: var(--phone-finish, natural-titanium); }
478
+ ```
479
+
480
+ 素材 `meta.json` の knob は、既存の `cssVar` バインド形式で指定する:
481
+ 取りうる値は `finishes.options` のキーと一致させる。meta 側は `options` 配列を持たず、`label` に列挙する。
482
+
483
+ ```json
484
+ { "cssVar": "--phone-finish", "type": "dropdown", "group": "style", "label": "本体の仕上げ(natural-titanium / cosmic-orange)", "default": "natural-titanium" }
485
+ ```
486
+
487
+ CSS 値が空なら `default`、未知の値なら警告を 1 回出して `default` を使う。
488
+ `materialOverrides` は `emissiveMap` と `emissiveIntensity`(brightness)を扱い、
489
+ `finishes` は `color` / `metalness` / `roughness` / `clearcoat` / `clearcoatRoughness` と
490
+ `emissive` 色を扱う。`materialOverrides` を先に適用し、その結果を素の値として記録してから
491
+ `finishes` が指定キーだけ上書きする。`emissiveMap` と `emissiveIntensity` は保持される。
492
+ 非発光の材質に貼った `emissiveMap` も、`finishes` で `emissive` を白などにすれば表示できる。
493
+ ライブプレビューと書き出しは同じ経路で適用する。
494
+
495
+ ### カメラ投影の pan / zoom
452
496
 
453
497
  ランタイムは `--akari-3d-pan-x` / `--akari-3d-pan-y` / `--akari-3d-zoom` という 3 つの
454
498
  **標準カスタムプロパティ**を解釈する。断片ルート(または祖先)へ CSS で宣言すると、
@@ -601,11 +645,11 @@ container を渡しても、断片の外側からは同じ値が同じ式で読
601
645
  - 同梱クリップの尺・向きが用途に合わないことは珍しくない。合わないなら**作り直す方が速い**
602
646
  (§glb を直接編集する)
603
647
 
604
- ### 発光しない材質には貼れない(materialOverrides の実測 2026-08-14)
648
+ ### 発光しない材質の emissiveMap(materialOverrides の実測 2026-08-14)
605
649
 
606
650
  `materialOverrides` の差し替え先は `emissiveMap` 固定である。three は `emissive`(glTF の
607
651
  `emissiveFactor`)を掛けてから合成するので、**`emissiveFactor` が未設定=黒の材質へ貼っても
608
- 「0 × テクスチャ」で完全に消える**。しかもエラーにならないため、原因に辿り着きにくい。
652
+ `materialOverrides` 単独では「0 × テクスチャ」で完全に消える**。しかもエラーにならないため、原因に辿り着きにくい。
609
653
 
610
654
  実害: アプリアイコンの glb は前面(`IconFaceMaterial`)が非発光で、`materialOverrides` を
611
655
  宣言しても**白い樹脂の面のまま**だった。同じ宣言でスマホの画面(`ScreenMaterial` =
@@ -618,6 +662,8 @@ container を渡しても、断片の外側からは同じ値が同じ式で読
618
662
  - 対処 1(推奨): モデル側の当該材質に `emissiveFactor: [1,1,1]` を与える。あわせて
619
663
  `baseColorFactor` を暗く落とすと、貼った絵の発色を下地が濁さない
620
664
  - 対処 2: 発光する材質(画面用など)を対象にする
665
+ - 対処 3: `finishes` の同じ材質名に `"emissive": "#ffffff"` などを指定する。
666
+ `materialOverrides` の `emissiveMap` と `emissiveIntensity` は保持される
621
667
  - ランタイムは非発光の材質を対象にすると**警告を出す**(2026-08-14 追加)。ログを見ること
622
668
 
623
669
  ### 動きプリセットは「正面を向く時間」を実測してから使う(2026-08-14)