akari-video 1.2.0-beta.5 → 1.3.0-beta.1
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 +369 -65
- package/vendor/packages/overlay-runtime/src/overlay-runtime.js +22 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0-beta.1",
|
|
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.
|
|
3
|
+
"version": "1.3.0-beta.1",
|
|
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--) {
|
|
@@ -340,6 +340,96 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
340
340
|
// テキスト編集で断片 HTML を保存するときに作者の元指定へ戻せるよう、初回値だけ保持する。
|
|
341
341
|
const hitPolicyOriginalPointerEvents = new WeakMap();
|
|
342
342
|
const hitPolicyAppliedContainers = new WeakSet();
|
|
343
|
+
const hitPolicyTimes = new WeakMap();
|
|
344
|
+
const staleHitPolicyContainers = new Set();
|
|
345
|
+
const pausedHitPolicyContainers = new Set();
|
|
346
|
+
let pausedHitPolicyTimer = null;
|
|
347
|
+
let lastPointerHitPolicyRefresh = -Infinity;
|
|
348
|
+
|
|
349
|
+
function hitPolicyOperationBusy() {
|
|
350
|
+
return Boolean(activeEdit || activeDrag || activeResize || activeRotate || activeLine
|
|
351
|
+
|| marqueeFrame || pointerOwner != null);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function schedulePausedHitPolicyRefresh(delay = 120) {
|
|
355
|
+
if (typeof setTimeout !== 'function') return;
|
|
356
|
+
if (pausedHitPolicyTimer !== null) clearTimeout(pausedHitPolicyTimer);
|
|
357
|
+
pausedHitPolicyTimer = setTimeout(flushPausedHitPolicies, delay);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// runtime は可視コンテナの時刻だけを記録する。CSS の読み取りはここでは行わない。
|
|
361
|
+
function markOverlayHitPolicyStale(container, localTimeMs, paused = false, reentered = false,
|
|
362
|
+
animationReady = null) {
|
|
363
|
+
if (!container) return;
|
|
364
|
+
let state = hitPolicyTimes.get(container);
|
|
365
|
+
if (!state) {
|
|
366
|
+
state = { current: localTimeMs, measured: null, paused: null };
|
|
367
|
+
hitPolicyTimes.set(container, state);
|
|
368
|
+
}
|
|
369
|
+
const timeChanged = state.current !== localTimeMs;
|
|
370
|
+
const enteredPause = paused && state.paused !== true;
|
|
371
|
+
state.current = localTimeMs;
|
|
372
|
+
state.paused = paused;
|
|
373
|
+
if (reentered) {
|
|
374
|
+
state.measured = null;
|
|
375
|
+
hitPolicyAppliedContainers.delete(container);
|
|
376
|
+
}
|
|
377
|
+
if (state.measured !== localTimeMs) staleHitPolicyContainers.add(container);
|
|
378
|
+
// pause()/currentTime の pending 補正はこの tick の測定より後に解決し得る。
|
|
379
|
+
// 暫定 apply が stale を消しても、停止後の一回は別の印で必ず残す。
|
|
380
|
+
if (paused && (reentered || timeChanged || enteredPause)) pausedHitPolicyContainers.add(container);
|
|
381
|
+
if (!paused) pausedHitPolicyContainers.delete(container);
|
|
382
|
+
if (paused && pausedHitPolicyContainers.size) {
|
|
383
|
+
// スクラブ中は連続 tick をまとめ、停止後にポインタ無しで確定する。
|
|
384
|
+
schedulePausedHitPolicyRefresh();
|
|
385
|
+
}
|
|
386
|
+
if (paused && (reentered || timeChanged || enteredPause) && animationReady?.then) {
|
|
387
|
+
const expectedState = state;
|
|
388
|
+
animationReady.then(() => {
|
|
389
|
+
if (hitPolicyTimes.get(container) !== expectedState || !state.paused
|
|
390
|
+
|| state.current !== localTimeMs) return;
|
|
391
|
+
// ready 後の currentTime 補正が 120ms の測定より遅れた場合も追い付く。
|
|
392
|
+
const alreadyRefreshed = !pausedHitPolicyContainers.has(container);
|
|
393
|
+
pausedHitPolicyContainers.add(container);
|
|
394
|
+
schedulePausedHitPolicyRefresh(alreadyRefreshed ? 0 : 120);
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function flushPausedHitPolicies() {
|
|
400
|
+
pausedHitPolicyTimer = null;
|
|
401
|
+
if (pausedHitPolicyContainers.size === 0) return;
|
|
402
|
+
if (hitPolicyOperationBusy()) {
|
|
403
|
+
// 編集・ジェスチャーが終わるまで印を保持する。
|
|
404
|
+
pausedHitPolicyTimer = setTimeout(flushPausedHitPolicies, 120);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
refreshStaleHitPolicies(true);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function refreshStaleHitPolicies(includePaused = false) {
|
|
411
|
+
if (hitPolicyOperationBusy()) return false;
|
|
412
|
+
let refreshed = false;
|
|
413
|
+
const candidates = includePaused ? pausedHitPolicyContainers : staleHitPolicyContainers;
|
|
414
|
+
for (const container of candidates) {
|
|
415
|
+
if (includePaused) pausedHitPolicyContainers.delete(container);
|
|
416
|
+
if (!container.isConnected || container.style.visibility === 'hidden') {
|
|
417
|
+
staleHitPolicyContainers.delete(container);
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
invalidateOverlayHitPolicy(container);
|
|
421
|
+
applyOverlayHitPolicy(container);
|
|
422
|
+
refreshed = true;
|
|
423
|
+
}
|
|
424
|
+
return refreshed;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function forgetOverlayHitPolicyTracking(container) {
|
|
428
|
+
staleHitPolicyContainers.delete(container);
|
|
429
|
+
pausedHitPolicyContainers.delete(container);
|
|
430
|
+
hitPolicyTimes.delete(container);
|
|
431
|
+
hitPolicyAppliedContainers.delete(container);
|
|
432
|
+
}
|
|
343
433
|
|
|
344
434
|
// overlay_write は操作順を保存する。selftest は今回の書き込みをこの記録から待つ。
|
|
345
435
|
let writeTail = Promise.resolve();
|
|
@@ -982,7 +1072,8 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
982
1072
|
|
|
983
1073
|
// 全画面の外側コンテナと断片ルートは素通しにし、実際に背景・枠・影・文字・置換要素を
|
|
984
1074
|
// 描く可視の子孫だけを拾う。data-akari-hit は最寄りの指定を配下へ継承し、機械判定より
|
|
985
|
-
// 優先する。字幕が 1,000 件級でも全件を走査しないよう、runtime
|
|
1075
|
+
// 優先する。字幕が 1,000 件級でも全件を走査しないよう、runtime は可視コンテナの
|
|
1076
|
+
// 初回・停止後・ポインタ入力時だけ適用し、再生 tick では時刻の印だけを付ける。
|
|
986
1077
|
function fragmentRootCoversContainer(element, container) {
|
|
987
1078
|
const rootRect = element.getBoundingClientRect();
|
|
988
1079
|
const containerRect = container.getBoundingClientRect();
|
|
@@ -1056,6 +1147,9 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1056
1147
|
|
|
1057
1148
|
for (const root of container.children) visit(root, null, true, true);
|
|
1058
1149
|
hitPolicyAppliedContainers.add(container);
|
|
1150
|
+
const state = hitPolicyTimes.get(container);
|
|
1151
|
+
if (state) state.measured = state.current;
|
|
1152
|
+
staleHitPolicyContainers.delete(container);
|
|
1059
1153
|
updateShapeLineHitProxyWidths();
|
|
1060
1154
|
}
|
|
1061
1155
|
|
|
@@ -1169,6 +1263,34 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1169
1263
|
}
|
|
1170
1264
|
}
|
|
1171
1265
|
|
|
1266
|
+
// 08-07 裁定: 背景は選択・削除できる。旧矩形フォールバックと違い、stage 面に
|
|
1267
|
+
// 落ちたクリックで、全面を覆って自ら塗るルートだけを拾う。実際のヒット先が
|
|
1268
|
+
// あるときは使わず 4680d90d8 の巻き込みを防ぎ、素通し規約は変えない。
|
|
1269
|
+
function paintedStageOverlayAt(clientX, clientY) {
|
|
1270
|
+
const candidates = [...stage.children]
|
|
1271
|
+
.flatMap((container, index) => {
|
|
1272
|
+
if (!isSelectable(container)) return [];
|
|
1273
|
+
const value = Number.parseFloat(getComputedStyle(container).zIndex);
|
|
1274
|
+
return [{ container, index, zIndex: Number.isFinite(value) ? value : 0 }];
|
|
1275
|
+
})
|
|
1276
|
+
.sort((a, b) => b.zIndex - a.zIndex || b.index - a.index);
|
|
1277
|
+
|
|
1278
|
+
for (const { container } of candidates) {
|
|
1279
|
+
for (const root of container.children) {
|
|
1280
|
+
if (root.hasAttribute("data-akari-interaction") || root.getAttribute("data-akari-hit") === "pass") continue;
|
|
1281
|
+
const style = getComputedStyle(root);
|
|
1282
|
+
if (style.display === "none" || Number(style.opacity) <= 0
|
|
1283
|
+
|| ["hidden", "collapse"].includes(style.visibility)
|
|
1284
|
+
|| !fragmentRootCoversContainer(root, container)
|
|
1285
|
+
|| !drawsOwnContent(root, style)) continue;
|
|
1286
|
+
const rect = root.getBoundingClientRect();
|
|
1287
|
+
if (clientX >= rect.left && clientX <= rect.right
|
|
1288
|
+
&& clientY >= rect.top && clientY <= rect.bottom) return container;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
return null;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1172
1294
|
function overlayForEvent(event) {
|
|
1173
1295
|
const eventTargetOverlay = findOverlayContainer(event.target);
|
|
1174
1296
|
if (eventTargetOverlay && !isSelectable(eventTargetOverlay)) {
|
|
@@ -1180,6 +1302,11 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1180
1302
|
) {
|
|
1181
1303
|
return selftestOverlayOverride;
|
|
1182
1304
|
}
|
|
1305
|
+
if (event.target === stage && !eventTargetOverlay
|
|
1306
|
+
&& Number.isFinite(event.clientX) && Number.isFinite(event.clientY)) {
|
|
1307
|
+
const paintedOverlay = paintedStageOverlayAt(event.clientX, event.clientY);
|
|
1308
|
+
if (paintedOverlay) return paintedOverlay;
|
|
1309
|
+
}
|
|
1183
1310
|
return isSelectable(eventTargetOverlay) ? eventTargetOverlay : null;
|
|
1184
1311
|
}
|
|
1185
1312
|
|
|
@@ -1722,50 +1849,143 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1722
1849
|
Object.assign(selectionFrame.style, { left: `${rect.left}px`, top: `${rect.top}px`,
|
|
1723
1850
|
width: `${rect.width}px`, height: `${rect.height}px` });
|
|
1724
1851
|
}
|
|
1852
|
+
let breadcrumbResizeObserver = null;
|
|
1853
|
+
let breadcrumbHoverActive = false;
|
|
1854
|
+
let breadcrumbHoverSuppressedAt = null;
|
|
1855
|
+
function clearBreadcrumbHover() {
|
|
1856
|
+
if (!breadcrumbHoverActive) return;
|
|
1857
|
+
hideHover();
|
|
1858
|
+
}
|
|
1859
|
+
function breadcrumbBounds(target) {
|
|
1860
|
+
if (target.kind === 'stage') return { rect: stage?.getBoundingClientRect() };
|
|
1861
|
+
if (target.kind === 'node') {
|
|
1862
|
+
const node = treeNode(target.id);
|
|
1863
|
+
const leaf = containerById(target.id);
|
|
1864
|
+
const lineRect = leaf && lineFrameGeometry(leaf);
|
|
1865
|
+
const rect = node?.kind !== 'leaf' ? unionBounds(visibleMembers(target.id))
|
|
1866
|
+
: lineRect ?? (leaf ? fragmentBounds(leaf) : null);
|
|
1867
|
+
return { rect, angle: lineRect?.angle ?? null };
|
|
1868
|
+
}
|
|
1869
|
+
if (target.kind === 'item') {
|
|
1870
|
+
const container = containerById(target.id);
|
|
1871
|
+
return { rect: container ? fragmentBounds(container) : null };
|
|
1872
|
+
}
|
|
1873
|
+
const element = target.element?.isConnected ? target.element
|
|
1874
|
+
: elementByAddress(fragmentRoot(containerById(target.overlayId)), target.ref);
|
|
1875
|
+
if (!element?.isConnected) return null;
|
|
1876
|
+
const geometry = elementScreenGeometry(element);
|
|
1877
|
+
const width = geometry.width * Math.hypot(geometry.axes.x.x, geometry.axes.x.y);
|
|
1878
|
+
const height = geometry.height * Math.hypot(geometry.axes.y.x, geometry.axes.y.y);
|
|
1879
|
+
return { rect: { left: geometry.center.x - width / 2, top: geometry.center.y - height / 2,
|
|
1880
|
+
width, height }, angle: Math.atan2(geometry.axes.x.y, geometry.axes.x.x) * 180 / Math.PI };
|
|
1881
|
+
}
|
|
1725
1882
|
function renderScopeBreadcrumb() {
|
|
1726
1883
|
const nav = document.querySelector('[data-akari-ui="preview-scope-breadcrumb"]');
|
|
1727
1884
|
if (!nav) return;
|
|
1885
|
+
if (!breadcrumbResizeObserver && typeof ResizeObserver !== 'undefined' && nav.parentElement) {
|
|
1886
|
+
breadcrumbResizeObserver = new ResizeObserver(() => renderScopeBreadcrumb());
|
|
1887
|
+
breadcrumbResizeObserver.observe(nav.parentElement);
|
|
1888
|
+
}
|
|
1889
|
+
clearBreadcrumbHover();
|
|
1890
|
+
const entries = [];
|
|
1891
|
+
const append = (label, action, target) => entries.push({ label, action, target });
|
|
1728
1892
|
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(); });
|
|
1893
|
+
append('全体', () => { clearSelection(); publishScopedSelection(); }, { kind: 'stage' });
|
|
1739
1894
|
for (const id of lineage(selectionTree(), scopeId)) {
|
|
1740
|
-
append(treeNode(id)?.label ?? id, () => applyScopedSelection({ selectId: id, scopeId: treeNode(id)?.parentId ?? null })
|
|
1895
|
+
append(treeNode(id)?.label ?? id, () => applyScopedSelection({ selectId: id, scopeId: treeNode(id)?.parentId ?? null }),
|
|
1896
|
+
{ kind: 'node', id });
|
|
1741
1897
|
}
|
|
1742
1898
|
const name = window.akari.state?.summary?.overlays?.find(overlay => overlay.id === selectedId)?.name ?? selectedId;
|
|
1743
|
-
append(name, () => focusElement(null));
|
|
1899
|
+
append(name, () => focusElement(null), { kind: 'item', id: selectedId });
|
|
1744
1900
|
const root = fragmentRoot(selectedOverlay);
|
|
1745
1901
|
const path = [];
|
|
1746
1902
|
for (let node = focusedElement(); node && node !== root; node = node.parentElement) {
|
|
1747
1903
|
if (selectableElement(root, node, stage?.getBoundingClientRect())) path.unshift(node);
|
|
1748
1904
|
}
|
|
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', () => {
|
|
1905
|
+
for (const node of path) append(elementLabel(root, node), () => focusElement(node),
|
|
1906
|
+
{ kind: 'element', element: node, overlayId: selectedId, ref: elementAddress(root, node) });
|
|
1907
|
+
} else if (scopeId !== floorScopeId && selectionTree().length) {
|
|
1908
|
+
const path = lineage(selectionTree(), scopeId);
|
|
1909
|
+
const ids = floorScopeId === null ? [null, ...path] : path.slice(path.indexOf(floorScopeId));
|
|
1910
|
+
ids.forEach(id => append(id === null ? '全体' : treeNode(id)?.label ?? id, () => {
|
|
1763
1911
|
const hit = descendantLeafIds(selectionTree(), selectedId)[0] ?? selectedId;
|
|
1764
1912
|
const next = resolveScopedSelection(selectionTree(), id, hit);
|
|
1765
1913
|
applyScopedSelection({ scopeId: id, selectId: next.scopeId === id ? next.selectId : null });
|
|
1914
|
+
}, id === null ? { kind: 'stage' } : { kind: 'node', id }));
|
|
1915
|
+
}
|
|
1916
|
+
nav.replaceChildren();
|
|
1917
|
+
nav.hidden = entries.length === 0;
|
|
1918
|
+
if (nav.hidden) return;
|
|
1919
|
+
const separator = () => {
|
|
1920
|
+
const span = document.createElement('span');
|
|
1921
|
+
span.className = 'akari-breadcrumb-separator'; span.textContent = ' › ';
|
|
1922
|
+
return span;
|
|
1923
|
+
};
|
|
1924
|
+
const button = entry => {
|
|
1925
|
+
const node = document.createElement('button');
|
|
1926
|
+
node.type = 'button'; node.textContent = entry.label;
|
|
1927
|
+
const show = () => {
|
|
1928
|
+
let geometry;
|
|
1929
|
+
try { geometry = breadcrumbBounds(entry.target); } catch { return; }
|
|
1930
|
+
if (!geometry?.rect || geometry.rect.width <= 0 || geometry.rect.height <= 0) return;
|
|
1931
|
+
breadcrumbHoverActive = true;
|
|
1932
|
+
showHoverFrame(geometry.rect, geometry.angle ?? null, entry.target.id ?? '', true);
|
|
1933
|
+
};
|
|
1934
|
+
node.addEventListener('pointerenter', () => {
|
|
1935
|
+
if (!breadcrumbHoverSuppressedAt) show();
|
|
1766
1936
|
});
|
|
1767
|
-
|
|
1768
|
-
|
|
1937
|
+
node.addEventListener('pointermove', event => {
|
|
1938
|
+
const point = breadcrumbHoverSuppressedAt;
|
|
1939
|
+
if (!point || Math.hypot(event.clientX - point.x, event.clientY - point.y) < 1) return;
|
|
1940
|
+
breadcrumbHoverSuppressedAt = null;
|
|
1941
|
+
show();
|
|
1942
|
+
});
|
|
1943
|
+
node.addEventListener('focus', show);
|
|
1944
|
+
node.addEventListener('pointerleave', () => {
|
|
1945
|
+
if (node.isConnected) breadcrumbHoverSuppressedAt = null;
|
|
1946
|
+
clearBreadcrumbHover();
|
|
1947
|
+
});
|
|
1948
|
+
node.addEventListener('blur', clearBreadcrumbHover);
|
|
1949
|
+
node.addEventListener('click', event => {
|
|
1950
|
+
breadcrumbHoverSuppressedAt = { x: event.clientX, y: event.clientY };
|
|
1951
|
+
clearBreadcrumbHover(); entry.action();
|
|
1952
|
+
});
|
|
1953
|
+
return node;
|
|
1954
|
+
};
|
|
1955
|
+
const add = (entry, index) => {
|
|
1956
|
+
if (index) nav.appendChild(separator());
|
|
1957
|
+
nav.appendChild(button(entry));
|
|
1958
|
+
};
|
|
1959
|
+
entries.forEach(add);
|
|
1960
|
+
const buttons = [...nav.querySelectorAll('button')];
|
|
1961
|
+
const widths = buttons.map(node => node.getBoundingClientRect().width);
|
|
1962
|
+
const separatorWidth = nav.querySelector('.akari-breadcrumb-separator')?.getBoundingClientRect().width ?? 0;
|
|
1963
|
+
const available = Math.max(0, nav.parentElement.getBoundingClientRect().width - 16);
|
|
1964
|
+
if (widths.reduce((sum, width) => sum + width, 0) + separatorWidth * (entries.length - 1) <= available) return;
|
|
1965
|
+
if (entries.length < 4) {
|
|
1966
|
+
const each = Math.max(3 * 10, (available - separatorWidth * (entries.length - 1) - widths[0]) / Math.max(1, entries.length - 1));
|
|
1967
|
+
buttons.slice(1).forEach(node => { node.style.maxWidth = `${each}px`; });
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
const ellipsis = document.createElement('span');
|
|
1971
|
+
ellipsis.textContent = '…'; ellipsis.setAttribute('aria-hidden', 'true');
|
|
1972
|
+
nav.appendChild(ellipsis);
|
|
1973
|
+
const ellipsisWidth = ellipsis.getBoundingClientRect().width;
|
|
1974
|
+
ellipsis.remove();
|
|
1975
|
+
let start = entries.length - 2;
|
|
1976
|
+
let used = widths[0] + widths[start] + widths[start + 1] + ellipsisWidth + 3 * separatorWidth;
|
|
1977
|
+
while (start > 2 && used + widths[start - 1] + separatorWidth <= available) {
|
|
1978
|
+
start--; used += widths[start] + separatorWidth;
|
|
1979
|
+
}
|
|
1980
|
+
nav.replaceChildren();
|
|
1981
|
+
add(entries[0], 0);
|
|
1982
|
+
nav.appendChild(separator()); nav.appendChild(ellipsis);
|
|
1983
|
+
for (let index = start; index < entries.length; index++) add(entries[index], index);
|
|
1984
|
+
if (used > available) {
|
|
1985
|
+
const tails = [...nav.querySelectorAll('button')].slice(1);
|
|
1986
|
+
const tailWidth = Math.max(30, (available - widths[0] - ellipsisWidth - 3 * separatorWidth) / tails.length);
|
|
1987
|
+
tails.forEach(node => { node.style.maxWidth = `${tailWidth}px`; });
|
|
1988
|
+
}
|
|
1769
1989
|
}
|
|
1770
1990
|
function publishScopedSelection(notify = true) {
|
|
1771
1991
|
syncLazyBag();
|
|
@@ -2117,6 +2337,13 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
2117
2337
|
const parts = String(value ?? '').match(/[-+]?(?:\d*\.)?\d+(?:e[-+]?\d+)?px/giu) ?? [];
|
|
2118
2338
|
return { x: Number.parseFloat(parts[0]) || 0, y: Number.parseFloat(parts[1]) || 0 };
|
|
2119
2339
|
}
|
|
2340
|
+
const elementDecimal = new Intl.NumberFormat('en-US', { useGrouping: false, maximumFractionDigits: 2 });
|
|
2341
|
+
function formatElementNumber(value) {
|
|
2342
|
+
return elementDecimal.format(Math.round((value + Number.EPSILON) * 100) / 100 || 0);
|
|
2343
|
+
}
|
|
2344
|
+
function formatElementTranslate(x, y) {
|
|
2345
|
+
return `${formatElementNumber(x)}px ${formatElementNumber(y)}px`;
|
|
2346
|
+
}
|
|
2120
2347
|
function elementScreenGeometry(element) {
|
|
2121
2348
|
const rect = element.getBoundingClientRect();
|
|
2122
2349
|
const center = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
|
@@ -2185,14 +2412,18 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
2185
2412
|
}
|
|
2186
2413
|
function elementWriteStyle(gesture, dimensionNames = []) {
|
|
2187
2414
|
const style = {};
|
|
2188
|
-
for (const name of dimensionNames)
|
|
2415
|
+
for (const name of dimensionNames) {
|
|
2416
|
+
style[name] = `${formatElementNumber(Number.parseFloat(gesture.element.style.getPropertyValue(name)))}px`;
|
|
2417
|
+
}
|
|
2189
2418
|
for (const name of elementInlineProperties) {
|
|
2190
2419
|
if (name === 'translate' || dimensionNames.includes(name)) continue;
|
|
2191
2420
|
const now = gesture.element.style.getPropertyValue(name);
|
|
2192
|
-
if (now && now !== gesture.originalInline[name][0])
|
|
2421
|
+
if (now && now !== gesture.originalInline[name][0]) {
|
|
2422
|
+
style[name] = name === 'rotate' ? `${formatElementNumber(Number.parseFloat(now))}deg` : now;
|
|
2423
|
+
}
|
|
2193
2424
|
}
|
|
2194
|
-
const translate = gesture.
|
|
2195
|
-
if (translate
|
|
2425
|
+
const translate = formatElementTranslate(gesture.x, gesture.y);
|
|
2426
|
+
if (translate !== formatElementTranslate(gesture.startX, gesture.startY)) style.translate = translate;
|
|
2196
2427
|
return style;
|
|
2197
2428
|
}
|
|
2198
2429
|
function beginElementGesture(event, handleEl) {
|
|
@@ -2214,7 +2445,8 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
2214
2445
|
startGeometry: before, startClientX: event.clientX, startClientY: event.clientY,
|
|
2215
2446
|
startWidth: Number.parseFloat(declared.width) || before.width,
|
|
2216
2447
|
startHeight: Number.parseFloat(declared.height) || before.height,
|
|
2217
|
-
x: translate.x, y: translate.y,
|
|
2448
|
+
x: translate.x, y: translate.y, startX: translate.x, startY: translate.y,
|
|
2449
|
+
moved: false, writeContext: captureWriteContext() };
|
|
2218
2450
|
if (startStyle.display === 'inline') {
|
|
2219
2451
|
element.style.display = 'inline-block';
|
|
2220
2452
|
const next = element.getBoundingClientRect();
|
|
@@ -2362,12 +2594,15 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
2362
2594
|
const gesture = elementNudge;
|
|
2363
2595
|
elementNudge = null;
|
|
2364
2596
|
if (!gesture) return;
|
|
2365
|
-
|
|
2597
|
+
const translate = formatElementTranslate(gesture.x, gesture.y);
|
|
2598
|
+
if (Math.abs(gesture.x - gesture.startX) < 0.5 && Math.abs(gesture.y - gesture.startY) < 0.5
|
|
2599
|
+
|| translate === formatElementTranslate(gesture.startX, gesture.startY)) {
|
|
2366
2600
|
gesture.element.style.translate = gesture.originalInline;
|
|
2367
2601
|
if (gesture.originalDisplay !== undefined) gesture.element.style.display = gesture.originalDisplay;
|
|
2602
|
+
refreshSelectionFrame();
|
|
2368
2603
|
return;
|
|
2369
2604
|
}
|
|
2370
|
-
const style = { translate
|
|
2605
|
+
const style = { translate };
|
|
2371
2606
|
if (gesture.originalDisplay !== undefined) style.display = 'inline-block';
|
|
2372
2607
|
enqueueWrite(gesture.writeContext, gesture.overlayId,
|
|
2373
2608
|
{ element: { ref: gesture.ref, tag: gesture.tag,
|
|
@@ -2475,17 +2710,41 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
2475
2710
|
}
|
|
2476
2711
|
|
|
2477
2712
|
function hideHover() {
|
|
2713
|
+
breadcrumbHoverActive = false;
|
|
2478
2714
|
hoverEvent = null;
|
|
2479
2715
|
if (hoverTick !== null) cancelAnimationFrame(hoverTick);
|
|
2480
2716
|
hoverTick = null;
|
|
2481
|
-
if (hoverFrame)
|
|
2717
|
+
if (hoverFrame) {
|
|
2718
|
+
hoverFrame.hidden = true;
|
|
2719
|
+
hoverFrame.removeAttribute('data-breadcrumb-hover');
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
function showHoverFrame(rect, angle = null, overlayId = '', breadcrumb = false, ariaHidden = true) {
|
|
2723
|
+
if (!hoverFrame) {
|
|
2724
|
+
hoverFrame = document.createElement('div');
|
|
2725
|
+
hoverFrame.setAttribute('data-akari-ui', 'preview-hover-frame');
|
|
2726
|
+
if (ariaHidden) hoverFrame.setAttribute('aria-hidden', 'true');
|
|
2727
|
+
Object.assign(hoverFrame.style, { position: 'fixed', pointerEvents: 'none',
|
|
2728
|
+
boxSizing: 'border-box', border: '1px solid var(--akari-accent, #4da3ff)',
|
|
2729
|
+
opacity: '0.45', zIndex: '90' });
|
|
2730
|
+
document.body.appendChild(hoverFrame);
|
|
2731
|
+
}
|
|
2732
|
+
if (breadcrumb) hoverFrame.setAttribute('data-breadcrumb-hover', 'true');
|
|
2733
|
+
else hoverFrame.removeAttribute('data-breadcrumb-hover');
|
|
2734
|
+
hoverFrame.dataset.overlayId = overlayId;
|
|
2735
|
+
hoverFrame.hidden = false;
|
|
2736
|
+
Object.assign(hoverFrame.style, { left: `${rect.left}px`, top: `${rect.top}px`,
|
|
2737
|
+
width: `${rect.width}px`, height: `${rect.height}px`,
|
|
2738
|
+
transform: angle === null ? '' : `rotate(${angle}deg)` });
|
|
2482
2739
|
}
|
|
2483
2740
|
function scheduleHover(event) {
|
|
2741
|
+
if (breadcrumbHoverActive) return;
|
|
2484
2742
|
if (!interactionEnabled || activeDrag || activeResize || activeEdit || event.buttons) { hideHover(); return; }
|
|
2485
2743
|
hoverEvent = event;
|
|
2486
2744
|
if (hoverTick !== null) return;
|
|
2487
2745
|
hoverTick = requestAnimationFrame(() => {
|
|
2488
2746
|
hoverTick = null;
|
|
2747
|
+
if (breadcrumbHoverActive) { hoverEvent = null; return; }
|
|
2489
2748
|
const event = hoverEvent;
|
|
2490
2749
|
if (!event || activeDrag || activeResize || activeEdit) { hideHover(); return; }
|
|
2491
2750
|
const container = overlayForEvent(event);
|
|
@@ -2501,17 +2760,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
2501
2760
|
const rect = element?.getBoundingClientRect() ?? fragmentBounds(container);
|
|
2502
2761
|
if (!rect || selectedOverlay === container
|
|
2503
2762
|
&& 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: '' });
|
|
2763
|
+
showHoverFrame(rect, null, container.dataset.overlayId, false, false);
|
|
2515
2764
|
return;
|
|
2516
2765
|
}
|
|
2517
2766
|
const next = isSelectable(container) && resolveScopedSelection(selectionTree(), scopeId,
|
|
@@ -2524,20 +2773,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
2524
2773
|
const rect = node && node.kind !== 'leaf' ? unionBounds(visibleMembers(next.selectId))
|
|
2525
2774
|
: leaf?.dataset.role === 'shape-line' ? lineRect : leaf ? fragmentBounds(leaf) : null;
|
|
2526
2775
|
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)` : '' });
|
|
2776
|
+
showHoverFrame(rect, lineRect?.angle ?? null, next.selectId);
|
|
2541
2777
|
});
|
|
2542
2778
|
}
|
|
2543
2779
|
|
|
@@ -2592,13 +2828,15 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
2592
2828
|
const displayScale = stage?.getBoundingClientRect().width / outputSize().width || 1;
|
|
2593
2829
|
const outputDx = (rect.left + rect.width / 2 - drag.startCenterX) / displayScale;
|
|
2594
2830
|
const outputDy = (rect.top + rect.height / 2 - drag.startCenterY) / displayScale;
|
|
2595
|
-
|
|
2831
|
+
const translate = formatElementTranslate(drag.x, drag.y);
|
|
2832
|
+
if (!drag.moved || (Math.abs(outputDx) < 0.5 && Math.abs(outputDy) < 0.5)
|
|
2833
|
+
|| translate === formatElementTranslate(drag.startX, drag.startY)) {
|
|
2596
2834
|
drag.element.style.translate = drag.originalInline;
|
|
2597
2835
|
if (drag.originalDisplay !== undefined) drag.element.style.display = drag.originalDisplay;
|
|
2598
2836
|
refreshSelectionFrame();
|
|
2599
2837
|
return null;
|
|
2600
2838
|
}
|
|
2601
|
-
const style = { translate
|
|
2839
|
+
const style = { translate };
|
|
2602
2840
|
if (drag.originalDisplay !== undefined) style.display = 'inline-block';
|
|
2603
2841
|
const record = enqueueWrite(drag.writeContext, drag.overlayId,
|
|
2604
2842
|
{ element: { ref: drag.elementRef, tag: drag.elementTag,
|
|
@@ -3872,6 +4110,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
3872
4110
|
|
|
3873
4111
|
function onPointerDown(event) {
|
|
3874
4112
|
if (!interactionEnabled || !canBeginPointerInteraction(pointerOwner)) return;
|
|
4113
|
+
event = correctedHitPolicyEvent(event);
|
|
3875
4114
|
if (event.button !== 0 || activeDrag || activeResize || activeRotate || activeLine) return;
|
|
3876
4115
|
if (selectedId && stage && event.target instanceof Element) {
|
|
3877
4116
|
const bounds = stage.getBoundingClientRect();
|
|
@@ -3887,8 +4126,8 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
3887
4126
|
hideHover();
|
|
3888
4127
|
clickOrigin = { selectedId, scopeId, moved: false, hadMultiple: selectedIds.length > 1 };
|
|
3889
4128
|
|
|
4129
|
+
if (event.target instanceof Element && event.target.closest('[data-akari-ui="preview-scope-breadcrumb"]')) return;
|
|
3890
4130
|
if (selectionTree().length) {
|
|
3891
|
-
if (event.target instanceof Element && event.target.closest('[data-akari-ui="preview-scope-breadcrumb"]')) return;
|
|
3892
4131
|
const handle = findHandleElement(event.target);
|
|
3893
4132
|
if (handle) {
|
|
3894
4133
|
if (selectedIds.length > 1) return;
|
|
@@ -4722,6 +4961,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
4722
4961
|
|
|
4723
4962
|
function onClick(event) {
|
|
4724
4963
|
if (!interactionEnabled || activeEdit) return;
|
|
4964
|
+
event = correctedHitPolicyEvent(event);
|
|
4725
4965
|
const hit = overlayForEvent(event);
|
|
4726
4966
|
// pointerdown already toggled. Do not toggle twice or update the cycle clock.
|
|
4727
4967
|
if (event.shiftKey && clickOrigin?.scopedHit) {
|
|
@@ -5218,11 +5458,73 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
5218
5458
|
for (const type of ["pointerdown", "pointerup", "click", "dblclick"]) {
|
|
5219
5459
|
window.addEventListener(type, passTransparentCanvasEvent, true);
|
|
5220
5460
|
}
|
|
5461
|
+
const correctedHitPolicyEvents = new WeakMap();
|
|
5462
|
+
let redirectedHitPolicyClick = null;
|
|
5463
|
+
function pointerInsideStage(event) {
|
|
5464
|
+
if (!stage || !Number.isFinite(event.clientX) || !Number.isFinite(event.clientY)) return false;
|
|
5465
|
+
const rect = stage.getBoundingClientRect();
|
|
5466
|
+
return event.clientX >= rect.left && event.clientX <= rect.right
|
|
5467
|
+
&& event.clientY >= rect.top && event.clientY <= rect.bottom;
|
|
5468
|
+
}
|
|
5469
|
+
function trustedEventAtTarget(event, target) {
|
|
5470
|
+
// 合成 Event は isTrusted=false になり E2 の焦点を失う。元の信頼済み
|
|
5471
|
+
// イベントを保ったまま、選択処理に見せる target だけ現在の hit-test にする。
|
|
5472
|
+
return new Proxy(event, { get(original, key) {
|
|
5473
|
+
if (key === 'target') return target;
|
|
5474
|
+
const value = Reflect.get(original, key, original);
|
|
5475
|
+
return typeof value === 'function' ? value.bind(original) : value;
|
|
5476
|
+
} });
|
|
5477
|
+
}
|
|
5478
|
+
function correctHitPolicyEvent(event, target) {
|
|
5479
|
+
correctedHitPolicyEvents.set(event, target);
|
|
5480
|
+
// Window capture の後続ホストにも素の HTML hit と同じ target を見せる。
|
|
5481
|
+
// Event 自体は元の信頼済みイベントなので、再生停止や混在選択は一度だけ走る。
|
|
5482
|
+
try { Object.defineProperty(event, 'target', { configurable: true, value: target }); }
|
|
5483
|
+
catch { /* document 側は WeakMap の target へフォールバックする。 */ }
|
|
5484
|
+
}
|
|
5485
|
+
function correctedHitPolicyEvent(event) {
|
|
5486
|
+
const target = correctedHitPolicyEvents.get(event);
|
|
5487
|
+
return target && event.target !== target ? trustedEventAtTarget(event, target) : event;
|
|
5488
|
+
}
|
|
5489
|
+
function stopCorrectedHitPolicyPropagation(event) {
|
|
5490
|
+
// window のホスト捕捉は通し、古い preview-stage への下流だけ止める。
|
|
5491
|
+
if (findOverlayContainer(correctedHitPolicyEvents.get(event))) event.stopPropagation();
|
|
5492
|
+
}
|
|
5493
|
+
window.addEventListener('pointermove', event => {
|
|
5494
|
+
if (!pointerInsideStage(event) || !staleHitPolicyContainers.size
|
|
5495
|
+
|| hitPolicyOperationBusy()) return;
|
|
5496
|
+
const now = performance.now();
|
|
5497
|
+
if (now - lastPointerHitPolicyRefresh < 125) return;
|
|
5498
|
+
if (!refreshStaleHitPolicies()) return;
|
|
5499
|
+
lastPointerHitPolicyRefresh = now;
|
|
5500
|
+
const target = document.elementFromPoint(event.clientX, event.clientY);
|
|
5501
|
+
if (target && target !== event.target) correctHitPolicyEvent(event, target);
|
|
5502
|
+
}, true);
|
|
5503
|
+
window.addEventListener('pointerdown', event => {
|
|
5504
|
+
if (event.button !== 0 || !pointerInsideStage(event) || hitPolicyOperationBusy()) return;
|
|
5505
|
+
redirectedHitPolicyClick = null;
|
|
5506
|
+
if (!refreshStaleHitPolicies()) return;
|
|
5507
|
+
const target = document.elementFromPoint(event.clientX, event.clientY);
|
|
5508
|
+
if (!target || target === event.target) return;
|
|
5509
|
+
redirectedHitPolicyClick = { x: event.clientX, y: event.clientY };
|
|
5510
|
+
correctHitPolicyEvent(event, target);
|
|
5511
|
+
}, true);
|
|
5512
|
+
window.addEventListener('click', event => {
|
|
5513
|
+
if (!redirectedHitPolicyClick) return;
|
|
5514
|
+
const redirect = redirectedHitPolicyClick;
|
|
5515
|
+
redirectedHitPolicyClick = null;
|
|
5516
|
+
if (Math.hypot(event.clientX - redirect.x, event.clientY - redirect.y) > 6) return;
|
|
5517
|
+
const target = document.elementFromPoint(event.clientX, event.clientY);
|
|
5518
|
+
if (!target || target === event.target) return;
|
|
5519
|
+
correctHitPolicyEvent(event, target);
|
|
5520
|
+
}, true);
|
|
5221
5521
|
listenerRoot.addEventListener('pointermove', updateShapeLineHitProxyWidths, true);
|
|
5222
5522
|
listenerRoot.addEventListener('pointerdown', updateShapeLineHitProxyWidths, true);
|
|
5223
5523
|
window.addEventListener('resize', updateShapeLineHitProxyWidths);
|
|
5224
5524
|
listenerRoot.addEventListener("click", onClick, true);
|
|
5225
5525
|
listenerRoot.addEventListener("pointerdown", onPointerDown, true);
|
|
5526
|
+
listenerRoot.addEventListener("click", stopCorrectedHitPolicyPropagation, true);
|
|
5527
|
+
listenerRoot.addEventListener("pointerdown", stopCorrectedHitPolicyPropagation, true);
|
|
5226
5528
|
listenerRoot.addEventListener("dblclick", onDoubleClick, true);
|
|
5227
5529
|
listenerRoot.addEventListener("blur", onBlur, true);
|
|
5228
5530
|
listenerRoot.addEventListener("input", onEditableInput, true);
|
|
@@ -5333,6 +5635,8 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
5333
5635
|
// ㉑ 素通し: overlay-runtime.js の tick() が可視化タイミングで呼ぶ。
|
|
5334
5636
|
applyOverlayHitPolicy,
|
|
5335
5637
|
invalidateOverlayHitPolicy,
|
|
5638
|
+
markOverlayHitPolicyStale,
|
|
5639
|
+
forgetOverlayHitPolicyTracking,
|
|
5336
5640
|
syncOverlayHitRegion,
|
|
5337
5641
|
// Web UI(preview-server)が編集モードを抜けるときに選択枠を畳むための公開口
|
|
5338
5642
|
// (Phase 2-4 一本化。shell では未使用の追加 export で挙動不変)。
|
|
@@ -341,6 +341,7 @@ function createOverlayRuntime(options = {}) {
|
|
|
341
341
|
|
|
342
342
|
function unmount() {
|
|
343
343
|
for (const overlay of mountedOverlays) {
|
|
344
|
+
window.akari.interaction?.forgetOverlayHitPolicyTracking?.(overlay.container);
|
|
344
345
|
for (const animation of overlay.animations ?? []) releaseAnimation(animation);
|
|
345
346
|
for (const runtime of overlayRuntimes(overlay)) runtime.dispose(overlay.container);
|
|
346
347
|
}
|
|
@@ -490,6 +491,7 @@ function createOverlayRuntime(options = {}) {
|
|
|
490
491
|
runtimesVersion: runtimeRegistryVersion,
|
|
491
492
|
keepLayoutWhenHidden,
|
|
492
493
|
hitPolicyPending: false,
|
|
494
|
+
hitPolicyEntry: false,
|
|
493
495
|
// getAnimations({ subtree: true }) の 250ms キャッシュ(tick() 参照)
|
|
494
496
|
animations: undefined,
|
|
495
497
|
animationsAt: 0,
|
|
@@ -574,6 +576,7 @@ function createOverlayRuntime(options = {}) {
|
|
|
574
576
|
}
|
|
575
577
|
mounted.container.style.setProperty("opacity", String(statics.opacity));
|
|
576
578
|
mounted.hitPolicyPending = true;
|
|
579
|
+
mounted.hitPolicyEntry = true;
|
|
577
580
|
mounted.statics = statics;
|
|
578
581
|
mounted.keyframes = Array.isArray(value.keyframes) ? value.keyframes : undefined;
|
|
579
582
|
mounted.fps = finiteNumber(summary?.output?.fps, 30);
|
|
@@ -595,6 +598,7 @@ function createOverlayRuntime(options = {}) {
|
|
|
595
598
|
mounted.animationsAt = 0;
|
|
596
599
|
mounted.animationEndTimes = [];
|
|
597
600
|
mounted.hitPolicyPending = true;
|
|
601
|
+
mounted.hitPolicyEntry = true;
|
|
598
602
|
return true;
|
|
599
603
|
}
|
|
600
604
|
|
|
@@ -636,7 +640,14 @@ function createOverlayRuntime(options = {}) {
|
|
|
636
640
|
// することで、実際に存在する CSS Animation を「今可視のオーバーレイ分だけ」に
|
|
637
641
|
// 抑え、この地雷を踏まない。
|
|
638
642
|
overlay.container.toggleAttribute("data-akari-active", visible);
|
|
643
|
+
if (!visible && !isPlaying && overlay.animations?.length) {
|
|
644
|
+
// 停止中の連続シークが同一描画フレームで外→内へ戻っても、CSS が
|
|
645
|
+
// inactive を一度観測するようにする。これが無いとゲートの off/on が
|
|
646
|
+
// 相殺され、新しい Animation が生成されず初期 opacity 0 に残る。
|
|
647
|
+
void overlay.container.offsetWidth;
|
|
648
|
+
}
|
|
639
649
|
overlay.hitPolicyPending = visible;
|
|
650
|
+
overlay.hitPolicyEntry = visible;
|
|
640
651
|
// getAnimations() のキャッシュ(下記)は可視化フリップで必ず捨てる。ゲート属性の付け外しで
|
|
641
652
|
// CSS animation の顔ぶれが変わるため、可視化直後の tick は引き直す。非表示化でも捨て、
|
|
642
653
|
// 非可視の間 Animation 参照を持ち越さない。
|
|
@@ -650,6 +661,7 @@ function createOverlayRuntime(options = {}) {
|
|
|
650
661
|
overlay.animationsAt = 0;
|
|
651
662
|
overlay.animationEndTimes = [];
|
|
652
663
|
if (!visible) {
|
|
664
|
+
window.akari.interaction?.forgetOverlayHitPolicyTracking?.(overlay.container);
|
|
653
665
|
if (!overlay.keepLayoutWhenHidden) {
|
|
654
666
|
overlay.container.setAttribute(RUNTIME_HIDDEN_ATTRIBUTE, "");
|
|
655
667
|
}
|
|
@@ -762,15 +774,18 @@ function createOverlayRuntime(options = {}) {
|
|
|
762
774
|
runtime.render(overlay.container, localTimeMs / 1000,
|
|
763
775
|
{ syncVideos: true, maxRenderSize, playing: Boolean(playing) });
|
|
764
776
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
777
|
+
const ready = !isPlaying
|
|
778
|
+
? overlay.animations.map(animation => pendingReady.get(animation)).filter(Boolean) : [];
|
|
779
|
+
window.akari.interaction?.markOverlayHitPolicyStale?.(
|
|
780
|
+
overlay.container, localTimeMs, !isPlaying, overlay.hitPolicyEntry,
|
|
781
|
+
ready.length ? Promise.allSettled(ready) : null);
|
|
782
|
+
overlay.hitPolicyEntry = false;
|
|
783
|
+
// 初回の仮判定と有限アニメ終端の確定判定だけをここで適用する。
|
|
784
|
+
// 途中の姿は上の時刻印を使い、停止後またはポインタ入力時に interaction が
|
|
785
|
+
// 必要な可視コンテナだけ測り直す。終端 tick が存在しない断片も停止後に更新される。
|
|
770
786
|
if (overlay.hitPolicyPending) {
|
|
771
787
|
window.akari.interaction?.syncOverlayHitRegion?.(overlay.container);
|
|
772
|
-
//
|
|
773
|
-
// 実質 no-op(暫定適用)。確定姿勢に達した tick で invalidate してから測り直す。
|
|
788
|
+
// 初回以外は WeakSet のガードで no-op。再入場時は時刻印側が解除する。
|
|
774
789
|
window.akari.interaction?.applyOverlayHitPolicy?.(overlay.container);
|
|
775
790
|
if (entryAnimationsSettled(overlay.animationEndTimes, localTimeMs)) {
|
|
776
791
|
window.akari.interaction?.invalidateOverlayHitPolicy?.(overlay.container);
|