akari-video 1.3.0-beta.2 → 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
|
+
"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
|
+
"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": [
|
|
@@ -422,6 +422,8 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
422
422
|
let selectedId = null;
|
|
423
423
|
let selectedIds = [];
|
|
424
424
|
let elementFocus = null;
|
|
425
|
+
// Inner selection belongs only to the currently selected telop, never to edit.json.
|
|
426
|
+
let telopInnerId = null;
|
|
425
427
|
let elementGeometryCache = null;
|
|
426
428
|
let elementNudge = null;
|
|
427
429
|
let elementNudgeTimer = null;
|
|
@@ -1819,8 +1821,11 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1819
1821
|
|
|
1820
1822
|
function clearSelection() {
|
|
1821
1823
|
const hadElementFocus = window.akari.capabilities?.elementSelection === true && Boolean(selectedElementFocus());
|
|
1824
|
+
const hadTelopSelection = window.akari.capabilities?.elementSelection === true
|
|
1825
|
+
&& selectedOverlay && isTelopOverlay(selectedOverlay);
|
|
1822
1826
|
flushElementNudge();
|
|
1823
1827
|
elementFocus = null;
|
|
1828
|
+
telopInnerId = null;
|
|
1824
1829
|
if (activeRotate) cancelRotate();
|
|
1825
1830
|
if (activeLine) finishLineEndpoint(true);
|
|
1826
1831
|
flushNudge();
|
|
@@ -1839,7 +1844,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1839
1844
|
selectionFrame?.remove();
|
|
1840
1845
|
selectionFrame = null;
|
|
1841
1846
|
hideSnapGuides();
|
|
1842
|
-
if (hadElementFocus) renderScopeBreadcrumb();
|
|
1847
|
+
if (hadElementFocus || hadTelopSelection) renderScopeBreadcrumb();
|
|
1843
1848
|
}
|
|
1844
1849
|
|
|
1845
1850
|
function handleSelectedOverlayUnavailable(container) {
|
|
@@ -1879,24 +1884,41 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1879
1884
|
selectedOverlay.setAttribute('data-akari-interaction-selected', 'true');
|
|
1880
1885
|
reconcileElementFocus();
|
|
1881
1886
|
} else {
|
|
1887
|
+
const keepTelopInner = selectedId === nextId && telopInnerId === nextId
|
|
1888
|
+
&& selectedIds.length === 1 && !groupSelection;
|
|
1882
1889
|
clearSelection();
|
|
1883
1890
|
selectedOverlay = container;
|
|
1884
1891
|
selectedId = nextId;
|
|
1885
1892
|
selectedIds = selectedId === null ? [] : [selectedId];
|
|
1893
|
+
if (keepTelopInner) telopInnerId = nextId;
|
|
1886
1894
|
selectedOverlay.setAttribute("data-akari-interaction-selected", "true");
|
|
1887
1895
|
}
|
|
1888
1896
|
}
|
|
1889
1897
|
|
|
1890
1898
|
refreshSelectionFrame();
|
|
1899
|
+
if (window.akari.capabilities?.elementSelection === true && isTelopOverlay(selectedOverlay)) renderScopeBreadcrumb();
|
|
1891
1900
|
startSelectionTracking();
|
|
1892
1901
|
return true;
|
|
1893
1902
|
}
|
|
1894
1903
|
|
|
1895
|
-
function
|
|
1904
|
+
function elementSelectionEnabled(container) {
|
|
1896
1905
|
if (window.akari.capabilities?.elementSelection !== true || !container || selectedIds.length > 1) return false;
|
|
1897
1906
|
return window.akari.state?.summary?.overlays?.some(overlay => overlay.id === container.dataset.overlayId
|
|
1898
1907
|
&& overlay.elementSelection === true) === true;
|
|
1899
1908
|
}
|
|
1909
|
+
function canFocusElement(container) {
|
|
1910
|
+
return elementSelectionEnabled(container)
|
|
1911
|
+
&& (!isTelopOverlay(container) || telopInnerId === selectedId && selectedId === container.dataset.overlayId);
|
|
1912
|
+
}
|
|
1913
|
+
function setTelopInnerSelection(overlayId, inner) {
|
|
1914
|
+
if (typeof overlayId !== 'string' || selectedId !== overlayId || selectedIds.length !== 1
|
|
1915
|
+
|| groupSelection || !selectedOverlay || !isTelopOverlay(selectedOverlay)
|
|
1916
|
+
|| !elementSelectionEnabled(selectedOverlay)) return false;
|
|
1917
|
+
telopInnerId = inner === true ? overlayId : null;
|
|
1918
|
+
if (!telopInnerId && selectedElementFocus()) focusElement(null);
|
|
1919
|
+
hideHover();
|
|
1920
|
+
return true;
|
|
1921
|
+
}
|
|
1900
1922
|
function focusedElement() {
|
|
1901
1923
|
if (!selectedElementFocus() || !selectedOverlay) return null;
|
|
1902
1924
|
const root = fragmentRoot(selectedOverlay);
|
|
@@ -1929,6 +1951,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1929
1951
|
const ref = element && elementAddress(root, element);
|
|
1930
1952
|
const next = ref ? { overlayId: selectedId, ref, tag: element.tagName.toLowerCase(),
|
|
1931
1953
|
label: elementLabel(root, element) } : null;
|
|
1954
|
+
if (!next && telopInnerId === selectedId) telopInnerId = null;
|
|
1932
1955
|
if (elementFocus?.ref === next?.ref && elementFocus?.overlayId === next?.overlayId) return;
|
|
1933
1956
|
flushElementNudge();
|
|
1934
1957
|
elementFocus = next;
|
|
@@ -2063,7 +2086,8 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
2063
2086
|
clearBreadcrumbHover();
|
|
2064
2087
|
const entries = [];
|
|
2065
2088
|
const append = (label, action, target) => entries.push({ label, action, target });
|
|
2066
|
-
if (selectedElementFocus()
|
|
2089
|
+
if (selectedOverlay && (selectedElementFocus() || window.akari.capabilities?.elementSelection === true
|
|
2090
|
+
&& selectedIds.length === 1 && isTelopOverlay(selectedOverlay))) {
|
|
2067
2091
|
append('全体', () => { clearSelection(); publishScopedSelection(); }, { kind: 'stage' });
|
|
2068
2092
|
for (const id of lineage(selectionTree(), scopeId)) {
|
|
2069
2093
|
append(treeNode(id)?.label ?? id, () => applyScopedSelection({ selectId: id, scopeId: treeNode(id)?.parentId ?? null }),
|
|
@@ -4383,7 +4407,8 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
4383
4407
|
if (activeEdit?.container === hit && eventHitsElement(event, activeEdit.element)) return;
|
|
4384
4408
|
clickOrigin.scopedHit = true;
|
|
4385
4409
|
if (activeEdit) void commitEdit();
|
|
4386
|
-
if (window.akari.capabilities?.elementSelection === true && event.isTrusted && !event.shiftKey
|
|
4410
|
+
if (window.akari.capabilities?.elementSelection === true && event.isTrusted && !event.shiftKey
|
|
4411
|
+
&& (!isTelopOverlay(hit) || canFocusElement(hit))) {
|
|
4387
4412
|
const root = fragmentRoot(hit);
|
|
4388
4413
|
const target = event.target instanceof Element && root?.contains(event.target)
|
|
4389
4414
|
? nearestSelectableElement(root, event.target, stage?.getBoundingClientRect(), selectionOptions(root)) : null;
|
|
@@ -4404,7 +4429,8 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
4404
4429
|
const focused = focusedElement();
|
|
4405
4430
|
if (focused && isSelectionContainer(focused) && focused.contains(event.target)) {
|
|
4406
4431
|
clickOrigin.preservedContainer = true;
|
|
4407
|
-
} else if (
|
|
4432
|
+
} else if ((!isTelopOverlay(selectedOverlay) || canFocusElement(selectedOverlay))
|
|
4433
|
+
&& clickOrigin?.hitId === selectedId) {
|
|
4408
4434
|
focusElement(elementByAddress(fragmentRoot(selectedOverlay), clickOrigin.elementRef));
|
|
4409
4435
|
} else focusElementAt(selectedOverlay, event);
|
|
4410
4436
|
} else if (selectedElementFocus()) focusElement(null);
|
|
@@ -5209,7 +5235,9 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
5209
5235
|
if (window.akari.capabilities?.elementSelection === true
|
|
5210
5236
|
&& event.isTrusted && !event.shiftKey && selectedOverlay) {
|
|
5211
5237
|
if (clickOrigin?.preservedContainer) focusElementAt(selectedOverlay, event);
|
|
5212
|
-
else if (!
|
|
5238
|
+
else if ((!isTelopOverlay(selectedOverlay) || canFocusElement(selectedOverlay))
|
|
5239
|
+
&& (!nextId || isTelopOverlay(selectedOverlay) && telopInnerId === selectedId
|
|
5240
|
+
&& nextId === selectedId) && clickOrigin?.hitId === selectedId) {
|
|
5213
5241
|
focusElement(elementByAddress(fragmentRoot(selectedOverlay), clickOrigin.elementRef));
|
|
5214
5242
|
} else focusElementAt(selectedOverlay, event);
|
|
5215
5243
|
}
|
|
@@ -5288,10 +5316,12 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
5288
5316
|
}
|
|
5289
5317
|
}
|
|
5290
5318
|
if (event.key === 'Enter' && !event.shiftKey && !activeEdit && !selectedElementFocus()
|
|
5291
|
-
&& selectedOverlay &&
|
|
5319
|
+
&& selectedOverlay && elementSelectionEnabled(selectedOverlay)
|
|
5320
|
+
&& (!isTelopOverlay(selectedOverlay) || selectedIds.length === 1)) {
|
|
5292
5321
|
const root = fragmentRoot(selectedOverlay);
|
|
5293
5322
|
const first = firstSelectableElement(root, stage?.getBoundingClientRect(), selectionOptions(root));
|
|
5294
5323
|
if (first) {
|
|
5324
|
+
if (isTelopOverlay(selectedOverlay)) telopInnerId = selectedId;
|
|
5295
5325
|
focusElement(first);
|
|
5296
5326
|
event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation();
|
|
5297
5327
|
return;
|
|
@@ -5339,7 +5369,8 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
5339
5369
|
if (groupSelection) {
|
|
5340
5370
|
applyScopedSelection(enterScope(selectionTree(), selectedId)); handled(); return;
|
|
5341
5371
|
}
|
|
5342
|
-
if (selectedOverlay)
|
|
5372
|
+
if (selectedOverlay && (!isTelopOverlay(selectedOverlay)
|
|
5373
|
+
|| telopInnerId === selectedId && Boolean(selectedElementFocus()))) {
|
|
5343
5374
|
const root = fragmentRoot(selectedOverlay);
|
|
5344
5375
|
const text = root && [root, ...root.querySelectorAll('*')].find(canEditText);
|
|
5345
5376
|
if (text) { beginEdit(selectedOverlay, text); handled(); }
|
|
@@ -5827,6 +5858,9 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
5827
5858
|
releasePointerOwner,
|
|
5828
5859
|
get selectedId() { return selectedId; },
|
|
5829
5860
|
get elementFocus() { return selectedElementFocus(); },
|
|
5861
|
+
get selectedTelop() { return Boolean(selectedOverlay && selectedIds.length === 1 && isTelopOverlay(selectedOverlay)); },
|
|
5862
|
+
get telopInner() { return telopInnerId !== null && telopInnerId === selectedId; },
|
|
5863
|
+
setTelopInnerSelection,
|
|
5830
5864
|
clearElementFocus() { if (selectedElementFocus()) focusElement(null, { notify: false }); },
|
|
5831
5865
|
focusElementAtPoint,
|
|
5832
5866
|
get selectedIds() { return [...selectedIds]; },
|
|
@@ -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`
|
|
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
|
|
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
|
-
###
|
|
648
|
+
### 発光しない材質の emissiveMap(materialOverrides の実測 2026-08-14)
|
|
605
649
|
|
|
606
650
|
`materialOverrides` の差し替え先は `emissiveMap` 固定である。three は `emissive`(glTF の
|
|
607
651
|
`emissiveFactor`)を掛けてから合成するので、**`emissiveFactor` が未設定=黒の材質へ貼っても
|
|
608
|
-
|
|
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)
|