akari-video 1.2.0-beta.5 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/kits.mjs +9 -11
- package/src/vendor-sources.mjs +2 -0
- package/vendor/packages/akari-launcher/package.json +1 -1
- package/vendor/packages/frame-scene/src/schema.mjs +102 -0
- package/vendor/packages/overlay-runtime/src/fragment-source-write.mjs +21 -6
- package/vendor/packages/overlay-runtime/src/interaction.js +175 -64
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "1.2.0
|
|
3
|
+
"version": "1.2.0",
|
|
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": {
|
package/src/kits.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
|
|
10
10
|
import { resolveLauncherAssets } from './repo-assets.mjs';
|
|
11
|
+
import { compareVersions } from './version.mjs';
|
|
11
12
|
const creatorRootModulePath = resolveLauncherAssets().creatorRootModulePath;
|
|
12
13
|
const creatorRoot = creatorRootModulePath ? await import(pathToFileURL(creatorRootModulePath).href) : null;
|
|
13
14
|
function resolveAssetLibraryRoots(env) {
|
|
@@ -20,15 +21,11 @@ const INSTALLED_ASSETS_SCHEMA = 'akari-installed-assets/v0';
|
|
|
20
21
|
const PLUGIN_DESCRIPTION = 'AKARI Video 拡張キットのスキルをまとめて提供するローカルプラグイン。';
|
|
21
22
|
|
|
22
23
|
function parseVersion(value) {
|
|
23
|
-
const match =
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
for (let index = 0; index < 3; index += 1) {
|
|
29
|
-
if (left[index] !== right[index]) return left[index] - right[index];
|
|
30
|
-
}
|
|
31
|
-
return 0;
|
|
24
|
+
const match = typeof value === 'string'
|
|
25
|
+
? value.match(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z.-]+)?$/u)
|
|
26
|
+
: null;
|
|
27
|
+
if (!match || match[4]?.split('.').some(id => /^0\d+$/u.test(id))) return null;
|
|
28
|
+
return match.slice(1, 4).map(Number);
|
|
32
29
|
}
|
|
33
30
|
|
|
34
31
|
function satisfies(version, range) {
|
|
@@ -36,13 +33,14 @@ function satisfies(version, range) {
|
|
|
36
33
|
const match = String(range).match(/^(\^|~|>=)?(\d+\.\d+\.\d+)$/u);
|
|
37
34
|
if (!actual || !match) return false;
|
|
38
35
|
const required = parseVersion(match[2]);
|
|
39
|
-
if (
|
|
36
|
+
if (!required) return false;
|
|
37
|
+
if (compareVersions(version, match[2]) < 0) return false;
|
|
40
38
|
if (match[1] === '^') {
|
|
41
39
|
return actual[0] === required[0] && (required[0] !== 0 || actual[1] === required[1]);
|
|
42
40
|
}
|
|
43
41
|
if (match[1] === '~') return actual[0] === required[0] && actual[1] === required[1];
|
|
44
42
|
if (match[1] === '>=') return true;
|
|
45
|
-
return compareVersions(
|
|
43
|
+
return compareVersions(version, match[2]) === 0;
|
|
46
44
|
}
|
|
47
45
|
|
|
48
46
|
export function readKitManifest(kitDir) {
|
package/src/vendor-sources.mjs
CHANGED
|
@@ -14,6 +14,8 @@ export const VENDOR_SOURCES = [
|
|
|
14
14
|
'presets/textanim',
|
|
15
15
|
'assets/font/noto-sans-jp/NotoSansJP-Variable.ttf',
|
|
16
16
|
'packages/schemas',
|
|
17
|
+
// schemas の frame-scene 検証器が参照する schema 定義だけを同梱する。
|
|
18
|
+
'packages/frame-scene/src/schema.mjs',
|
|
17
19
|
// schemas の asset / kit 検証は runtimes.mjs を実行し、runtime 宣言は src の
|
|
18
20
|
// レジストリ・ブラウザスクリプトと既定フォントをファイルとして読み込む。
|
|
19
21
|
'packages/overlay-runtime/runtimes.mjs',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "1.2.0
|
|
3
|
+
"version": "1.2.0",
|
|
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": [
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
2
|
+
import schema from '../../schemas/frame-scene.schema.json' with { type: 'json' };
|
|
3
|
+
|
|
4
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
5
|
+
const check = ajv.compile(schema);
|
|
6
|
+
const JOINTS = new Set('hips spine chest neck head leftUpperArm leftLowerArm leftHand rightUpperArm rightLowerArm rightHand leftUpperLeg leftLowerLeg leftFoot rightUpperLeg rightLowerLeg rightFoot'.split(' '));
|
|
7
|
+
const known = {
|
|
8
|
+
'': schema.properties, '/world': schema.$defs.world.properties,
|
|
9
|
+
'/objects/*': schema.$defs.object.properties, '/scenes/*': schema.$defs.scene.properties,
|
|
10
|
+
'/scenes/*/camera': schema.$defs.camera.properties, '/scenes/*/camera_to': schema.$defs.camera.properties,
|
|
11
|
+
'/scenes/*/world': schema.$defs.world.properties,
|
|
12
|
+
'/scenes/*/set/*': schema.$defs.setObject.properties,
|
|
13
|
+
'/scenes/*/annotations/*': schema.$defs.annotation.properties,
|
|
14
|
+
'/scenes/*/annotations/*/anchor': schema.$defs.annotation.properties.anchor.properties,
|
|
15
|
+
'/objects/*/pose': schema.$defs.pose.oneOf[1].properties,
|
|
16
|
+
'/scenes/*/set/*/pose': schema.$defs.pose.oneOf[1].properties,
|
|
17
|
+
'/objects/*/pose/joints/*': { x: true, y: true, z: true },
|
|
18
|
+
'/scenes/*/set/*/pose/joints/*': { x: true, y: true, z: true },
|
|
19
|
+
'/objects/*/screen': schema.$defs.object.properties.screen.properties,
|
|
20
|
+
'/scenes/*/set/*/screen': schema.$defs.object.properties.screen.properties
|
|
21
|
+
};
|
|
22
|
+
const ptr = (base, key) => `${base}/${String(key).replaceAll('~', '~0').replaceAll('/', '~1')}`;
|
|
23
|
+
const issue = (code, path, message) => ({ code, path, message });
|
|
24
|
+
|
|
25
|
+
export function validateFrameScene(doc, { strict = false, fileBytes } = {}) {
|
|
26
|
+
const errors = [], warnings = [];
|
|
27
|
+
if (doc && typeof doc === 'object' && Number.isInteger(doc.version) && doc.version > 0) {
|
|
28
|
+
return { ok: false, errors: [issue('unsupported_version', '/version', `version ${doc.version} is unsupported`)], warnings };
|
|
29
|
+
}
|
|
30
|
+
if ((fileBytes ?? Buffer.byteLength(JSON.stringify(doc) ?? '')) > 512 * 1024) warnings.push(issue('limit.file', '', 'file exceeds 512KB'));
|
|
31
|
+
if (!check(doc)) for (const e of check.errors ?? []) {
|
|
32
|
+
const path = e.keyword === 'required' ? ptr(e.instancePath, e.params.missingProperty) : e.instancePath;
|
|
33
|
+
errors.push(issue(e.keyword === 'minItems' && e.instancePath === '/scenes' ? 'scene.empty' : `schema.${e.keyword.toLowerCase()}`, path, e.message ?? e.keyword));
|
|
34
|
+
}
|
|
35
|
+
if (!doc || typeof doc !== 'object' || Array.isArray(doc)) return { ok: false, errors, warnings };
|
|
36
|
+
const objects = Array.isArray(doc.objects) ? doc.objects : [];
|
|
37
|
+
const scenes = Array.isArray(doc.scenes) ? doc.scenes : [];
|
|
38
|
+
if (objects.length > 200) warnings.push(issue('limit.objects', '/objects', 'objects exceeds 200'));
|
|
39
|
+
if (scenes.length > 50) warnings.push(issue('limit.scenes', '/scenes', 'scenes exceeds 50'));
|
|
40
|
+
const ids = new Set(), sceneIds = new Set();
|
|
41
|
+
const ref = (value, path, code = 'object.reference.unresolved') => {
|
|
42
|
+
if (typeof value === 'string' && !ids.has(value)) errors.push(issue(code, path, `unknown object ${value}`));
|
|
43
|
+
};
|
|
44
|
+
for (const [i, o] of objects.entries()) {
|
|
45
|
+
if (!o || typeof o !== 'object') continue;
|
|
46
|
+
const p = `/objects/${i}`;
|
|
47
|
+
if (typeof o.id === 'string') {
|
|
48
|
+
if (!/^[a-z0-9_-]+$/.test(o.id)) errors.push(issue('object.id.invalid', `${p}/id`, 'id must use [a-z0-9_-]'));
|
|
49
|
+
if (ids.has(o.id)) errors.push(issue('object.id.duplicate', `${p}/id`, 'duplicate object id'));
|
|
50
|
+
ids.add(o.id);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function inspectObject(o, p) {
|
|
54
|
+
if (!o || typeof o !== 'object') return;
|
|
55
|
+
if (o.facing !== undefined && (o.ry !== undefined || o.rot !== undefined)) errors.push(issue('object.rotation.exclusive', p, 'facing and ry/rot are exclusive'));
|
|
56
|
+
if (o.ry !== undefined && o.rot !== undefined) errors.push(issue('object.rotation.exclusive', p, 'ry and rot are exclusive'));
|
|
57
|
+
if (o.h !== undefined && o.scale !== undefined) errors.push(issue('object.size.exclusive', p, 'h and scale are exclusive'));
|
|
58
|
+
if (o.pose !== undefined && o.kind !== 'human') errors.push(issue('pose.kind.invalid', `${p}/pose`, 'pose requires human'));
|
|
59
|
+
if (o.on !== undefined) ref(o.on, `${p}/on`, 'on.unresolved');
|
|
60
|
+
if (typeof o.facing === 'string' && o.facing !== 'camera') ref(o.facing, `${p}/facing`, 'facing.unresolved');
|
|
61
|
+
if (o.screen_of !== undefined) ref(o.screen_of, `${p}/screen_of`);
|
|
62
|
+
const pose = o.pose;
|
|
63
|
+
if (pose && typeof pose === 'object' && !Array.isArray(pose)) {
|
|
64
|
+
for (const joint of Object.keys(pose.joints ?? {})) if (!JOINTS.has(joint)) warnings.push(issue('pose.joint.unknown', `${p}/pose/joints/${joint}`, `unknown joint ${joint}`));
|
|
65
|
+
for (const key of ['look', 'point_at']) if (typeof pose[key] === 'string') ref(pose[key], `${p}/pose/${key}`, 'pose.target.unresolved');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
objects.forEach((o, i) => inspectObject(o, `/objects/${i}`));
|
|
69
|
+
for (const [i, scene] of scenes.entries()) {
|
|
70
|
+
if (!scene || typeof scene !== 'object') continue;
|
|
71
|
+
const p = `/scenes/${i}`;
|
|
72
|
+
if (typeof scene.id === 'string') {
|
|
73
|
+
if (!/^[a-z0-9_-]+$/.test(scene.id)) errors.push(issue('scene.id.invalid', `${p}/id`, 'invalid scene id'));
|
|
74
|
+
if (sceneIds.has(scene.id)) errors.push(issue('scene.id.duplicate', `${p}/id`, 'duplicate scene id'));
|
|
75
|
+
sceneIds.add(scene.id);
|
|
76
|
+
}
|
|
77
|
+
for (const key of ['camera', 'camera_to']) if (typeof scene[key]?.look === 'string') ref(scene[key].look, `${p}/${key}/look`, 'camera.look.unresolved');
|
|
78
|
+
for (const [id, delta] of Object.entries(scene.set ?? {})) {
|
|
79
|
+
if (!ids.has(id)) warnings.push(issue('set.unresolved', ptr(`${p}/set`, id), `unknown object ${id}`));
|
|
80
|
+
else inspectObject({ ...objects.find(o => o?.id === id), ...delta }, ptr(`${p}/set`, id));
|
|
81
|
+
}
|
|
82
|
+
const annotations = Array.isArray(scene.annotations) ? scene.annotations : [];
|
|
83
|
+
if (annotations.length > 100) warnings.push(issue('limit.annotations', `${p}/annotations`, 'annotations exceeds 100'));
|
|
84
|
+
annotations.forEach((a, j) => {
|
|
85
|
+
if (a?.anchor?.object !== undefined) ref(a.anchor.object, `${p}/annotations/${j}/anchor/object`, 'anchor.unresolved');
|
|
86
|
+
if (a?.frame) for (const [key, value] of Object.entries(a.frame)) {
|
|
87
|
+
if (['at', 'from', 'to'].includes(key) && Array.isArray(value)) value.forEach((n, k) => { if (typeof n !== 'number' || n < 0 || n > 1) errors.push(issue('annotation.coordinate.invalid', `${p}/annotations/${j}/frame/${key}/${k}`, 'coordinate must be 0..1')); });
|
|
88
|
+
if (key === 'points' && Array.isArray(value)) value.flat().forEach((n, k) => { if (typeof n !== 'number' || n < 0 || n > 1) errors.push(issue('annotation.coordinate.invalid', `${p}/annotations/${j}/frame/points/${k}`, 'coordinate must be 0..1')); });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const walk = (value, path = '') => {
|
|
93
|
+
if (!value || typeof value !== 'object') return;
|
|
94
|
+
if (Array.isArray(value)) { value.forEach((v, i) => walk(v, `${path}/${i}`)); return; }
|
|
95
|
+
const normalized = path.replace(/\/\d+(?=\/|$)/g, '/*').replace(/\/set\/[^/]+/, '/set/*').replace(/\/joints\/[^/]+/, '/joints/*');
|
|
96
|
+
const props = normalized.includes('/annotations/*') && !normalized.endsWith('/anchor') ? undefined : known[normalized];
|
|
97
|
+
if (props) for (const key of Object.keys(value)) if (!(key in props)) (strict ? errors : warnings).push(issue('key.unknown', ptr(path, key), `unknown key ${key}`));
|
|
98
|
+
for (const [key, v] of Object.entries(value)) walk(v, ptr(path, key));
|
|
99
|
+
};
|
|
100
|
+
walk(doc);
|
|
101
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
102
|
+
}
|
|
@@ -54,17 +54,25 @@ function textSpans(html, tags = []) {
|
|
|
54
54
|
if (!frame?.inSlot || frame.isSlotRoot) tags.push('/' + closing[1].toLowerCase());
|
|
55
55
|
} else if (opening) {
|
|
56
56
|
const name = opening[1].toLowerCase();
|
|
57
|
-
const
|
|
57
|
+
const parent = slotStack[slotStack.length - 1];
|
|
58
|
+
const parentInSlot = parent?.inSlot ?? false;
|
|
59
|
+
const namespace = name === 'svg' || name === 'math' ? name
|
|
60
|
+
: parent?.name === 'foreignobject' && parent.namespace === 'svg'
|
|
61
|
+
? 'html' : (parent?.namespace ?? 'html');
|
|
62
|
+
const selfClosing = tag[0].endsWith('/>');
|
|
58
63
|
if (!parentInSlot) tags.push(name);
|
|
59
|
-
if ((name === 'style' || name === 'script') && !
|
|
64
|
+
if ((name === 'style' || name === 'script') && !selfClosing) {
|
|
60
65
|
const close = new RegExp(`</${name}\\s*>`, 'ig');
|
|
61
66
|
close.lastIndex = cursor;
|
|
62
67
|
const end = close.exec(html);
|
|
63
68
|
if (!end) throw new Error(`${name} タグが閉じていません`);
|
|
64
69
|
cursor = close.lastIndex;
|
|
65
|
-
} else if (
|
|
70
|
+
} else if (selfClosing && namespace !== 'html' && name !== 'style' && name !== 'script') {
|
|
71
|
+
if (!parentInSlot) tags.push('/' + name);
|
|
72
|
+
} else if (!VOID_ELEMENTS.has(name) && !selfClosing) {
|
|
66
73
|
const isSlotRoot = !parentInSlot && hasSlotAttribute(tag[0], opening[1]);
|
|
67
|
-
slotStack.push({ inSlot: parentInSlot || isSlotRoot, isSlotRoot
|
|
74
|
+
slotStack.push({ inSlot: parentInSlot || isSlotRoot, isSlotRoot,
|
|
75
|
+
name, namespace });
|
|
68
76
|
}
|
|
69
77
|
}
|
|
70
78
|
continue;
|
|
@@ -85,8 +93,15 @@ export function patchFragmentSourceText(source, edited) {
|
|
|
85
93
|
const afterTags = [];
|
|
86
94
|
const before = textSpans(source, beforeTags);
|
|
87
95
|
const after = textSpans(edited, afterTags);
|
|
88
|
-
|
|
89
|
-
|
|
96
|
+
const refusal = '断片の構造が変わったため、元ソースの文字だけを安全に保存できません';
|
|
97
|
+
const tagIndex = beforeTags.findIndex((tag, index) => tag !== afterTags[index]);
|
|
98
|
+
if (tagIndex >= 0 || beforeTags.length !== afterTags.length) {
|
|
99
|
+
const index = tagIndex >= 0 ? tagIndex : Math.min(beforeTags.length, afterTags.length);
|
|
100
|
+
const label = tag => tag === undefined ? 'なし' : `<${tag}>`;
|
|
101
|
+
throw new Error(`${refusal}(最初の違い: ${index + 1} 番目・元 ${label(beforeTags[index])} / 編集後 ${label(afterTags[index])})`);
|
|
102
|
+
}
|
|
103
|
+
if (before.length !== after.length) {
|
|
104
|
+
throw new Error(`${refusal}(文字の区切りの数: 元 ${before.length} / 編集後 ${after.length})`);
|
|
90
105
|
}
|
|
91
106
|
let result = source;
|
|
92
107
|
for (let index = before.length - 1; index >= 0; index--) {
|
|
@@ -1722,50 +1722,143 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1722
1722
|
Object.assign(selectionFrame.style, { left: `${rect.left}px`, top: `${rect.top}px`,
|
|
1723
1723
|
width: `${rect.width}px`, height: `${rect.height}px` });
|
|
1724
1724
|
}
|
|
1725
|
+
let breadcrumbResizeObserver = null;
|
|
1726
|
+
let breadcrumbHoverActive = false;
|
|
1727
|
+
let breadcrumbHoverSuppressedAt = null;
|
|
1728
|
+
function clearBreadcrumbHover() {
|
|
1729
|
+
if (!breadcrumbHoverActive) return;
|
|
1730
|
+
hideHover();
|
|
1731
|
+
}
|
|
1732
|
+
function breadcrumbBounds(target) {
|
|
1733
|
+
if (target.kind === 'stage') return { rect: stage?.getBoundingClientRect() };
|
|
1734
|
+
if (target.kind === 'node') {
|
|
1735
|
+
const node = treeNode(target.id);
|
|
1736
|
+
const leaf = containerById(target.id);
|
|
1737
|
+
const lineRect = leaf && lineFrameGeometry(leaf);
|
|
1738
|
+
const rect = node?.kind !== 'leaf' ? unionBounds(visibleMembers(target.id))
|
|
1739
|
+
: lineRect ?? (leaf ? fragmentBounds(leaf) : null);
|
|
1740
|
+
return { rect, angle: lineRect?.angle ?? null };
|
|
1741
|
+
}
|
|
1742
|
+
if (target.kind === 'item') {
|
|
1743
|
+
const container = containerById(target.id);
|
|
1744
|
+
return { rect: container ? fragmentBounds(container) : null };
|
|
1745
|
+
}
|
|
1746
|
+
const element = target.element?.isConnected ? target.element
|
|
1747
|
+
: elementByAddress(fragmentRoot(containerById(target.overlayId)), target.ref);
|
|
1748
|
+
if (!element?.isConnected) return null;
|
|
1749
|
+
const geometry = elementScreenGeometry(element);
|
|
1750
|
+
const width = geometry.width * Math.hypot(geometry.axes.x.x, geometry.axes.x.y);
|
|
1751
|
+
const height = geometry.height * Math.hypot(geometry.axes.y.x, geometry.axes.y.y);
|
|
1752
|
+
return { rect: { left: geometry.center.x - width / 2, top: geometry.center.y - height / 2,
|
|
1753
|
+
width, height }, angle: Math.atan2(geometry.axes.x.y, geometry.axes.x.x) * 180 / Math.PI };
|
|
1754
|
+
}
|
|
1725
1755
|
function renderScopeBreadcrumb() {
|
|
1726
1756
|
const nav = document.querySelector('[data-akari-ui="preview-scope-breadcrumb"]');
|
|
1727
1757
|
if (!nav) return;
|
|
1758
|
+
if (!breadcrumbResizeObserver && typeof ResizeObserver !== 'undefined' && nav.parentElement) {
|
|
1759
|
+
breadcrumbResizeObserver = new ResizeObserver(() => renderScopeBreadcrumb());
|
|
1760
|
+
breadcrumbResizeObserver.observe(nav.parentElement);
|
|
1761
|
+
}
|
|
1762
|
+
clearBreadcrumbHover();
|
|
1763
|
+
const entries = [];
|
|
1764
|
+
const append = (label, action, target) => entries.push({ label, action, target });
|
|
1728
1765
|
if (selectedElementFocus() && selectedOverlay) {
|
|
1729
|
-
|
|
1730
|
-
nav.replaceChildren();
|
|
1731
|
-
const append = (label, action) => {
|
|
1732
|
-
if (nav.childNodes.length) nav.append(' › ');
|
|
1733
|
-
const button = document.createElement('button');
|
|
1734
|
-
button.type = 'button'; button.textContent = label;
|
|
1735
|
-
button.addEventListener('click', action);
|
|
1736
|
-
nav.appendChild(button);
|
|
1737
|
-
};
|
|
1738
|
-
append('全体', () => { clearSelection(); publishScopedSelection(); });
|
|
1766
|
+
append('全体', () => { clearSelection(); publishScopedSelection(); }, { kind: 'stage' });
|
|
1739
1767
|
for (const id of lineage(selectionTree(), scopeId)) {
|
|
1740
|
-
append(treeNode(id)?.label ?? id, () => applyScopedSelection({ selectId: id, scopeId: treeNode(id)?.parentId ?? null })
|
|
1768
|
+
append(treeNode(id)?.label ?? id, () => applyScopedSelection({ selectId: id, scopeId: treeNode(id)?.parentId ?? null }),
|
|
1769
|
+
{ kind: 'node', id });
|
|
1741
1770
|
}
|
|
1742
1771
|
const name = window.akari.state?.summary?.overlays?.find(overlay => overlay.id === selectedId)?.name ?? selectedId;
|
|
1743
|
-
append(name, () => focusElement(null));
|
|
1772
|
+
append(name, () => focusElement(null), { kind: 'item', id: selectedId });
|
|
1744
1773
|
const root = fragmentRoot(selectedOverlay);
|
|
1745
1774
|
const path = [];
|
|
1746
1775
|
for (let node = focusedElement(); node && node !== root; node = node.parentElement) {
|
|
1747
1776
|
if (selectableElement(root, node, stage?.getBoundingClientRect())) path.unshift(node);
|
|
1748
1777
|
}
|
|
1749
|
-
for (const node of path) append(elementLabel(root, node), () => focusElement(node)
|
|
1750
|
-
|
|
1751
|
-
}
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
const path = lineage(selectionTree(), scopeId);
|
|
1756
|
-
const ids = floorScopeId === null ? [null, ...path] : path.slice(path.indexOf(floorScopeId));
|
|
1757
|
-
ids.forEach((id, index) => {
|
|
1758
|
-
if (index) nav.append(' › ');
|
|
1759
|
-
const button = document.createElement('button');
|
|
1760
|
-
button.type = 'button';
|
|
1761
|
-
button.textContent = id === null ? '全体' : treeNode(id)?.label ?? id;
|
|
1762
|
-
button.addEventListener('click', () => {
|
|
1778
|
+
for (const node of path) append(elementLabel(root, node), () => focusElement(node),
|
|
1779
|
+
{ kind: 'element', element: node, overlayId: selectedId, ref: elementAddress(root, node) });
|
|
1780
|
+
} else if (scopeId !== floorScopeId && selectionTree().length) {
|
|
1781
|
+
const path = lineage(selectionTree(), scopeId);
|
|
1782
|
+
const ids = floorScopeId === null ? [null, ...path] : path.slice(path.indexOf(floorScopeId));
|
|
1783
|
+
ids.forEach(id => append(id === null ? '全体' : treeNode(id)?.label ?? id, () => {
|
|
1763
1784
|
const hit = descendantLeafIds(selectionTree(), selectedId)[0] ?? selectedId;
|
|
1764
1785
|
const next = resolveScopedSelection(selectionTree(), id, hit);
|
|
1765
1786
|
applyScopedSelection({ scopeId: id, selectId: next.scopeId === id ? next.selectId : null });
|
|
1787
|
+
}, id === null ? { kind: 'stage' } : { kind: 'node', id }));
|
|
1788
|
+
}
|
|
1789
|
+
nav.replaceChildren();
|
|
1790
|
+
nav.hidden = entries.length === 0;
|
|
1791
|
+
if (nav.hidden) return;
|
|
1792
|
+
const separator = () => {
|
|
1793
|
+
const span = document.createElement('span');
|
|
1794
|
+
span.className = 'akari-breadcrumb-separator'; span.textContent = ' › ';
|
|
1795
|
+
return span;
|
|
1796
|
+
};
|
|
1797
|
+
const button = entry => {
|
|
1798
|
+
const node = document.createElement('button');
|
|
1799
|
+
node.type = 'button'; node.textContent = entry.label;
|
|
1800
|
+
const show = () => {
|
|
1801
|
+
let geometry;
|
|
1802
|
+
try { geometry = breadcrumbBounds(entry.target); } catch { return; }
|
|
1803
|
+
if (!geometry?.rect || geometry.rect.width <= 0 || geometry.rect.height <= 0) return;
|
|
1804
|
+
breadcrumbHoverActive = true;
|
|
1805
|
+
showHoverFrame(geometry.rect, geometry.angle ?? null, entry.target.id ?? '', true);
|
|
1806
|
+
};
|
|
1807
|
+
node.addEventListener('pointerenter', () => {
|
|
1808
|
+
if (!breadcrumbHoverSuppressedAt) show();
|
|
1766
1809
|
});
|
|
1767
|
-
|
|
1768
|
-
|
|
1810
|
+
node.addEventListener('pointermove', event => {
|
|
1811
|
+
const point = breadcrumbHoverSuppressedAt;
|
|
1812
|
+
if (!point || Math.hypot(event.clientX - point.x, event.clientY - point.y) < 1) return;
|
|
1813
|
+
breadcrumbHoverSuppressedAt = null;
|
|
1814
|
+
show();
|
|
1815
|
+
});
|
|
1816
|
+
node.addEventListener('focus', show);
|
|
1817
|
+
node.addEventListener('pointerleave', () => {
|
|
1818
|
+
if (node.isConnected) breadcrumbHoverSuppressedAt = null;
|
|
1819
|
+
clearBreadcrumbHover();
|
|
1820
|
+
});
|
|
1821
|
+
node.addEventListener('blur', clearBreadcrumbHover);
|
|
1822
|
+
node.addEventListener('click', event => {
|
|
1823
|
+
breadcrumbHoverSuppressedAt = { x: event.clientX, y: event.clientY };
|
|
1824
|
+
clearBreadcrumbHover(); entry.action();
|
|
1825
|
+
});
|
|
1826
|
+
return node;
|
|
1827
|
+
};
|
|
1828
|
+
const add = (entry, index) => {
|
|
1829
|
+
if (index) nav.appendChild(separator());
|
|
1830
|
+
nav.appendChild(button(entry));
|
|
1831
|
+
};
|
|
1832
|
+
entries.forEach(add);
|
|
1833
|
+
const buttons = [...nav.querySelectorAll('button')];
|
|
1834
|
+
const widths = buttons.map(node => node.getBoundingClientRect().width);
|
|
1835
|
+
const separatorWidth = nav.querySelector('.akari-breadcrumb-separator')?.getBoundingClientRect().width ?? 0;
|
|
1836
|
+
const available = Math.max(0, nav.parentElement.getBoundingClientRect().width - 16);
|
|
1837
|
+
if (widths.reduce((sum, width) => sum + width, 0) + separatorWidth * (entries.length - 1) <= available) return;
|
|
1838
|
+
if (entries.length < 4) {
|
|
1839
|
+
const each = Math.max(3 * 10, (available - separatorWidth * (entries.length - 1) - widths[0]) / Math.max(1, entries.length - 1));
|
|
1840
|
+
buttons.slice(1).forEach(node => { node.style.maxWidth = `${each}px`; });
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
const ellipsis = document.createElement('span');
|
|
1844
|
+
ellipsis.textContent = '…'; ellipsis.setAttribute('aria-hidden', 'true');
|
|
1845
|
+
nav.appendChild(ellipsis);
|
|
1846
|
+
const ellipsisWidth = ellipsis.getBoundingClientRect().width;
|
|
1847
|
+
ellipsis.remove();
|
|
1848
|
+
let start = entries.length - 2;
|
|
1849
|
+
let used = widths[0] + widths[start] + widths[start + 1] + ellipsisWidth + 3 * separatorWidth;
|
|
1850
|
+
while (start > 2 && used + widths[start - 1] + separatorWidth <= available) {
|
|
1851
|
+
start--; used += widths[start] + separatorWidth;
|
|
1852
|
+
}
|
|
1853
|
+
nav.replaceChildren();
|
|
1854
|
+
add(entries[0], 0);
|
|
1855
|
+
nav.appendChild(separator()); nav.appendChild(ellipsis);
|
|
1856
|
+
for (let index = start; index < entries.length; index++) add(entries[index], index);
|
|
1857
|
+
if (used > available) {
|
|
1858
|
+
const tails = [...nav.querySelectorAll('button')].slice(1);
|
|
1859
|
+
const tailWidth = Math.max(30, (available - widths[0] - ellipsisWidth - 3 * separatorWidth) / tails.length);
|
|
1860
|
+
tails.forEach(node => { node.style.maxWidth = `${tailWidth}px`; });
|
|
1861
|
+
}
|
|
1769
1862
|
}
|
|
1770
1863
|
function publishScopedSelection(notify = true) {
|
|
1771
1864
|
syncLazyBag();
|
|
@@ -2117,6 +2210,13 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
2117
2210
|
const parts = String(value ?? '').match(/[-+]?(?:\d*\.)?\d+(?:e[-+]?\d+)?px/giu) ?? [];
|
|
2118
2211
|
return { x: Number.parseFloat(parts[0]) || 0, y: Number.parseFloat(parts[1]) || 0 };
|
|
2119
2212
|
}
|
|
2213
|
+
const elementDecimal = new Intl.NumberFormat('en-US', { useGrouping: false, maximumFractionDigits: 2 });
|
|
2214
|
+
function formatElementNumber(value) {
|
|
2215
|
+
return elementDecimal.format(Math.round((value + Number.EPSILON) * 100) / 100 || 0);
|
|
2216
|
+
}
|
|
2217
|
+
function formatElementTranslate(x, y) {
|
|
2218
|
+
return `${formatElementNumber(x)}px ${formatElementNumber(y)}px`;
|
|
2219
|
+
}
|
|
2120
2220
|
function elementScreenGeometry(element) {
|
|
2121
2221
|
const rect = element.getBoundingClientRect();
|
|
2122
2222
|
const center = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
|
@@ -2185,14 +2285,18 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
2185
2285
|
}
|
|
2186
2286
|
function elementWriteStyle(gesture, dimensionNames = []) {
|
|
2187
2287
|
const style = {};
|
|
2188
|
-
for (const name of dimensionNames)
|
|
2288
|
+
for (const name of dimensionNames) {
|
|
2289
|
+
style[name] = `${formatElementNumber(Number.parseFloat(gesture.element.style.getPropertyValue(name)))}px`;
|
|
2290
|
+
}
|
|
2189
2291
|
for (const name of elementInlineProperties) {
|
|
2190
2292
|
if (name === 'translate' || dimensionNames.includes(name)) continue;
|
|
2191
2293
|
const now = gesture.element.style.getPropertyValue(name);
|
|
2192
|
-
if (now && now !== gesture.originalInline[name][0])
|
|
2294
|
+
if (now && now !== gesture.originalInline[name][0]) {
|
|
2295
|
+
style[name] = name === 'rotate' ? `${formatElementNumber(Number.parseFloat(now))}deg` : now;
|
|
2296
|
+
}
|
|
2193
2297
|
}
|
|
2194
|
-
const translate = gesture.
|
|
2195
|
-
if (translate
|
|
2298
|
+
const translate = formatElementTranslate(gesture.x, gesture.y);
|
|
2299
|
+
if (translate !== formatElementTranslate(gesture.startX, gesture.startY)) style.translate = translate;
|
|
2196
2300
|
return style;
|
|
2197
2301
|
}
|
|
2198
2302
|
function beginElementGesture(event, handleEl) {
|
|
@@ -2214,7 +2318,8 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
2214
2318
|
startGeometry: before, startClientX: event.clientX, startClientY: event.clientY,
|
|
2215
2319
|
startWidth: Number.parseFloat(declared.width) || before.width,
|
|
2216
2320
|
startHeight: Number.parseFloat(declared.height) || before.height,
|
|
2217
|
-
x: translate.x, y: translate.y,
|
|
2321
|
+
x: translate.x, y: translate.y, startX: translate.x, startY: translate.y,
|
|
2322
|
+
moved: false, writeContext: captureWriteContext() };
|
|
2218
2323
|
if (startStyle.display === 'inline') {
|
|
2219
2324
|
element.style.display = 'inline-block';
|
|
2220
2325
|
const next = element.getBoundingClientRect();
|
|
@@ -2362,12 +2467,15 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
2362
2467
|
const gesture = elementNudge;
|
|
2363
2468
|
elementNudge = null;
|
|
2364
2469
|
if (!gesture) return;
|
|
2365
|
-
|
|
2470
|
+
const translate = formatElementTranslate(gesture.x, gesture.y);
|
|
2471
|
+
if (Math.abs(gesture.x - gesture.startX) < 0.5 && Math.abs(gesture.y - gesture.startY) < 0.5
|
|
2472
|
+
|| translate === formatElementTranslate(gesture.startX, gesture.startY)) {
|
|
2366
2473
|
gesture.element.style.translate = gesture.originalInline;
|
|
2367
2474
|
if (gesture.originalDisplay !== undefined) gesture.element.style.display = gesture.originalDisplay;
|
|
2475
|
+
refreshSelectionFrame();
|
|
2368
2476
|
return;
|
|
2369
2477
|
}
|
|
2370
|
-
const style = { translate
|
|
2478
|
+
const style = { translate };
|
|
2371
2479
|
if (gesture.originalDisplay !== undefined) style.display = 'inline-block';
|
|
2372
2480
|
enqueueWrite(gesture.writeContext, gesture.overlayId,
|
|
2373
2481
|
{ element: { ref: gesture.ref, tag: gesture.tag,
|
|
@@ -2475,17 +2583,41 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
2475
2583
|
}
|
|
2476
2584
|
|
|
2477
2585
|
function hideHover() {
|
|
2586
|
+
breadcrumbHoverActive = false;
|
|
2478
2587
|
hoverEvent = null;
|
|
2479
2588
|
if (hoverTick !== null) cancelAnimationFrame(hoverTick);
|
|
2480
2589
|
hoverTick = null;
|
|
2481
|
-
if (hoverFrame)
|
|
2590
|
+
if (hoverFrame) {
|
|
2591
|
+
hoverFrame.hidden = true;
|
|
2592
|
+
hoverFrame.removeAttribute('data-breadcrumb-hover');
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
function showHoverFrame(rect, angle = null, overlayId = '', breadcrumb = false, ariaHidden = true) {
|
|
2596
|
+
if (!hoverFrame) {
|
|
2597
|
+
hoverFrame = document.createElement('div');
|
|
2598
|
+
hoverFrame.setAttribute('data-akari-ui', 'preview-hover-frame');
|
|
2599
|
+
if (ariaHidden) hoverFrame.setAttribute('aria-hidden', 'true');
|
|
2600
|
+
Object.assign(hoverFrame.style, { position: 'fixed', pointerEvents: 'none',
|
|
2601
|
+
boxSizing: 'border-box', border: '1px solid var(--akari-accent, #4da3ff)',
|
|
2602
|
+
opacity: '0.45', zIndex: '90' });
|
|
2603
|
+
document.body.appendChild(hoverFrame);
|
|
2604
|
+
}
|
|
2605
|
+
if (breadcrumb) hoverFrame.setAttribute('data-breadcrumb-hover', 'true');
|
|
2606
|
+
else hoverFrame.removeAttribute('data-breadcrumb-hover');
|
|
2607
|
+
hoverFrame.dataset.overlayId = overlayId;
|
|
2608
|
+
hoverFrame.hidden = false;
|
|
2609
|
+
Object.assign(hoverFrame.style, { left: `${rect.left}px`, top: `${rect.top}px`,
|
|
2610
|
+
width: `${rect.width}px`, height: `${rect.height}px`,
|
|
2611
|
+
transform: angle === null ? '' : `rotate(${angle}deg)` });
|
|
2482
2612
|
}
|
|
2483
2613
|
function scheduleHover(event) {
|
|
2614
|
+
if (breadcrumbHoverActive) return;
|
|
2484
2615
|
if (!interactionEnabled || activeDrag || activeResize || activeEdit || event.buttons) { hideHover(); return; }
|
|
2485
2616
|
hoverEvent = event;
|
|
2486
2617
|
if (hoverTick !== null) return;
|
|
2487
2618
|
hoverTick = requestAnimationFrame(() => {
|
|
2488
2619
|
hoverTick = null;
|
|
2620
|
+
if (breadcrumbHoverActive) { hoverEvent = null; return; }
|
|
2489
2621
|
const event = hoverEvent;
|
|
2490
2622
|
if (!event || activeDrag || activeResize || activeEdit) { hideHover(); return; }
|
|
2491
2623
|
const container = overlayForEvent(event);
|
|
@@ -2501,17 +2633,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
2501
2633
|
const rect = element?.getBoundingClientRect() ?? fragmentBounds(container);
|
|
2502
2634
|
if (!rect || selectedOverlay === container
|
|
2503
2635
|
&& selectedElementFocus()?.ref === elementAddress(root, element)) { hideHover(); return; }
|
|
2504
|
-
|
|
2505
|
-
hoverFrame = document.createElement('div');
|
|
2506
|
-
hoverFrame.setAttribute('data-akari-ui', 'preview-hover-frame');
|
|
2507
|
-
Object.assign(hoverFrame.style, { position: 'fixed', pointerEvents: 'none', boxSizing: 'border-box',
|
|
2508
|
-
border: '1px solid var(--akari-accent, #4da3ff)', opacity: '0.45', zIndex: '90' });
|
|
2509
|
-
document.body.appendChild(hoverFrame);
|
|
2510
|
-
}
|
|
2511
|
-
hoverFrame.dataset.overlayId = container.dataset.overlayId;
|
|
2512
|
-
hoverFrame.hidden = false;
|
|
2513
|
-
Object.assign(hoverFrame.style, { left: `${rect.left}px`, top: `${rect.top}px`,
|
|
2514
|
-
width: `${rect.width}px`, height: `${rect.height}px`, transform: '' });
|
|
2636
|
+
showHoverFrame(rect, null, container.dataset.overlayId, false, false);
|
|
2515
2637
|
return;
|
|
2516
2638
|
}
|
|
2517
2639
|
const next = isSelectable(container) && resolveScopedSelection(selectionTree(), scopeId,
|
|
@@ -2524,20 +2646,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
2524
2646
|
const rect = node && node.kind !== 'leaf' ? unionBounds(visibleMembers(next.selectId))
|
|
2525
2647
|
: leaf?.dataset.role === 'shape-line' ? lineRect : leaf ? fragmentBounds(leaf) : null;
|
|
2526
2648
|
if (!rect) { hideHover(); return; }
|
|
2527
|
-
|
|
2528
|
-
hoverFrame = document.createElement('div');
|
|
2529
|
-
hoverFrame.setAttribute('data-akari-ui', 'preview-hover-frame');
|
|
2530
|
-
hoverFrame.setAttribute('aria-hidden', 'true');
|
|
2531
|
-
// Standalone hosts also get a non-interactive, viewport-based frame.
|
|
2532
|
-
Object.assign(hoverFrame.style, { position: 'fixed', pointerEvents: 'none',
|
|
2533
|
-
boxSizing: 'border-box', border: '1px solid var(--akari-accent, #4da3ff)', opacity: '0.45', zIndex: '90' });
|
|
2534
|
-
document.body.appendChild(hoverFrame);
|
|
2535
|
-
}
|
|
2536
|
-
hoverFrame.dataset.overlayId = next.selectId;
|
|
2537
|
-
hoverFrame.hidden = false;
|
|
2538
|
-
Object.assign(hoverFrame.style, { left: `${rect.left}px`, top: `${rect.top}px`,
|
|
2539
|
-
width: `${rect.width}px`, height: `${rect.height}px`,
|
|
2540
|
-
transform: lineRect ? `rotate(${lineRect.angle}deg)` : '' });
|
|
2649
|
+
showHoverFrame(rect, lineRect?.angle ?? null, next.selectId);
|
|
2541
2650
|
});
|
|
2542
2651
|
}
|
|
2543
2652
|
|
|
@@ -2592,13 +2701,15 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
2592
2701
|
const displayScale = stage?.getBoundingClientRect().width / outputSize().width || 1;
|
|
2593
2702
|
const outputDx = (rect.left + rect.width / 2 - drag.startCenterX) / displayScale;
|
|
2594
2703
|
const outputDy = (rect.top + rect.height / 2 - drag.startCenterY) / displayScale;
|
|
2595
|
-
|
|
2704
|
+
const translate = formatElementTranslate(drag.x, drag.y);
|
|
2705
|
+
if (!drag.moved || (Math.abs(outputDx) < 0.5 && Math.abs(outputDy) < 0.5)
|
|
2706
|
+
|| translate === formatElementTranslate(drag.startX, drag.startY)) {
|
|
2596
2707
|
drag.element.style.translate = drag.originalInline;
|
|
2597
2708
|
if (drag.originalDisplay !== undefined) drag.element.style.display = drag.originalDisplay;
|
|
2598
2709
|
refreshSelectionFrame();
|
|
2599
2710
|
return null;
|
|
2600
2711
|
}
|
|
2601
|
-
const style = { translate
|
|
2712
|
+
const style = { translate };
|
|
2602
2713
|
if (drag.originalDisplay !== undefined) style.display = 'inline-block';
|
|
2603
2714
|
const record = enqueueWrite(drag.writeContext, drag.overlayId,
|
|
2604
2715
|
{ element: { ref: drag.elementRef, tag: drag.elementTag,
|
|
@@ -3887,8 +3998,8 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
3887
3998
|
hideHover();
|
|
3888
3999
|
clickOrigin = { selectedId, scopeId, moved: false, hadMultiple: selectedIds.length > 1 };
|
|
3889
4000
|
|
|
4001
|
+
if (event.target instanceof Element && event.target.closest('[data-akari-ui="preview-scope-breadcrumb"]')) return;
|
|
3890
4002
|
if (selectionTree().length) {
|
|
3891
|
-
if (event.target instanceof Element && event.target.closest('[data-akari-ui="preview-scope-breadcrumb"]')) return;
|
|
3892
4003
|
const handle = findHandleElement(event.target);
|
|
3893
4004
|
if (handle) {
|
|
3894
4005
|
if (selectedIds.length > 1) return;
|