akari-video 1.2.0 → 1.3.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/vendor/packages/akari-launcher/package.json +1 -1
- package/vendor/packages/media-bin/test/media-bin.test.mjs +9 -6
- package/vendor/packages/media-bin/test/preview-audio-sidecar-async.test.mjs +1 -1
- package/vendor/packages/overlay-runtime/src/element-selection.d.ts +13 -3
- package/vendor/packages/overlay-runtime/src/element-selection.mjs +145 -11
- package/vendor/packages/overlay-runtime/src/interaction.js +447 -28
- 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.2",
|
|
4
4
|
"description": "AKARI Video launcher CLI — start an AI-edited video project from any directory: scaffold, connection check, then hand over to Claude Code (or opencode). AKARI Video を opencode や Claude Code で、どのディレクトリからでも始めるための `akari` ランチャー CLI。接続確認(doctor)→ 未セットアップならプロジェクト雛形を作成 → AI エージェントを起動する。外部 npm 依存ゼロ(Node.js 組み込みモジュールのみ)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0-beta.2",
|
|
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": [
|
|
@@ -5,7 +5,7 @@ import { mkdtemp, rm, writeFile, chmod } from "node:fs/promises";
|
|
|
5
5
|
import { syncBuiltinESMExports } from "node:module";
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
7
7
|
import path from "node:path";
|
|
8
|
-
import test from "node:test";
|
|
8
|
+
import test, { after } from "node:test";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
|
|
11
11
|
import { resolveFfmpeg, resolveFfprobe } from "../src/index.mjs";
|
|
@@ -37,9 +37,10 @@ function baseEnv(overrides = {}) {
|
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
// PATH からシステムの ffmpeg/ffprobe
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
// PATH からシステムの ffmpeg/ffprobe を OS に依らず外す。
|
|
41
|
+
const emptyPathDirectory = fs.mkdtempSync(path.join(tmpdir(), 'akari-media-bin-empty-path-'));
|
|
42
|
+
after(() => fs.rmSync(emptyPathDirectory, { recursive: true, force: true }));
|
|
43
|
+
const STRIPPED_PATH_ENV = baseEnv({ PATH: emptyPathDirectory });
|
|
43
44
|
|
|
44
45
|
test("packagedBinaryPath: packageRoot の ../../media-bin を候補にし、win32 は .exe を付ける", () => {
|
|
45
46
|
const packageRoot = fileURLToPath(new URL("../", import.meta.url));
|
|
@@ -192,7 +193,8 @@ test("resolveFfmpeg: PATH に ffmpeg があればコマンド名を返す", () =
|
|
|
192
193
|
assert.equal(resolved, "ffmpeg");
|
|
193
194
|
});
|
|
194
195
|
|
|
195
|
-
test("resolveFfmpeg: PATH から外すと vendor 同梱バイナリへフォールバックし、実行できる",
|
|
196
|
+
test("resolveFfmpeg: PATH から外すと vendor 同梱バイナリへフォールバックし、実行できる", t => {
|
|
197
|
+
if (!fs.existsSync(vendorBinaryPath('ffmpeg'))) return t.skip('vendor ffmpeg is not installed when postinstall is skipped');
|
|
196
198
|
const resolved = resolveFfmpeg({ env: STRIPPED_PATH_ENV });
|
|
197
199
|
assert.notEqual(resolved, "ffmpeg");
|
|
198
200
|
assert.ok(path.isAbsolute(resolved), `絶対パスのはず: ${resolved}`);
|
|
@@ -202,7 +204,8 @@ test("resolveFfmpeg: PATH から外すと vendor 同梱バイナリへフォー
|
|
|
202
204
|
assert.match(result.stdout, /ffmpeg version/);
|
|
203
205
|
});
|
|
204
206
|
|
|
205
|
-
test("resolveFfprobe: PATH から外すと vendor 同梱バイナリへフォールバックし、実行できる",
|
|
207
|
+
test("resolveFfprobe: PATH から外すと vendor 同梱バイナリへフォールバックし、実行できる", t => {
|
|
208
|
+
if (!fs.existsSync(vendorBinaryPath('ffprobe'))) return t.skip('vendor ffprobe is not installed when postinstall is skipped');
|
|
206
209
|
const resolved = resolveFfprobe({ env: STRIPPED_PATH_ENV });
|
|
207
210
|
assert.notEqual(resolved, "ffprobe");
|
|
208
211
|
assert.ok(path.isAbsolute(resolved), `絶対パスのはず: ${resolved}`);
|
|
@@ -100,7 +100,7 @@ test('サイドカー生成中もイベントループは止まらない(spawn
|
|
|
100
100
|
assert.equal(result.ok, true, result.reason);
|
|
101
101
|
assert.equal(result.skipped, false);
|
|
102
102
|
assert.deepEqual(Object.keys(result).sort(),
|
|
103
|
-
['channels', 'durationSec', 'key', 'ok', 'path', 'reason', 'sampleRate', 'skipped']);
|
|
103
|
+
['channels', 'durationSec', 'format', 'key', 'ok', 'path', 'reason', 'sampleRate', 'skipped']);
|
|
104
104
|
assert.equal(result.sampleRate, 48000);
|
|
105
105
|
assert.equal(result.channels, 1);
|
|
106
106
|
assert.equal(result.durationSec, 1);
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
export declare function elementAddress(root: Element, element: Element): string | null;
|
|
2
|
-
export
|
|
3
|
-
export declare function
|
|
4
|
-
export declare function
|
|
2
|
+
export interface SelectionBounds { left: number; top: number; right: number; bottom: number; width: number; height: number }
|
|
3
|
+
export declare function drawsSelectionContent(element: Element, style?: CSSStyleDeclaration): boolean;
|
|
4
|
+
export declare function selectionContentBounds(element: Element, options?: {
|
|
5
|
+
container?: Element;
|
|
6
|
+
visibleRect?: (rect: DOMRect, element: Element) => SelectionBounds | null;
|
|
7
|
+
rawRect?: (element: Element) => DOMRect;
|
|
8
|
+
}): SelectionBounds | null;
|
|
9
|
+
export declare function isSelectionContainer(element: Element): boolean;
|
|
10
|
+
export declare function selectableContainer(root: Element, element: Element, outputRect: DOMRect | undefined, bounds: SelectionBounds | null, options?: SelectionOptions): boolean;
|
|
11
|
+
export interface SelectionOptions { boundsFor?: (element: Element) => SelectionBounds | null }
|
|
12
|
+
export declare function selectableElement(root: Element, element: Element, outputRect?: DOMRect, options?: SelectionOptions): boolean;
|
|
13
|
+
export declare function nearestSelectableElement(root: Element, hit: Element, outputRect?: DOMRect, options?: SelectionOptions): Element | null;
|
|
14
|
+
export declare function firstSelectableElement(root: Element, outputRect?: DOMRect, options?: SelectionOptions): Element | null;
|
|
5
15
|
export declare function elementByAddress(root: Element, ref: string): Element | null;
|
|
6
16
|
export declare function elementLabel(root: Element, element: Element): string;
|
|
@@ -17,30 +17,163 @@ function elementAddress(root, element) {
|
|
|
17
17
|
return index < 0 ? null : `${id ? '#' : '.'}${id || token}[${index}]`;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function
|
|
20
|
+
function drawsSelectionContent(element, style = getComputedStyle(element)) {
|
|
21
|
+
const tag = element.tagName.toUpperCase();
|
|
22
|
+
if (['BASE', 'HEAD', 'LINK', 'META', 'NOSCRIPT', 'SCRIPT', 'STYLE', 'TEMPLATE', 'TITLE'].includes(tag)) return false;
|
|
23
|
+
if (['AUDIO', 'CANVAS', 'EMBED', 'IFRAME', 'IMG', 'OBJECT', 'SVG', 'VIDEO'].includes(tag)) return true;
|
|
24
|
+
if (element.namespaceURI === 'http://www.w3.org/2000/svg'
|
|
25
|
+
&& ['path', 'rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'use', 'text', 'image'].includes(tag.toLowerCase())) return true;
|
|
26
|
+
if ([...element.childNodes].some(node => node.nodeType === 3 && node.textContent.trim())) return true;
|
|
27
|
+
const transparent = value => {
|
|
28
|
+
const color = String(value ?? '').trim().toLowerCase();
|
|
29
|
+
if (!color || color === 'transparent') return true;
|
|
30
|
+
const legacy = color.match(/^(?:rgba|hsla)\([^)]*,\s*([\d.]+)\)$/);
|
|
31
|
+
if (legacy) return Number(legacy[1]) <= 0;
|
|
32
|
+
const modern = color.match(/\/\s*([\d.]+)%?\s*\)$/);
|
|
33
|
+
return Boolean(modern && Number(modern[1]) <= 0);
|
|
34
|
+
};
|
|
35
|
+
const shadow = String(style.boxShadow ?? '').trim().toLowerCase();
|
|
36
|
+
const shadowColors = shadow.match(/(?:rgba?|hsla?|color)\([^)]*\)|transparent/g) ?? [];
|
|
37
|
+
if (!transparent(style.backgroundColor) || style.backgroundImage && style.backgroundImage !== 'none'
|
|
38
|
+
|| shadow && shadow !== 'none' && (shadowColors.length === 0 || shadowColors.some(color => !transparent(color)))) return true;
|
|
39
|
+
for (const side of ['Top', 'Right', 'Bottom', 'Left']) {
|
|
40
|
+
if (parseFloat(style[`border${side}Width`]) > 0
|
|
41
|
+
&& !['none', 'hidden'].includes(style[`border${side}Style`])
|
|
42
|
+
&& !transparent(style[`border${side}Color`])) return true;
|
|
43
|
+
}
|
|
44
|
+
return parseFloat(style.outlineWidth) > 0 && !['none', 'hidden'].includes(style.outlineStyle)
|
|
45
|
+
&& !transparent(style.outlineColor);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function selectionVisibleRect(rect, element, container) {
|
|
49
|
+
if (!rect || !(rect.width > 0 && rect.height > 0)) return null;
|
|
50
|
+
let { left, top, right, bottom } = rect;
|
|
51
|
+
for (let node = element; node && node !== container; node = node.parentElement) {
|
|
52
|
+
const style = getComputedStyle(node);
|
|
53
|
+
const paint = /\b(?:paint|content|strict)\b/.test(style.contain ?? '');
|
|
54
|
+
const clipX = paint || node.tagName.toLowerCase() === 'svg' || (style.overflowX || style.overflow) !== 'visible';
|
|
55
|
+
const clipY = paint || node.tagName.toLowerCase() === 'svg' || (style.overflowY || style.overflow) !== 'visible';
|
|
56
|
+
if (clipX || clipY) {
|
|
57
|
+
const clip = node.getBoundingClientRect();
|
|
58
|
+
if (clipX) { left = Math.max(left, clip.left); right = Math.min(right, clip.right); }
|
|
59
|
+
if (clipY) { top = Math.max(top, clip.top); bottom = Math.min(bottom, clip.bottom); }
|
|
60
|
+
}
|
|
61
|
+
if (right <= left || bottom <= top) return null;
|
|
62
|
+
}
|
|
63
|
+
return { left, top, right, bottom, width: right - left, height: bottom - top };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function selectionContentBounds(element, options = {}) {
|
|
67
|
+
if (!element) return null;
|
|
68
|
+
let left = Infinity, top = Infinity, right = -Infinity, bottom = -Infinity;
|
|
69
|
+
for (const node of [element, ...element.querySelectorAll('*')]) {
|
|
70
|
+
if (node.closest('[data-akari-interaction]') || node.closest('defs, symbol, pattern, clipPath, mask, marker')) continue;
|
|
71
|
+
let visible = true;
|
|
72
|
+
for (let ancestor = node; ancestor && ancestor !== options.container; ancestor = ancestor.parentElement) {
|
|
73
|
+
const style = getComputedStyle(ancestor);
|
|
74
|
+
if (style.display === 'none' || ['hidden', 'collapse'].includes(style.visibility)
|
|
75
|
+
|| Number(style.opacity) === 0) { visible = false; break; }
|
|
76
|
+
}
|
|
77
|
+
if (!visible || !drawsSelectionContent(node)) continue;
|
|
78
|
+
if (node.tagName.toLowerCase() === 'svg'
|
|
79
|
+
&& node.querySelector('path, rect, circle, ellipse, line, polyline, polygon, use, text, image')
|
|
80
|
+
&& getComputedStyle(node).backgroundColor === 'rgba(0, 0, 0, 0)') continue;
|
|
81
|
+
const raw = options.rawRect?.(node) ?? node.getBoundingClientRect();
|
|
82
|
+
const rect = options.visibleRect ? options.visibleRect(raw, node)
|
|
83
|
+
: selectionVisibleRect(raw, node, options.container);
|
|
84
|
+
if (!rect || !(rect.width > 0 && rect.height > 0)) continue;
|
|
85
|
+
left = Math.min(left, rect.left); top = Math.min(top, rect.top);
|
|
86
|
+
right = Math.max(right, rect.right); bottom = Math.max(bottom, rect.bottom);
|
|
87
|
+
}
|
|
88
|
+
return Number.isFinite(left) ? { left, top, right, bottom, width: right - left, height: bottom - top } : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function selectableContentBranches(root, element, outputRect) {
|
|
92
|
+
let count = 0;
|
|
93
|
+
const visit = node => {
|
|
94
|
+
if (count > 1) return;
|
|
95
|
+
const style = getComputedStyle(node);
|
|
96
|
+
if (style.display === 'none' || ['hidden', 'collapse'].includes(style.visibility)
|
|
97
|
+
|| Number(style.opacity) === 0 || isRuntimeElement(node)) return;
|
|
98
|
+
const rect = node.getBoundingClientRect();
|
|
99
|
+
if (node !== element && elementAddress(root, node) && rect.width > 1 && rect.height > 1
|
|
100
|
+
&& drawsSelectionContent(node) && (!outputRect || rect.width < outputRect.width * 0.95
|
|
101
|
+
|| rect.height < outputRect.height * 0.95)) { count++; return; }
|
|
102
|
+
for (const child of node.children) visit(child);
|
|
103
|
+
};
|
|
104
|
+
for (const child of element.children) visit(child);
|
|
105
|
+
return count;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isSelectionContainer(element) {
|
|
109
|
+
return Boolean(element?.children?.length) && !drawsSelectionContent(element);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function selectionBoundsFor(element, options) {
|
|
113
|
+
const memo = options.boundsMemo ??= new Map();
|
|
114
|
+
if (!memo.has(element)) memo.set(element,
|
|
115
|
+
options.boundsFor ? options.boundsFor(element) : selectionContentBounds(element));
|
|
116
|
+
return memo.get(element);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function hasUncoveredPaintedContent(root, element, outputRect, options) {
|
|
120
|
+
const visit = node => {
|
|
121
|
+
if (node.closest('[data-akari-interaction], defs, symbol, pattern, clipPath, mask, marker')) return false;
|
|
122
|
+
if (selectableElement(root, node, outputRect, options)) return false;
|
|
123
|
+
const rect = node.getBoundingClientRect();
|
|
124
|
+
if (rect.width > 0 && Number(getComputedStyle(node).opacity) !== 0
|
|
125
|
+
&& drawsSelectionContent(node)) return true;
|
|
126
|
+
return [...node.children].some(visit);
|
|
127
|
+
};
|
|
128
|
+
return [...element.children].some(visit);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function selectableContainer(root, element, outputRect, bounds, options = {}) {
|
|
132
|
+
if (!isSelectionContainer(element)) return true;
|
|
133
|
+
if (!bounds) return false;
|
|
134
|
+
// Keep an animated wrapper reachable while one of its painted phases is visible.
|
|
135
|
+
if (hasUncoveredPaintedContent(root, element, outputRect, options)) return true;
|
|
136
|
+
if (selectableContentBranches(root, element, outputRect) <= 1) return false;
|
|
137
|
+
const box = element.getBoundingClientRect();
|
|
138
|
+
return !(outputRect && (box.width >= outputRect.width * 0.5 || box.height >= outputRect.height * 0.5)
|
|
139
|
+
&& (box.width >= bounds.width * 1.5 || box.height >= bounds.height * 1.5));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function selectableElement(root, element, outputRect, options = {}) {
|
|
143
|
+
const memo = options.selectableMemo ??= new Map();
|
|
144
|
+
if (memo.has(element)) return memo.get(element);
|
|
21
145
|
if (!root || !element || element === root || !root.contains(element) || isRuntimeElement(element)
|
|
22
146
|
|| element.closest('svg') !== (element.tagName.toLowerCase() === 'svg' ? element : null)
|
|
23
|
-
|| !elementAddress(root, element)) return false;
|
|
147
|
+
|| !elementAddress(root, element)) { memo.set(element, false); return false; }
|
|
24
148
|
const rect = element.getBoundingClientRect();
|
|
25
|
-
if (!(rect.width > 1 && rect.height > 1)) return false;
|
|
149
|
+
if (!(rect.width > 1 && rect.height > 1)) { memo.set(element, false); return false; }
|
|
26
150
|
for (let ancestor = element; ancestor && root.contains(ancestor); ancestor = ancestor.parentElement) {
|
|
27
151
|
const style = getComputedStyle(ancestor);
|
|
28
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity) === 0)
|
|
152
|
+
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity) === 0) {
|
|
153
|
+
memo.set(element, false); return false;
|
|
154
|
+
}
|
|
29
155
|
}
|
|
156
|
+
const bounds = selectionBoundsFor(element, options);
|
|
157
|
+
const coverage = bounds ?? rect;
|
|
30
158
|
const replaced = ['img', 'video', 'canvas', 'svg'].includes(element.tagName.toLowerCase());
|
|
31
|
-
|
|
32
|
-
||
|
|
159
|
+
const eligible = (replaced || !outputRect || coverage.width < outputRect.width * 0.95
|
|
160
|
+
|| coverage.height < outputRect.height * 0.95)
|
|
161
|
+
&& selectableContainer(root, element, outputRect, bounds, options);
|
|
162
|
+
memo.set(element, eligible);
|
|
163
|
+
return eligible;
|
|
33
164
|
}
|
|
34
165
|
|
|
35
|
-
function nearestSelectableElement(root, hit, outputRect) {
|
|
166
|
+
function nearestSelectableElement(root, hit, outputRect, options = {}) {
|
|
167
|
+
options = { ...options, selectableMemo: new Map(), boundsMemo: new Map() };
|
|
36
168
|
for (let element = hit; element && element !== root; element = element.parentElement) {
|
|
37
|
-
if (selectableElement(root, element, outputRect)) return element;
|
|
169
|
+
if (selectableElement(root, element, outputRect, options)) return element;
|
|
38
170
|
}
|
|
39
171
|
return null;
|
|
40
172
|
}
|
|
41
173
|
|
|
42
|
-
function firstSelectableElement(root, outputRect) {
|
|
43
|
-
|
|
174
|
+
function firstSelectableElement(root, outputRect, options = {}) {
|
|
175
|
+
options = { ...options, selectableMemo: new Map(), boundsMemo: new Map() };
|
|
176
|
+
return [root, ...root.querySelectorAll('*')].find(element => selectableElement(root, element, outputRect, options)) ?? null;
|
|
44
177
|
}
|
|
45
178
|
|
|
46
179
|
function elementByAddress(root, ref) {
|
|
@@ -57,5 +190,6 @@ function elementLabel(root, element) {
|
|
|
57
190
|
return siblings.length > 1 ? `${name} ${siblings.indexOf(element) + 1}` : name;
|
|
58
191
|
}
|
|
59
192
|
// END element-selection
|
|
60
|
-
export { isRuntimeElement, elementAddress,
|
|
193
|
+
export { isRuntimeElement, elementAddress, drawsSelectionContent, selectionContentBounds,
|
|
194
|
+
isSelectionContainer, selectableContainer, selectableElement, nearestSelectableElement,
|
|
61
195
|
firstSelectableElement, elementByAddress, elementLabel };
|
|
@@ -122,30 +122,163 @@ function elementAddress(root, element) {
|
|
|
122
122
|
return index < 0 ? null : `${id ? '#' : '.'}${id || token}[${index}]`;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
function
|
|
125
|
+
function drawsSelectionContent(element, style = getComputedStyle(element)) {
|
|
126
|
+
const tag = element.tagName.toUpperCase();
|
|
127
|
+
if (['BASE', 'HEAD', 'LINK', 'META', 'NOSCRIPT', 'SCRIPT', 'STYLE', 'TEMPLATE', 'TITLE'].includes(tag)) return false;
|
|
128
|
+
if (['AUDIO', 'CANVAS', 'EMBED', 'IFRAME', 'IMG', 'OBJECT', 'SVG', 'VIDEO'].includes(tag)) return true;
|
|
129
|
+
if (element.namespaceURI === 'http://www.w3.org/2000/svg'
|
|
130
|
+
&& ['path', 'rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'use', 'text', 'image'].includes(tag.toLowerCase())) return true;
|
|
131
|
+
if ([...element.childNodes].some(node => node.nodeType === 3 && node.textContent.trim())) return true;
|
|
132
|
+
const transparent = value => {
|
|
133
|
+
const color = String(value ?? '').trim().toLowerCase();
|
|
134
|
+
if (!color || color === 'transparent') return true;
|
|
135
|
+
const legacy = color.match(/^(?:rgba|hsla)\([^)]*,\s*([\d.]+)\)$/);
|
|
136
|
+
if (legacy) return Number(legacy[1]) <= 0;
|
|
137
|
+
const modern = color.match(/\/\s*([\d.]+)%?\s*\)$/);
|
|
138
|
+
return Boolean(modern && Number(modern[1]) <= 0);
|
|
139
|
+
};
|
|
140
|
+
const shadow = String(style.boxShadow ?? '').trim().toLowerCase();
|
|
141
|
+
const shadowColors = shadow.match(/(?:rgba?|hsla?|color)\([^)]*\)|transparent/g) ?? [];
|
|
142
|
+
if (!transparent(style.backgroundColor) || style.backgroundImage && style.backgroundImage !== 'none'
|
|
143
|
+
|| shadow && shadow !== 'none' && (shadowColors.length === 0 || shadowColors.some(color => !transparent(color)))) return true;
|
|
144
|
+
for (const side of ['Top', 'Right', 'Bottom', 'Left']) {
|
|
145
|
+
if (parseFloat(style[`border${side}Width`]) > 0
|
|
146
|
+
&& !['none', 'hidden'].includes(style[`border${side}Style`])
|
|
147
|
+
&& !transparent(style[`border${side}Color`])) return true;
|
|
148
|
+
}
|
|
149
|
+
return parseFloat(style.outlineWidth) > 0 && !['none', 'hidden'].includes(style.outlineStyle)
|
|
150
|
+
&& !transparent(style.outlineColor);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function selectionVisibleRect(rect, element, container) {
|
|
154
|
+
if (!rect || !(rect.width > 0 && rect.height > 0)) return null;
|
|
155
|
+
let { left, top, right, bottom } = rect;
|
|
156
|
+
for (let node = element; node && node !== container; node = node.parentElement) {
|
|
157
|
+
const style = getComputedStyle(node);
|
|
158
|
+
const paint = /\b(?:paint|content|strict)\b/.test(style.contain ?? '');
|
|
159
|
+
const clipX = paint || node.tagName.toLowerCase() === 'svg' || (style.overflowX || style.overflow) !== 'visible';
|
|
160
|
+
const clipY = paint || node.tagName.toLowerCase() === 'svg' || (style.overflowY || style.overflow) !== 'visible';
|
|
161
|
+
if (clipX || clipY) {
|
|
162
|
+
const clip = node.getBoundingClientRect();
|
|
163
|
+
if (clipX) { left = Math.max(left, clip.left); right = Math.min(right, clip.right); }
|
|
164
|
+
if (clipY) { top = Math.max(top, clip.top); bottom = Math.min(bottom, clip.bottom); }
|
|
165
|
+
}
|
|
166
|
+
if (right <= left || bottom <= top) return null;
|
|
167
|
+
}
|
|
168
|
+
return { left, top, right, bottom, width: right - left, height: bottom - top };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function selectionContentBounds(element, options = {}) {
|
|
172
|
+
if (!element) return null;
|
|
173
|
+
let left = Infinity, top = Infinity, right = -Infinity, bottom = -Infinity;
|
|
174
|
+
for (const node of [element, ...element.querySelectorAll('*')]) {
|
|
175
|
+
if (node.closest('[data-akari-interaction]') || node.closest('defs, symbol, pattern, clipPath, mask, marker')) continue;
|
|
176
|
+
let visible = true;
|
|
177
|
+
for (let ancestor = node; ancestor && ancestor !== options.container; ancestor = ancestor.parentElement) {
|
|
178
|
+
const style = getComputedStyle(ancestor);
|
|
179
|
+
if (style.display === 'none' || ['hidden', 'collapse'].includes(style.visibility)
|
|
180
|
+
|| Number(style.opacity) === 0) { visible = false; break; }
|
|
181
|
+
}
|
|
182
|
+
if (!visible || !drawsSelectionContent(node)) continue;
|
|
183
|
+
if (node.tagName.toLowerCase() === 'svg'
|
|
184
|
+
&& node.querySelector('path, rect, circle, ellipse, line, polyline, polygon, use, text, image')
|
|
185
|
+
&& getComputedStyle(node).backgroundColor === 'rgba(0, 0, 0, 0)') continue;
|
|
186
|
+
const raw = options.rawRect?.(node) ?? node.getBoundingClientRect();
|
|
187
|
+
const rect = options.visibleRect ? options.visibleRect(raw, node)
|
|
188
|
+
: selectionVisibleRect(raw, node, options.container);
|
|
189
|
+
if (!rect || !(rect.width > 0 && rect.height > 0)) continue;
|
|
190
|
+
left = Math.min(left, rect.left); top = Math.min(top, rect.top);
|
|
191
|
+
right = Math.max(right, rect.right); bottom = Math.max(bottom, rect.bottom);
|
|
192
|
+
}
|
|
193
|
+
return Number.isFinite(left) ? { left, top, right, bottom, width: right - left, height: bottom - top } : null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function selectableContentBranches(root, element, outputRect) {
|
|
197
|
+
let count = 0;
|
|
198
|
+
const visit = node => {
|
|
199
|
+
if (count > 1) return;
|
|
200
|
+
const style = getComputedStyle(node);
|
|
201
|
+
if (style.display === 'none' || ['hidden', 'collapse'].includes(style.visibility)
|
|
202
|
+
|| Number(style.opacity) === 0 || isRuntimeElement(node)) return;
|
|
203
|
+
const rect = node.getBoundingClientRect();
|
|
204
|
+
if (node !== element && elementAddress(root, node) && rect.width > 1 && rect.height > 1
|
|
205
|
+
&& drawsSelectionContent(node) && (!outputRect || rect.width < outputRect.width * 0.95
|
|
206
|
+
|| rect.height < outputRect.height * 0.95)) { count++; return; }
|
|
207
|
+
for (const child of node.children) visit(child);
|
|
208
|
+
};
|
|
209
|
+
for (const child of element.children) visit(child);
|
|
210
|
+
return count;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function isSelectionContainer(element) {
|
|
214
|
+
return Boolean(element?.children?.length) && !drawsSelectionContent(element);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function selectionBoundsFor(element, options) {
|
|
218
|
+
const memo = options.boundsMemo ??= new Map();
|
|
219
|
+
if (!memo.has(element)) memo.set(element,
|
|
220
|
+
options.boundsFor ? options.boundsFor(element) : selectionContentBounds(element));
|
|
221
|
+
return memo.get(element);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function hasUncoveredPaintedContent(root, element, outputRect, options) {
|
|
225
|
+
const visit = node => {
|
|
226
|
+
if (node.closest('[data-akari-interaction], defs, symbol, pattern, clipPath, mask, marker')) return false;
|
|
227
|
+
if (selectableElement(root, node, outputRect, options)) return false;
|
|
228
|
+
const rect = node.getBoundingClientRect();
|
|
229
|
+
if (rect.width > 0 && Number(getComputedStyle(node).opacity) !== 0
|
|
230
|
+
&& drawsSelectionContent(node)) return true;
|
|
231
|
+
return [...node.children].some(visit);
|
|
232
|
+
};
|
|
233
|
+
return [...element.children].some(visit);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function selectableContainer(root, element, outputRect, bounds, options = {}) {
|
|
237
|
+
if (!isSelectionContainer(element)) return true;
|
|
238
|
+
if (!bounds) return false;
|
|
239
|
+
// Keep an animated wrapper reachable while one of its painted phases is visible.
|
|
240
|
+
if (hasUncoveredPaintedContent(root, element, outputRect, options)) return true;
|
|
241
|
+
if (selectableContentBranches(root, element, outputRect) <= 1) return false;
|
|
242
|
+
const box = element.getBoundingClientRect();
|
|
243
|
+
return !(outputRect && (box.width >= outputRect.width * 0.5 || box.height >= outputRect.height * 0.5)
|
|
244
|
+
&& (box.width >= bounds.width * 1.5 || box.height >= bounds.height * 1.5));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function selectableElement(root, element, outputRect, options = {}) {
|
|
248
|
+
const memo = options.selectableMemo ??= new Map();
|
|
249
|
+
if (memo.has(element)) return memo.get(element);
|
|
126
250
|
if (!root || !element || element === root || !root.contains(element) || isRuntimeElement(element)
|
|
127
251
|
|| element.closest('svg') !== (element.tagName.toLowerCase() === 'svg' ? element : null)
|
|
128
|
-
|| !elementAddress(root, element)) return false;
|
|
252
|
+
|| !elementAddress(root, element)) { memo.set(element, false); return false; }
|
|
129
253
|
const rect = element.getBoundingClientRect();
|
|
130
|
-
if (!(rect.width > 1 && rect.height > 1)) return false;
|
|
254
|
+
if (!(rect.width > 1 && rect.height > 1)) { memo.set(element, false); return false; }
|
|
131
255
|
for (let ancestor = element; ancestor && root.contains(ancestor); ancestor = ancestor.parentElement) {
|
|
132
256
|
const style = getComputedStyle(ancestor);
|
|
133
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity) === 0)
|
|
257
|
+
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity) === 0) {
|
|
258
|
+
memo.set(element, false); return false;
|
|
259
|
+
}
|
|
134
260
|
}
|
|
261
|
+
const bounds = selectionBoundsFor(element, options);
|
|
262
|
+
const coverage = bounds ?? rect;
|
|
135
263
|
const replaced = ['img', 'video', 'canvas', 'svg'].includes(element.tagName.toLowerCase());
|
|
136
|
-
|
|
137
|
-
||
|
|
264
|
+
const eligible = (replaced || !outputRect || coverage.width < outputRect.width * 0.95
|
|
265
|
+
|| coverage.height < outputRect.height * 0.95)
|
|
266
|
+
&& selectableContainer(root, element, outputRect, bounds, options);
|
|
267
|
+
memo.set(element, eligible);
|
|
268
|
+
return eligible;
|
|
138
269
|
}
|
|
139
270
|
|
|
140
|
-
function nearestSelectableElement(root, hit, outputRect) {
|
|
271
|
+
function nearestSelectableElement(root, hit, outputRect, options = {}) {
|
|
272
|
+
options = { ...options, selectableMemo: new Map(), boundsMemo: new Map() };
|
|
141
273
|
for (let element = hit; element && element !== root; element = element.parentElement) {
|
|
142
|
-
if (selectableElement(root, element, outputRect)) return element;
|
|
274
|
+
if (selectableElement(root, element, outputRect, options)) return element;
|
|
143
275
|
}
|
|
144
276
|
return null;
|
|
145
277
|
}
|
|
146
278
|
|
|
147
|
-
function firstSelectableElement(root, outputRect) {
|
|
148
|
-
|
|
279
|
+
function firstSelectableElement(root, outputRect, options = {}) {
|
|
280
|
+
options = { ...options, selectableMemo: new Map(), boundsMemo: new Map() };
|
|
281
|
+
return [root, ...root.querySelectorAll('*')].find(element => selectableElement(root, element, outputRect, options)) ?? null;
|
|
149
282
|
}
|
|
150
283
|
|
|
151
284
|
function elementByAddress(root, ref) {
|
|
@@ -340,6 +473,96 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
340
473
|
// テキスト編集で断片 HTML を保存するときに作者の元指定へ戻せるよう、初回値だけ保持する。
|
|
341
474
|
const hitPolicyOriginalPointerEvents = new WeakMap();
|
|
342
475
|
const hitPolicyAppliedContainers = new WeakSet();
|
|
476
|
+
const hitPolicyTimes = new WeakMap();
|
|
477
|
+
const staleHitPolicyContainers = new Set();
|
|
478
|
+
const pausedHitPolicyContainers = new Set();
|
|
479
|
+
let pausedHitPolicyTimer = null;
|
|
480
|
+
let lastPointerHitPolicyRefresh = -Infinity;
|
|
481
|
+
|
|
482
|
+
function hitPolicyOperationBusy() {
|
|
483
|
+
return Boolean(activeEdit || activeDrag || activeResize || activeRotate || activeLine
|
|
484
|
+
|| marqueeFrame || pointerOwner != null);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function schedulePausedHitPolicyRefresh(delay = 120) {
|
|
488
|
+
if (typeof setTimeout !== 'function') return;
|
|
489
|
+
if (pausedHitPolicyTimer !== null) clearTimeout(pausedHitPolicyTimer);
|
|
490
|
+
pausedHitPolicyTimer = setTimeout(flushPausedHitPolicies, delay);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// runtime は可視コンテナの時刻だけを記録する。CSS の読み取りはここでは行わない。
|
|
494
|
+
function markOverlayHitPolicyStale(container, localTimeMs, paused = false, reentered = false,
|
|
495
|
+
animationReady = null) {
|
|
496
|
+
if (!container) return;
|
|
497
|
+
let state = hitPolicyTimes.get(container);
|
|
498
|
+
if (!state) {
|
|
499
|
+
state = { current: localTimeMs, measured: null, paused: null };
|
|
500
|
+
hitPolicyTimes.set(container, state);
|
|
501
|
+
}
|
|
502
|
+
const timeChanged = state.current !== localTimeMs;
|
|
503
|
+
const enteredPause = paused && state.paused !== true;
|
|
504
|
+
state.current = localTimeMs;
|
|
505
|
+
state.paused = paused;
|
|
506
|
+
if (reentered) {
|
|
507
|
+
state.measured = null;
|
|
508
|
+
hitPolicyAppliedContainers.delete(container);
|
|
509
|
+
}
|
|
510
|
+
if (state.measured !== localTimeMs) staleHitPolicyContainers.add(container);
|
|
511
|
+
// pause()/currentTime の pending 補正はこの tick の測定より後に解決し得る。
|
|
512
|
+
// 暫定 apply が stale を消しても、停止後の一回は別の印で必ず残す。
|
|
513
|
+
if (paused && (reentered || timeChanged || enteredPause)) pausedHitPolicyContainers.add(container);
|
|
514
|
+
if (!paused) pausedHitPolicyContainers.delete(container);
|
|
515
|
+
if (paused && pausedHitPolicyContainers.size) {
|
|
516
|
+
// スクラブ中は連続 tick をまとめ、停止後にポインタ無しで確定する。
|
|
517
|
+
schedulePausedHitPolicyRefresh();
|
|
518
|
+
}
|
|
519
|
+
if (paused && (reentered || timeChanged || enteredPause) && animationReady?.then) {
|
|
520
|
+
const expectedState = state;
|
|
521
|
+
animationReady.then(() => {
|
|
522
|
+
if (hitPolicyTimes.get(container) !== expectedState || !state.paused
|
|
523
|
+
|| state.current !== localTimeMs) return;
|
|
524
|
+
// ready 後の currentTime 補正が 120ms の測定より遅れた場合も追い付く。
|
|
525
|
+
const alreadyRefreshed = !pausedHitPolicyContainers.has(container);
|
|
526
|
+
pausedHitPolicyContainers.add(container);
|
|
527
|
+
schedulePausedHitPolicyRefresh(alreadyRefreshed ? 0 : 120);
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function flushPausedHitPolicies() {
|
|
533
|
+
pausedHitPolicyTimer = null;
|
|
534
|
+
if (pausedHitPolicyContainers.size === 0) return;
|
|
535
|
+
if (hitPolicyOperationBusy()) {
|
|
536
|
+
// 編集・ジェスチャーが終わるまで印を保持する。
|
|
537
|
+
pausedHitPolicyTimer = setTimeout(flushPausedHitPolicies, 120);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
refreshStaleHitPolicies(true);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function refreshStaleHitPolicies(includePaused = false) {
|
|
544
|
+
if (hitPolicyOperationBusy()) return false;
|
|
545
|
+
let refreshed = false;
|
|
546
|
+
const candidates = includePaused ? pausedHitPolicyContainers : staleHitPolicyContainers;
|
|
547
|
+
for (const container of candidates) {
|
|
548
|
+
if (includePaused) pausedHitPolicyContainers.delete(container);
|
|
549
|
+
if (!container.isConnected || container.style.visibility === 'hidden') {
|
|
550
|
+
staleHitPolicyContainers.delete(container);
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
invalidateOverlayHitPolicy(container);
|
|
554
|
+
applyOverlayHitPolicy(container);
|
|
555
|
+
refreshed = true;
|
|
556
|
+
}
|
|
557
|
+
return refreshed;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function forgetOverlayHitPolicyTracking(container) {
|
|
561
|
+
staleHitPolicyContainers.delete(container);
|
|
562
|
+
pausedHitPolicyContainers.delete(container);
|
|
563
|
+
hitPolicyTimes.delete(container);
|
|
564
|
+
hitPolicyAppliedContainers.delete(container);
|
|
565
|
+
}
|
|
343
566
|
|
|
344
567
|
// overlay_write は操作順を保存する。selftest は今回の書き込みをこの記録から待つ。
|
|
345
568
|
let writeTail = Promise.resolve();
|
|
@@ -486,6 +709,21 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
486
709
|
return null;
|
|
487
710
|
}
|
|
488
711
|
|
|
712
|
+
function contentBounds(element, container) {
|
|
713
|
+
return selectionContentBounds(element, {
|
|
714
|
+
container,
|
|
715
|
+
visibleRect: (rect, node) => visibleFragmentRect(rect, node, container),
|
|
716
|
+
rawRect: node => node.tagName.toUpperCase() === "CANVAS"
|
|
717
|
+
? (canvasHasDecoration(node) ? node.getBoundingClientRect()
|
|
718
|
+
: window.akari.threeRuntime?.contentBounds?.(node)
|
|
719
|
+
?? measureCanvasBounds(node) ?? node.getBoundingClientRect())
|
|
720
|
+
: node.getBoundingClientRect(),
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
function selectionOptions(root) {
|
|
724
|
+
return { boundsFor: element => contentBounds(element, root?.parentElement) };
|
|
725
|
+
}
|
|
726
|
+
|
|
489
727
|
// 断片ルートは「inset:0 全画面ラッパー + flex/絶対配置」パターン(overlay-authoring
|
|
490
728
|
// 規約・text-behind-person.md 等)を許容するため、ルート自身の矩形がコンテナ(断片の
|
|
491
729
|
// 親 = [data-overlay-id] 要素。overlay-runtime.js の mount() が inset:0 で全画面付与)
|
|
@@ -504,7 +742,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
504
742
|
);
|
|
505
743
|
}
|
|
506
744
|
|
|
507
|
-
function
|
|
745
|
+
function legacyFragmentBounds(container) {
|
|
508
746
|
const root = fragmentRoot(container);
|
|
509
747
|
if (!root) return null;
|
|
510
748
|
|
|
@@ -594,6 +832,13 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
594
832
|
return { left, top, right, bottom, width: right - left, height: bottom - top };
|
|
595
833
|
}
|
|
596
834
|
|
|
835
|
+
function fragmentBounds(container) {
|
|
836
|
+
const root = fragmentRoot(container);
|
|
837
|
+
if (!root) return null;
|
|
838
|
+
if (drawsOwnContent(root, getComputedStyle(root))) return legacyFragmentBounds(container);
|
|
839
|
+
return contentBounds(root, container) ?? legacyFragmentBounds(container);
|
|
840
|
+
}
|
|
841
|
+
|
|
597
842
|
function svgReferenceRect(element, value) {
|
|
598
843
|
const id = String(value ?? '').match(/url\(["']?(?:[^#)]*#)([^)"']+)["']?\)/)?.[1];
|
|
599
844
|
const svg = element.namespaceURI === 'http://www.w3.org/2000/svg'
|
|
@@ -982,7 +1227,8 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
982
1227
|
|
|
983
1228
|
// 全画面の外側コンテナと断片ルートは素通しにし、実際に背景・枠・影・文字・置換要素を
|
|
984
1229
|
// 描く可視の子孫だけを拾う。data-akari-hit は最寄りの指定を配下へ継承し、機械判定より
|
|
985
|
-
// 優先する。字幕が 1,000 件級でも全件を走査しないよう、runtime
|
|
1230
|
+
// 優先する。字幕が 1,000 件級でも全件を走査しないよう、runtime は可視コンテナの
|
|
1231
|
+
// 初回・停止後・ポインタ入力時だけ適用し、再生 tick では時刻の印だけを付ける。
|
|
986
1232
|
function fragmentRootCoversContainer(element, container) {
|
|
987
1233
|
const rootRect = element.getBoundingClientRect();
|
|
988
1234
|
const containerRect = container.getBoundingClientRect();
|
|
@@ -1056,6 +1302,9 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1056
1302
|
|
|
1057
1303
|
for (const root of container.children) visit(root, null, true, true);
|
|
1058
1304
|
hitPolicyAppliedContainers.add(container);
|
|
1305
|
+
const state = hitPolicyTimes.get(container);
|
|
1306
|
+
if (state) state.measured = state.current;
|
|
1307
|
+
staleHitPolicyContainers.delete(container);
|
|
1059
1308
|
updateShapeLineHitProxyWidths();
|
|
1060
1309
|
}
|
|
1061
1310
|
|
|
@@ -1169,6 +1418,34 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1169
1418
|
}
|
|
1170
1419
|
}
|
|
1171
1420
|
|
|
1421
|
+
// 08-07 裁定: 背景は選択・削除できる。旧矩形フォールバックと違い、stage 面に
|
|
1422
|
+
// 落ちたクリックで、全面を覆って自ら塗るルートだけを拾う。実際のヒット先が
|
|
1423
|
+
// あるときは使わず 4680d90d8 の巻き込みを防ぎ、素通し規約は変えない。
|
|
1424
|
+
function paintedStageOverlayAt(clientX, clientY) {
|
|
1425
|
+
const candidates = [...stage.children]
|
|
1426
|
+
.flatMap((container, index) => {
|
|
1427
|
+
if (!isSelectable(container)) return [];
|
|
1428
|
+
const value = Number.parseFloat(getComputedStyle(container).zIndex);
|
|
1429
|
+
return [{ container, index, zIndex: Number.isFinite(value) ? value : 0 }];
|
|
1430
|
+
})
|
|
1431
|
+
.sort((a, b) => b.zIndex - a.zIndex || b.index - a.index);
|
|
1432
|
+
|
|
1433
|
+
for (const { container } of candidates) {
|
|
1434
|
+
for (const root of container.children) {
|
|
1435
|
+
if (root.hasAttribute("data-akari-interaction") || root.getAttribute("data-akari-hit") === "pass") continue;
|
|
1436
|
+
const style = getComputedStyle(root);
|
|
1437
|
+
if (style.display === "none" || Number(style.opacity) <= 0
|
|
1438
|
+
|| ["hidden", "collapse"].includes(style.visibility)
|
|
1439
|
+
|| !fragmentRootCoversContainer(root, container)
|
|
1440
|
+
|| !drawsOwnContent(root, style)) continue;
|
|
1441
|
+
const rect = root.getBoundingClientRect();
|
|
1442
|
+
if (clientX >= rect.left && clientX <= rect.right
|
|
1443
|
+
&& clientY >= rect.top && clientY <= rect.bottom) return container;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
return null;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1172
1449
|
function overlayForEvent(event) {
|
|
1173
1450
|
const eventTargetOverlay = findOverlayContainer(event.target);
|
|
1174
1451
|
if (eventTargetOverlay && !isSelectable(eventTargetOverlay)) {
|
|
@@ -1180,6 +1457,11 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1180
1457
|
) {
|
|
1181
1458
|
return selftestOverlayOverride;
|
|
1182
1459
|
}
|
|
1460
|
+
if (event.target === stage && !eventTargetOverlay
|
|
1461
|
+
&& Number.isFinite(event.clientX) && Number.isFinite(event.clientY)) {
|
|
1462
|
+
const paintedOverlay = paintedStageOverlayAt(event.clientX, event.clientY);
|
|
1463
|
+
if (paintedOverlay) return paintedOverlay;
|
|
1464
|
+
}
|
|
1183
1465
|
return isSelectable(eventTargetOverlay) ? eventTargetOverlay : null;
|
|
1184
1466
|
}
|
|
1185
1467
|
|
|
@@ -1380,7 +1662,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1380
1662
|
const transform = readTransform(selectedOverlay);
|
|
1381
1663
|
const lineRect = lineFrameGeometry(selectedOverlay);
|
|
1382
1664
|
const focused = focusedElement();
|
|
1383
|
-
const focusedGeometry = focused ?
|
|
1665
|
+
const focusedGeometry = focused ? focusedSelectionGeometry(focused) : null;
|
|
1384
1666
|
const focusedWidth = focusedGeometry && focusedGeometry.width * Math.hypot(focusedGeometry.axes.x.x, focusedGeometry.axes.x.y);
|
|
1385
1667
|
const focusedHeight = focusedGeometry && focusedGeometry.height * Math.hypot(focusedGeometry.axes.y.x, focusedGeometry.axes.y.y);
|
|
1386
1668
|
const focusedAngle = focusedGeometry && Math.atan2(focusedGeometry.axes.x.y, focusedGeometry.axes.x.x) * 180 / Math.PI;
|
|
@@ -1437,7 +1719,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1437
1719
|
}
|
|
1438
1720
|
const layout = elementHandleLayout(focusedWidth, focusedHeight);
|
|
1439
1721
|
const name = [...handle.classList].find(token => /^is-(?:n|e|s|w|nw|ne|se|sw|rotate|move)$/.test(token))?.slice(3);
|
|
1440
|
-
handle.style.display = name === 'move' ||
|
|
1722
|
+
handle.style.display = name === 'move' || (isSelectionContainer(focused) && name !== 'rotate') ||
|
|
1441
1723
|
(['n', 's'].includes(name) && layout.hideHorizontalEdges) ||
|
|
1442
1724
|
(['e', 'w'].includes(name) && layout.hideVerticalEdges) ? 'none' : '';
|
|
1443
1725
|
if (name === 'rotate') {
|
|
@@ -1448,6 +1730,11 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1448
1730
|
const y = name.includes('n') ? -layout.cornerOffsetY : layout.cornerOffsetY;
|
|
1449
1731
|
if (x || y) handle.style.translate = `${x}px ${y}px`;
|
|
1450
1732
|
else handle.style.removeProperty('translate');
|
|
1733
|
+
} else if (['n', 's', 'e', 'w'].includes(name)) {
|
|
1734
|
+
const x = name === 'w' ? -layout.cornerOffsetX : name === 'e' ? layout.cornerOffsetX : 0;
|
|
1735
|
+
const y = name === 'n' ? -layout.cornerOffsetY : name === 's' ? layout.cornerOffsetY : 0;
|
|
1736
|
+
if (x || y) handle.style.translate = `${x}px ${y}px`;
|
|
1737
|
+
else handle.style.removeProperty('translate');
|
|
1451
1738
|
}
|
|
1452
1739
|
if (name && name !== 'move' && name !== 'rotate') {
|
|
1453
1740
|
const directions = ['ew-resize', 'nwse-resize', 'ns-resize', 'nesw-resize'];
|
|
@@ -1612,7 +1899,8 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1612
1899
|
}
|
|
1613
1900
|
function focusedElement() {
|
|
1614
1901
|
if (!selectedElementFocus() || !selectedOverlay) return null;
|
|
1615
|
-
|
|
1902
|
+
const root = fragmentRoot(selectedOverlay);
|
|
1903
|
+
return elementByAddress(root, elementFocus.ref);
|
|
1616
1904
|
}
|
|
1617
1905
|
function selectedElementFocus() {
|
|
1618
1906
|
return elementFocus?.overlayId === selectedId
|
|
@@ -1621,10 +1909,23 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1621
1909
|
function reconcileElementFocus() {
|
|
1622
1910
|
if (!selectedElementFocus()) return;
|
|
1623
1911
|
const root = fragmentRoot(selectedOverlay);
|
|
1624
|
-
if (
|
|
1912
|
+
if (!root) return;
|
|
1913
|
+
const ref = elementFocus.ref;
|
|
1914
|
+
const element = elementByAddress(root, ref);
|
|
1915
|
+
if (!element) { focusElement(null); return; }
|
|
1916
|
+
if (selectedOverlay?.isConnected && !selectableElement(root, element, stage?.getBoundingClientRect(), selectionOptions(root))) {
|
|
1917
|
+
requestAnimationFrame(() => {
|
|
1918
|
+
if (elementFocus?.ref === ref && selectedOverlay?.isConnected) {
|
|
1919
|
+
const currentRoot = fragmentRoot(selectedOverlay);
|
|
1920
|
+
const current = elementByAddress(currentRoot, ref);
|
|
1921
|
+
if (!current || !selectableElement(currentRoot, current, stage?.getBoundingClientRect(), selectionOptions(currentRoot))) focusElement(null);
|
|
1922
|
+
}
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1625
1925
|
}
|
|
1626
1926
|
function focusElement(element, { notify = true } = {}) {
|
|
1627
1927
|
const root = fragmentRoot(selectedOverlay);
|
|
1928
|
+
if (element && !selectableElement(root, element, stage?.getBoundingClientRect(), selectionOptions(root))) element = null;
|
|
1628
1929
|
const ref = element && elementAddress(root, element);
|
|
1629
1930
|
const next = ref ? { overlayId: selectedId, ref, tag: element.tagName.toLowerCase(),
|
|
1630
1931
|
label: elementLabel(root, element) } : null;
|
|
@@ -1643,7 +1944,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1643
1944
|
const root = fragmentRoot(container);
|
|
1644
1945
|
const hit = event.target instanceof Element && root?.contains(event.target) ? event.target
|
|
1645
1946
|
: document.elementsFromPoint(event.clientX, event.clientY).find(element => root?.contains(element)) ?? null;
|
|
1646
|
-
focusElement(nearestSelectableElement(root, hit, stage?.getBoundingClientRect()));
|
|
1947
|
+
focusElement(nearestSelectableElement(root, hit, stage?.getBoundingClientRect(), selectionOptions(root)));
|
|
1647
1948
|
}
|
|
1648
1949
|
function focusElementAtPoint(overlayId, clientX, clientY, attempt = 0) {
|
|
1649
1950
|
if (window.akari.capabilities?.elementSelection !== true || !Number.isFinite(clientX)
|
|
@@ -1662,7 +1963,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1662
1963
|
return false;
|
|
1663
1964
|
}
|
|
1664
1965
|
const hit = document.elementsFromPoint(clientX, clientY).find(element => root.contains(element)) ?? null;
|
|
1665
|
-
focusElement(nearestSelectableElement(root, hit, stage?.getBoundingClientRect()));
|
|
1966
|
+
focusElement(nearestSelectableElement(root, hit, stage?.getBoundingClientRect(), selectionOptions(root)));
|
|
1666
1967
|
return true;
|
|
1667
1968
|
}
|
|
1668
1969
|
|
|
@@ -1746,7 +2047,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1746
2047
|
const element = target.element?.isConnected ? target.element
|
|
1747
2048
|
: elementByAddress(fragmentRoot(containerById(target.overlayId)), target.ref);
|
|
1748
2049
|
if (!element?.isConnected) return null;
|
|
1749
|
-
const geometry =
|
|
2050
|
+
const geometry = focusedSelectionGeometry(element);
|
|
1750
2051
|
const width = geometry.width * Math.hypot(geometry.axes.x.x, geometry.axes.x.y);
|
|
1751
2052
|
const height = geometry.height * Math.hypot(geometry.axes.y.x, geometry.axes.y.y);
|
|
1752
2053
|
return { rect: { left: geometry.center.x - width / 2, top: geometry.center.y - height / 2,
|
|
@@ -1773,7 +2074,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1773
2074
|
const root = fragmentRoot(selectedOverlay);
|
|
1774
2075
|
const path = [];
|
|
1775
2076
|
for (let node = focusedElement(); node && node !== root; node = node.parentElement) {
|
|
1776
|
-
if (selectableElement(root, node, stage?.getBoundingClientRect())) path.unshift(node);
|
|
2077
|
+
if (selectableElement(root, node, stage?.getBoundingClientRect(), selectionOptions(root))) path.unshift(node);
|
|
1777
2078
|
}
|
|
1778
2079
|
for (const node of path) append(elementLabel(root, node), () => focusElement(node),
|
|
1779
2080
|
{ kind: 'element', element: node, overlayId: selectedId, ref: elementAddress(root, node) });
|
|
@@ -2264,6 +2565,48 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
2264
2565
|
elementGeometryCache = { element, signature, geometry };
|
|
2265
2566
|
return geometry;
|
|
2266
2567
|
}
|
|
2568
|
+
function focusedSelectionGeometry(element) {
|
|
2569
|
+
const geometry = elementScreenGeometry(element);
|
|
2570
|
+
if (!isSelectionContainer(element)) return geometry;
|
|
2571
|
+
const container = selectedOverlay ?? element.parentElement;
|
|
2572
|
+
const bounds = contentBounds(element, container);
|
|
2573
|
+
if (!bounds) return geometry;
|
|
2574
|
+
const scaleX = Math.hypot(geometry.axes.x.x, geometry.axes.x.y);
|
|
2575
|
+
const scaleY = Math.hypot(geometry.axes.y.x, geometry.axes.y.y);
|
|
2576
|
+
if (!(scaleX > 0 && scaleY > 0)) return geometry;
|
|
2577
|
+
const ux = { x: geometry.axes.x.x / scaleX, y: geometry.axes.x.y / scaleX };
|
|
2578
|
+
const uy = { x: geometry.axes.y.x / scaleY, y: geometry.axes.y.y / scaleY };
|
|
2579
|
+
const angle = Math.abs(Math.atan2(ux.y, ux.x));
|
|
2580
|
+
if (angle < 0.001) return { ...geometry,
|
|
2581
|
+
center: { x: bounds.left + bounds.width / 2, y: bounds.top + bounds.height / 2 },
|
|
2582
|
+
width: bounds.width / scaleX, height: bounds.height / scaleY };
|
|
2583
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
2584
|
+
for (const node of element.querySelectorAll('*')) {
|
|
2585
|
+
if (!drawsSelectionContent(node) || !isPaintedElement(node, container)) continue;
|
|
2586
|
+
const rect = visibleFragmentRect(node.getBoundingClientRect(), node, container);
|
|
2587
|
+
if (!rect) continue;
|
|
2588
|
+
const center = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
|
2589
|
+
const dx = center.x - geometry.center.x, dy = center.y - geometry.center.y;
|
|
2590
|
+
const x = dx * ux.x + dy * ux.y, y = dx * uy.x + dy * uy.y;
|
|
2591
|
+
const halfX = (node.offsetWidth || rect.width / scaleX) * scaleX / 2;
|
|
2592
|
+
const halfY = (node.offsetHeight || rect.height / scaleY) * scaleY / 2;
|
|
2593
|
+
minX = Math.min(minX, x - halfX); maxX = Math.max(maxX, x + halfX);
|
|
2594
|
+
minY = Math.min(minY, y - halfY); maxY = Math.max(maxY, y + halfY);
|
|
2595
|
+
}
|
|
2596
|
+
if (!Number.isFinite(minX)) return geometry;
|
|
2597
|
+
const midX = (minX + maxX) / 2, midY = (minY + maxY) / 2;
|
|
2598
|
+
return { ...geometry, center: { x: geometry.center.x + midX * ux.x + midY * uy.x,
|
|
2599
|
+
y: geometry.center.y + midX * ux.y + midY * uy.y },
|
|
2600
|
+
width: (maxX - minX) / scaleX, height: (maxY - minY) / scaleY };
|
|
2601
|
+
}
|
|
2602
|
+
function focusedSelectionRect(element) {
|
|
2603
|
+
const geometry = focusedSelectionGeometry(element);
|
|
2604
|
+
const width = geometry.width * Math.hypot(geometry.axes.x.x, geometry.axes.x.y);
|
|
2605
|
+
const height = geometry.height * Math.hypot(geometry.axes.y.x, geometry.axes.y.y);
|
|
2606
|
+
return { left: geometry.center.x - width / 2, top: geometry.center.y - height / 2,
|
|
2607
|
+
right: geometry.center.x + width / 2, bottom: geometry.center.y + height / 2,
|
|
2608
|
+
width, height };
|
|
2609
|
+
}
|
|
2267
2610
|
function moveElementPointTo(gesture, horizontal, vertical, target) {
|
|
2268
2611
|
const geometry = elementScreenGeometry(gesture.element);
|
|
2269
2612
|
const point = elementPoint(geometry, horizontal, vertical);
|
|
@@ -2629,8 +2972,8 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
2629
2972
|
&& (!scoped || scoped.selectId === container.dataset.overlayId)) {
|
|
2630
2973
|
const root = fragmentRoot(container);
|
|
2631
2974
|
const hit = event.target instanceof Element && root?.contains(event.target) ? event.target : null;
|
|
2632
|
-
const element = nearestSelectableElement(root, hit, stage?.getBoundingClientRect());
|
|
2633
|
-
const rect = element
|
|
2975
|
+
const element = nearestSelectableElement(root, hit, stage?.getBoundingClientRect(), selectionOptions(root));
|
|
2976
|
+
const rect = element ? focusedSelectionRect(element) : fragmentBounds(container);
|
|
2634
2977
|
if (!rect || selectedOverlay === container
|
|
2635
2978
|
&& selectedElementFocus()?.ref === elementAddress(root, element)) { hideHover(); return; }
|
|
2636
2979
|
showHoverFrame(rect, null, container.dataset.overlayId, false, false);
|
|
@@ -3983,6 +4326,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
3983
4326
|
|
|
3984
4327
|
function onPointerDown(event) {
|
|
3985
4328
|
if (!interactionEnabled || !canBeginPointerInteraction(pointerOwner)) return;
|
|
4329
|
+
event = correctedHitPolicyEvent(event);
|
|
3986
4330
|
if (event.button !== 0 || activeDrag || activeResize || activeRotate || activeLine) return;
|
|
3987
4331
|
if (selectedId && stage && event.target instanceof Element) {
|
|
3988
4332
|
const bounds = stage.getBoundingClientRect();
|
|
@@ -4042,7 +4386,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
4042
4386
|
if (window.akari.capabilities?.elementSelection === true && event.isTrusted && !event.shiftKey) {
|
|
4043
4387
|
const root = fragmentRoot(hit);
|
|
4044
4388
|
const target = event.target instanceof Element && root?.contains(event.target)
|
|
4045
|
-
? nearestSelectableElement(root, event.target, stage?.getBoundingClientRect()) : null;
|
|
4389
|
+
? nearestSelectableElement(root, event.target, stage?.getBoundingClientRect(), selectionOptions(root)) : null;
|
|
4046
4390
|
clickOrigin.hitId = hit.dataset.overlayId;
|
|
4047
4391
|
clickOrigin.elementRef = target ? elementAddress(root, target) : null;
|
|
4048
4392
|
}
|
|
@@ -4057,7 +4401,10 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
4057
4401
|
if (!selectedOverlay || (window.akari.capabilities?.elementSelection === true
|
|
4058
4402
|
? selectedId !== hit.dataset.overlayId : selectedOverlay !== hit)) return;
|
|
4059
4403
|
if (!event.shiftKey) {
|
|
4060
|
-
|
|
4404
|
+
const focused = focusedElement();
|
|
4405
|
+
if (focused && isSelectionContainer(focused) && focused.contains(event.target)) {
|
|
4406
|
+
clickOrigin.preservedContainer = true;
|
|
4407
|
+
} else if (clickOrigin?.hitId === selectedId) {
|
|
4061
4408
|
focusElement(elementByAddress(fragmentRoot(selectedOverlay), clickOrigin.elementRef));
|
|
4062
4409
|
} else focusElementAt(selectedOverlay, event);
|
|
4063
4410
|
} else if (selectedElementFocus()) focusElement(null);
|
|
@@ -4093,7 +4440,12 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
4093
4440
|
}
|
|
4094
4441
|
|
|
4095
4442
|
selectOverlay(container);
|
|
4096
|
-
if (!event.shiftKey)
|
|
4443
|
+
if (!event.shiftKey) {
|
|
4444
|
+
const focused = focusedElement();
|
|
4445
|
+
if (focused && isSelectionContainer(focused) && focused.contains(event.target)) {
|
|
4446
|
+
clickOrigin.preservedContainer = true;
|
|
4447
|
+
} else focusElementAt(container, event);
|
|
4448
|
+
}
|
|
4097
4449
|
else if (selectedElementFocus()) focusElement(null);
|
|
4098
4450
|
if (window.akari.capabilities?.elementSelection === true && clickOrigin) {
|
|
4099
4451
|
clickOrigin.hitId = selectedId;
|
|
@@ -4833,6 +5185,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
4833
5185
|
|
|
4834
5186
|
function onClick(event) {
|
|
4835
5187
|
if (!interactionEnabled || activeEdit) return;
|
|
5188
|
+
event = correctedHitPolicyEvent(event);
|
|
4836
5189
|
const hit = overlayForEvent(event);
|
|
4837
5190
|
// pointerdown already toggled. Do not toggle twice or update the cycle clock.
|
|
4838
5191
|
if (event.shiftKey && clickOrigin?.scopedHit) {
|
|
@@ -4855,7 +5208,8 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
4855
5208
|
else selectOverlay(hit);
|
|
4856
5209
|
if (window.akari.capabilities?.elementSelection === true
|
|
4857
5210
|
&& event.isTrusted && !event.shiftKey && selectedOverlay) {
|
|
4858
|
-
if (
|
|
5211
|
+
if (clickOrigin?.preservedContainer) focusElementAt(selectedOverlay, event);
|
|
5212
|
+
else if (!nextId && clickOrigin?.hitId === selectedId) {
|
|
4859
5213
|
focusElement(elementByAddress(fragmentRoot(selectedOverlay), clickOrigin.elementRef));
|
|
4860
5214
|
} else focusElementAt(selectedOverlay, event);
|
|
4861
5215
|
}
|
|
@@ -4923,7 +5277,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
4923
5277
|
if (event.key === 'Escape' || event.key === 'Enter' && event.shiftKey) {
|
|
4924
5278
|
const root = fragmentRoot(selectedOverlay);
|
|
4925
5279
|
const parent = nearestSelectableElement(root, focusedElement()?.parentElement,
|
|
4926
|
-
stage?.getBoundingClientRect());
|
|
5280
|
+
stage?.getBoundingClientRect(), selectionOptions(root));
|
|
4927
5281
|
focusElement(parent); stop(); return;
|
|
4928
5282
|
}
|
|
4929
5283
|
if ((event.key === 'Delete' || event.key === 'Backspace'
|
|
@@ -4935,7 +5289,8 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
4935
5289
|
}
|
|
4936
5290
|
if (event.key === 'Enter' && !event.shiftKey && !activeEdit && !selectedElementFocus()
|
|
4937
5291
|
&& selectedOverlay && canFocusElement(selectedOverlay)) {
|
|
4938
|
-
const
|
|
5292
|
+
const root = fragmentRoot(selectedOverlay);
|
|
5293
|
+
const first = firstSelectableElement(root, stage?.getBoundingClientRect(), selectionOptions(root));
|
|
4939
5294
|
if (first) {
|
|
4940
5295
|
focusElement(first);
|
|
4941
5296
|
event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation();
|
|
@@ -5329,11 +5684,73 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
5329
5684
|
for (const type of ["pointerdown", "pointerup", "click", "dblclick"]) {
|
|
5330
5685
|
window.addEventListener(type, passTransparentCanvasEvent, true);
|
|
5331
5686
|
}
|
|
5687
|
+
const correctedHitPolicyEvents = new WeakMap();
|
|
5688
|
+
let redirectedHitPolicyClick = null;
|
|
5689
|
+
function pointerInsideStage(event) {
|
|
5690
|
+
if (!stage || !Number.isFinite(event.clientX) || !Number.isFinite(event.clientY)) return false;
|
|
5691
|
+
const rect = stage.getBoundingClientRect();
|
|
5692
|
+
return event.clientX >= rect.left && event.clientX <= rect.right
|
|
5693
|
+
&& event.clientY >= rect.top && event.clientY <= rect.bottom;
|
|
5694
|
+
}
|
|
5695
|
+
function trustedEventAtTarget(event, target) {
|
|
5696
|
+
// 合成 Event は isTrusted=false になり E2 の焦点を失う。元の信頼済み
|
|
5697
|
+
// イベントを保ったまま、選択処理に見せる target だけ現在の hit-test にする。
|
|
5698
|
+
return new Proxy(event, { get(original, key) {
|
|
5699
|
+
if (key === 'target') return target;
|
|
5700
|
+
const value = Reflect.get(original, key, original);
|
|
5701
|
+
return typeof value === 'function' ? value.bind(original) : value;
|
|
5702
|
+
} });
|
|
5703
|
+
}
|
|
5704
|
+
function correctHitPolicyEvent(event, target) {
|
|
5705
|
+
correctedHitPolicyEvents.set(event, target);
|
|
5706
|
+
// Window capture の後続ホストにも素の HTML hit と同じ target を見せる。
|
|
5707
|
+
// Event 自体は元の信頼済みイベントなので、再生停止や混在選択は一度だけ走る。
|
|
5708
|
+
try { Object.defineProperty(event, 'target', { configurable: true, value: target }); }
|
|
5709
|
+
catch { /* document 側は WeakMap の target へフォールバックする。 */ }
|
|
5710
|
+
}
|
|
5711
|
+
function correctedHitPolicyEvent(event) {
|
|
5712
|
+
const target = correctedHitPolicyEvents.get(event);
|
|
5713
|
+
return target && event.target !== target ? trustedEventAtTarget(event, target) : event;
|
|
5714
|
+
}
|
|
5715
|
+
function stopCorrectedHitPolicyPropagation(event) {
|
|
5716
|
+
// window のホスト捕捉は通し、古い preview-stage への下流だけ止める。
|
|
5717
|
+
if (findOverlayContainer(correctedHitPolicyEvents.get(event))) event.stopPropagation();
|
|
5718
|
+
}
|
|
5719
|
+
window.addEventListener('pointermove', event => {
|
|
5720
|
+
if (!pointerInsideStage(event) || !staleHitPolicyContainers.size
|
|
5721
|
+
|| hitPolicyOperationBusy()) return;
|
|
5722
|
+
const now = performance.now();
|
|
5723
|
+
if (now - lastPointerHitPolicyRefresh < 125) return;
|
|
5724
|
+
if (!refreshStaleHitPolicies()) return;
|
|
5725
|
+
lastPointerHitPolicyRefresh = now;
|
|
5726
|
+
const target = document.elementFromPoint(event.clientX, event.clientY);
|
|
5727
|
+
if (target && target !== event.target) correctHitPolicyEvent(event, target);
|
|
5728
|
+
}, true);
|
|
5729
|
+
window.addEventListener('pointerdown', event => {
|
|
5730
|
+
if (event.button !== 0 || !pointerInsideStage(event) || hitPolicyOperationBusy()) return;
|
|
5731
|
+
redirectedHitPolicyClick = null;
|
|
5732
|
+
if (!refreshStaleHitPolicies()) return;
|
|
5733
|
+
const target = document.elementFromPoint(event.clientX, event.clientY);
|
|
5734
|
+
if (!target || target === event.target) return;
|
|
5735
|
+
redirectedHitPolicyClick = { x: event.clientX, y: event.clientY };
|
|
5736
|
+
correctHitPolicyEvent(event, target);
|
|
5737
|
+
}, true);
|
|
5738
|
+
window.addEventListener('click', event => {
|
|
5739
|
+
if (!redirectedHitPolicyClick) return;
|
|
5740
|
+
const redirect = redirectedHitPolicyClick;
|
|
5741
|
+
redirectedHitPolicyClick = null;
|
|
5742
|
+
if (Math.hypot(event.clientX - redirect.x, event.clientY - redirect.y) > 6) return;
|
|
5743
|
+
const target = document.elementFromPoint(event.clientX, event.clientY);
|
|
5744
|
+
if (!target || target === event.target) return;
|
|
5745
|
+
correctHitPolicyEvent(event, target);
|
|
5746
|
+
}, true);
|
|
5332
5747
|
listenerRoot.addEventListener('pointermove', updateShapeLineHitProxyWidths, true);
|
|
5333
5748
|
listenerRoot.addEventListener('pointerdown', updateShapeLineHitProxyWidths, true);
|
|
5334
5749
|
window.addEventListener('resize', updateShapeLineHitProxyWidths);
|
|
5335
5750
|
listenerRoot.addEventListener("click", onClick, true);
|
|
5336
5751
|
listenerRoot.addEventListener("pointerdown", onPointerDown, true);
|
|
5752
|
+
listenerRoot.addEventListener("click", stopCorrectedHitPolicyPropagation, true);
|
|
5753
|
+
listenerRoot.addEventListener("pointerdown", stopCorrectedHitPolicyPropagation, true);
|
|
5337
5754
|
listenerRoot.addEventListener("dblclick", onDoubleClick, true);
|
|
5338
5755
|
listenerRoot.addEventListener("blur", onBlur, true);
|
|
5339
5756
|
listenerRoot.addEventListener("input", onEditableInput, true);
|
|
@@ -5444,6 +5861,8 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
5444
5861
|
// ㉑ 素通し: overlay-runtime.js の tick() が可視化タイミングで呼ぶ。
|
|
5445
5862
|
applyOverlayHitPolicy,
|
|
5446
5863
|
invalidateOverlayHitPolicy,
|
|
5864
|
+
markOverlayHitPolicyStale,
|
|
5865
|
+
forgetOverlayHitPolicyTracking,
|
|
5447
5866
|
syncOverlayHitRegion,
|
|
5448
5867
|
// Web UI(preview-server)が編集モードを抜けるときに選択枠を畳むための公開口
|
|
5449
5868
|
// (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);
|