akari-video 1.3.0-beta.1 → 1.3.0-beta.3
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 +293 -33
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "1.3.0-beta.
|
|
3
|
+
"version": "1.3.0-beta.3",
|
|
4
4
|
"description": "AKARI Video launcher CLI — start an AI-edited video project from any directory: scaffold, connection check, then hand over to Claude Code (or opencode). AKARI Video を opencode や Claude Code で、どのディレクトリからでも始めるための `akari` ランチャー CLI。接続確認(doctor)→ 未セットアップならプロジェクト雛形を作成 → AI エージェントを起動する。外部 npm 依存ゼロ(Node.js 組み込みモジュールのみ)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "1.3.0-beta.
|
|
3
|
+
"version": "1.3.0-beta.3",
|
|
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) {
|
|
@@ -289,6 +422,8 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
289
422
|
let selectedId = null;
|
|
290
423
|
let selectedIds = [];
|
|
291
424
|
let elementFocus = null;
|
|
425
|
+
// Inner selection belongs only to the currently selected telop, never to edit.json.
|
|
426
|
+
let telopInnerId = null;
|
|
292
427
|
let elementGeometryCache = null;
|
|
293
428
|
let elementNudge = null;
|
|
294
429
|
let elementNudgeTimer = null;
|
|
@@ -576,6 +711,21 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
576
711
|
return null;
|
|
577
712
|
}
|
|
578
713
|
|
|
714
|
+
function contentBounds(element, container) {
|
|
715
|
+
return selectionContentBounds(element, {
|
|
716
|
+
container,
|
|
717
|
+
visibleRect: (rect, node) => visibleFragmentRect(rect, node, container),
|
|
718
|
+
rawRect: node => node.tagName.toUpperCase() === "CANVAS"
|
|
719
|
+
? (canvasHasDecoration(node) ? node.getBoundingClientRect()
|
|
720
|
+
: window.akari.threeRuntime?.contentBounds?.(node)
|
|
721
|
+
?? measureCanvasBounds(node) ?? node.getBoundingClientRect())
|
|
722
|
+
: node.getBoundingClientRect(),
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
function selectionOptions(root) {
|
|
726
|
+
return { boundsFor: element => contentBounds(element, root?.parentElement) };
|
|
727
|
+
}
|
|
728
|
+
|
|
579
729
|
// 断片ルートは「inset:0 全画面ラッパー + flex/絶対配置」パターン(overlay-authoring
|
|
580
730
|
// 規約・text-behind-person.md 等)を許容するため、ルート自身の矩形がコンテナ(断片の
|
|
581
731
|
// 親 = [data-overlay-id] 要素。overlay-runtime.js の mount() が inset:0 で全画面付与)
|
|
@@ -594,7 +744,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
594
744
|
);
|
|
595
745
|
}
|
|
596
746
|
|
|
597
|
-
function
|
|
747
|
+
function legacyFragmentBounds(container) {
|
|
598
748
|
const root = fragmentRoot(container);
|
|
599
749
|
if (!root) return null;
|
|
600
750
|
|
|
@@ -684,6 +834,13 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
684
834
|
return { left, top, right, bottom, width: right - left, height: bottom - top };
|
|
685
835
|
}
|
|
686
836
|
|
|
837
|
+
function fragmentBounds(container) {
|
|
838
|
+
const root = fragmentRoot(container);
|
|
839
|
+
if (!root) return null;
|
|
840
|
+
if (drawsOwnContent(root, getComputedStyle(root))) return legacyFragmentBounds(container);
|
|
841
|
+
return contentBounds(root, container) ?? legacyFragmentBounds(container);
|
|
842
|
+
}
|
|
843
|
+
|
|
687
844
|
function svgReferenceRect(element, value) {
|
|
688
845
|
const id = String(value ?? '').match(/url\(["']?(?:[^#)]*#)([^)"']+)["']?\)/)?.[1];
|
|
689
846
|
const svg = element.namespaceURI === 'http://www.w3.org/2000/svg'
|
|
@@ -1507,7 +1664,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1507
1664
|
const transform = readTransform(selectedOverlay);
|
|
1508
1665
|
const lineRect = lineFrameGeometry(selectedOverlay);
|
|
1509
1666
|
const focused = focusedElement();
|
|
1510
|
-
const focusedGeometry = focused ?
|
|
1667
|
+
const focusedGeometry = focused ? focusedSelectionGeometry(focused) : null;
|
|
1511
1668
|
const focusedWidth = focusedGeometry && focusedGeometry.width * Math.hypot(focusedGeometry.axes.x.x, focusedGeometry.axes.x.y);
|
|
1512
1669
|
const focusedHeight = focusedGeometry && focusedGeometry.height * Math.hypot(focusedGeometry.axes.y.x, focusedGeometry.axes.y.y);
|
|
1513
1670
|
const focusedAngle = focusedGeometry && Math.atan2(focusedGeometry.axes.x.y, focusedGeometry.axes.x.x) * 180 / Math.PI;
|
|
@@ -1564,7 +1721,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1564
1721
|
}
|
|
1565
1722
|
const layout = elementHandleLayout(focusedWidth, focusedHeight);
|
|
1566
1723
|
const name = [...handle.classList].find(token => /^is-(?:n|e|s|w|nw|ne|se|sw|rotate|move)$/.test(token))?.slice(3);
|
|
1567
|
-
handle.style.display = name === 'move' ||
|
|
1724
|
+
handle.style.display = name === 'move' || (isSelectionContainer(focused) && name !== 'rotate') ||
|
|
1568
1725
|
(['n', 's'].includes(name) && layout.hideHorizontalEdges) ||
|
|
1569
1726
|
(['e', 'w'].includes(name) && layout.hideVerticalEdges) ? 'none' : '';
|
|
1570
1727
|
if (name === 'rotate') {
|
|
@@ -1575,6 +1732,11 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1575
1732
|
const y = name.includes('n') ? -layout.cornerOffsetY : layout.cornerOffsetY;
|
|
1576
1733
|
if (x || y) handle.style.translate = `${x}px ${y}px`;
|
|
1577
1734
|
else handle.style.removeProperty('translate');
|
|
1735
|
+
} else if (['n', 's', 'e', 'w'].includes(name)) {
|
|
1736
|
+
const x = name === 'w' ? -layout.cornerOffsetX : name === 'e' ? layout.cornerOffsetX : 0;
|
|
1737
|
+
const y = name === 'n' ? -layout.cornerOffsetY : name === 's' ? layout.cornerOffsetY : 0;
|
|
1738
|
+
if (x || y) handle.style.translate = `${x}px ${y}px`;
|
|
1739
|
+
else handle.style.removeProperty('translate');
|
|
1578
1740
|
}
|
|
1579
1741
|
if (name && name !== 'move' && name !== 'rotate') {
|
|
1580
1742
|
const directions = ['ew-resize', 'nwse-resize', 'ns-resize', 'nesw-resize'];
|
|
@@ -1659,8 +1821,11 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1659
1821
|
|
|
1660
1822
|
function clearSelection() {
|
|
1661
1823
|
const hadElementFocus = window.akari.capabilities?.elementSelection === true && Boolean(selectedElementFocus());
|
|
1824
|
+
const hadTelopSelection = window.akari.capabilities?.elementSelection === true
|
|
1825
|
+
&& selectedOverlay && isTelopOverlay(selectedOverlay);
|
|
1662
1826
|
flushElementNudge();
|
|
1663
1827
|
elementFocus = null;
|
|
1828
|
+
telopInnerId = null;
|
|
1664
1829
|
if (activeRotate) cancelRotate();
|
|
1665
1830
|
if (activeLine) finishLineEndpoint(true);
|
|
1666
1831
|
flushNudge();
|
|
@@ -1679,7 +1844,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1679
1844
|
selectionFrame?.remove();
|
|
1680
1845
|
selectionFrame = null;
|
|
1681
1846
|
hideSnapGuides();
|
|
1682
|
-
if (hadElementFocus) renderScopeBreadcrumb();
|
|
1847
|
+
if (hadElementFocus || hadTelopSelection) renderScopeBreadcrumb();
|
|
1683
1848
|
}
|
|
1684
1849
|
|
|
1685
1850
|
function handleSelectedOverlayUnavailable(container) {
|
|
@@ -1719,27 +1884,45 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1719
1884
|
selectedOverlay.setAttribute('data-akari-interaction-selected', 'true');
|
|
1720
1885
|
reconcileElementFocus();
|
|
1721
1886
|
} else {
|
|
1887
|
+
const keepTelopInner = selectedId === nextId && telopInnerId === nextId
|
|
1888
|
+
&& selectedIds.length === 1 && !groupSelection;
|
|
1722
1889
|
clearSelection();
|
|
1723
1890
|
selectedOverlay = container;
|
|
1724
1891
|
selectedId = nextId;
|
|
1725
1892
|
selectedIds = selectedId === null ? [] : [selectedId];
|
|
1893
|
+
if (keepTelopInner) telopInnerId = nextId;
|
|
1726
1894
|
selectedOverlay.setAttribute("data-akari-interaction-selected", "true");
|
|
1727
1895
|
}
|
|
1728
1896
|
}
|
|
1729
1897
|
|
|
1730
1898
|
refreshSelectionFrame();
|
|
1899
|
+
if (window.akari.capabilities?.elementSelection === true && isTelopOverlay(selectedOverlay)) renderScopeBreadcrumb();
|
|
1731
1900
|
startSelectionTracking();
|
|
1732
1901
|
return true;
|
|
1733
1902
|
}
|
|
1734
1903
|
|
|
1735
|
-
function
|
|
1904
|
+
function elementSelectionEnabled(container) {
|
|
1736
1905
|
if (window.akari.capabilities?.elementSelection !== true || !container || selectedIds.length > 1) return false;
|
|
1737
1906
|
return window.akari.state?.summary?.overlays?.some(overlay => overlay.id === container.dataset.overlayId
|
|
1738
1907
|
&& overlay.elementSelection === true) === true;
|
|
1739
1908
|
}
|
|
1909
|
+
function canFocusElement(container) {
|
|
1910
|
+
return elementSelectionEnabled(container)
|
|
1911
|
+
&& (!isTelopOverlay(container) || telopInnerId === selectedId && selectedId === container.dataset.overlayId);
|
|
1912
|
+
}
|
|
1913
|
+
function setTelopInnerSelection(overlayId, inner) {
|
|
1914
|
+
if (typeof overlayId !== 'string' || selectedId !== overlayId || selectedIds.length !== 1
|
|
1915
|
+
|| groupSelection || !selectedOverlay || !isTelopOverlay(selectedOverlay)
|
|
1916
|
+
|| !elementSelectionEnabled(selectedOverlay)) return false;
|
|
1917
|
+
telopInnerId = inner === true ? overlayId : null;
|
|
1918
|
+
if (!telopInnerId && selectedElementFocus()) focusElement(null);
|
|
1919
|
+
hideHover();
|
|
1920
|
+
return true;
|
|
1921
|
+
}
|
|
1740
1922
|
function focusedElement() {
|
|
1741
1923
|
if (!selectedElementFocus() || !selectedOverlay) return null;
|
|
1742
|
-
|
|
1924
|
+
const root = fragmentRoot(selectedOverlay);
|
|
1925
|
+
return elementByAddress(root, elementFocus.ref);
|
|
1743
1926
|
}
|
|
1744
1927
|
function selectedElementFocus() {
|
|
1745
1928
|
return elementFocus?.overlayId === selectedId
|
|
@@ -1748,13 +1931,27 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1748
1931
|
function reconcileElementFocus() {
|
|
1749
1932
|
if (!selectedElementFocus()) return;
|
|
1750
1933
|
const root = fragmentRoot(selectedOverlay);
|
|
1751
|
-
if (
|
|
1934
|
+
if (!root) return;
|
|
1935
|
+
const ref = elementFocus.ref;
|
|
1936
|
+
const element = elementByAddress(root, ref);
|
|
1937
|
+
if (!element) { focusElement(null); return; }
|
|
1938
|
+
if (selectedOverlay?.isConnected && !selectableElement(root, element, stage?.getBoundingClientRect(), selectionOptions(root))) {
|
|
1939
|
+
requestAnimationFrame(() => {
|
|
1940
|
+
if (elementFocus?.ref === ref && selectedOverlay?.isConnected) {
|
|
1941
|
+
const currentRoot = fragmentRoot(selectedOverlay);
|
|
1942
|
+
const current = elementByAddress(currentRoot, ref);
|
|
1943
|
+
if (!current || !selectableElement(currentRoot, current, stage?.getBoundingClientRect(), selectionOptions(currentRoot))) focusElement(null);
|
|
1944
|
+
}
|
|
1945
|
+
});
|
|
1946
|
+
}
|
|
1752
1947
|
}
|
|
1753
1948
|
function focusElement(element, { notify = true } = {}) {
|
|
1754
1949
|
const root = fragmentRoot(selectedOverlay);
|
|
1950
|
+
if (element && !selectableElement(root, element, stage?.getBoundingClientRect(), selectionOptions(root))) element = null;
|
|
1755
1951
|
const ref = element && elementAddress(root, element);
|
|
1756
1952
|
const next = ref ? { overlayId: selectedId, ref, tag: element.tagName.toLowerCase(),
|
|
1757
1953
|
label: elementLabel(root, element) } : null;
|
|
1954
|
+
if (!next && telopInnerId === selectedId) telopInnerId = null;
|
|
1758
1955
|
if (elementFocus?.ref === next?.ref && elementFocus?.overlayId === next?.overlayId) return;
|
|
1759
1956
|
flushElementNudge();
|
|
1760
1957
|
elementFocus = next;
|
|
@@ -1770,7 +1967,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1770
1967
|
const root = fragmentRoot(container);
|
|
1771
1968
|
const hit = event.target instanceof Element && root?.contains(event.target) ? event.target
|
|
1772
1969
|
: document.elementsFromPoint(event.clientX, event.clientY).find(element => root?.contains(element)) ?? null;
|
|
1773
|
-
focusElement(nearestSelectableElement(root, hit, stage?.getBoundingClientRect()));
|
|
1970
|
+
focusElement(nearestSelectableElement(root, hit, stage?.getBoundingClientRect(), selectionOptions(root)));
|
|
1774
1971
|
}
|
|
1775
1972
|
function focusElementAtPoint(overlayId, clientX, clientY, attempt = 0) {
|
|
1776
1973
|
if (window.akari.capabilities?.elementSelection !== true || !Number.isFinite(clientX)
|
|
@@ -1789,7 +1986,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1789
1986
|
return false;
|
|
1790
1987
|
}
|
|
1791
1988
|
const hit = document.elementsFromPoint(clientX, clientY).find(element => root.contains(element)) ?? null;
|
|
1792
|
-
focusElement(nearestSelectableElement(root, hit, stage?.getBoundingClientRect()));
|
|
1989
|
+
focusElement(nearestSelectableElement(root, hit, stage?.getBoundingClientRect(), selectionOptions(root)));
|
|
1793
1990
|
return true;
|
|
1794
1991
|
}
|
|
1795
1992
|
|
|
@@ -1873,7 +2070,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1873
2070
|
const element = target.element?.isConnected ? target.element
|
|
1874
2071
|
: elementByAddress(fragmentRoot(containerById(target.overlayId)), target.ref);
|
|
1875
2072
|
if (!element?.isConnected) return null;
|
|
1876
|
-
const geometry =
|
|
2073
|
+
const geometry = focusedSelectionGeometry(element);
|
|
1877
2074
|
const width = geometry.width * Math.hypot(geometry.axes.x.x, geometry.axes.x.y);
|
|
1878
2075
|
const height = geometry.height * Math.hypot(geometry.axes.y.x, geometry.axes.y.y);
|
|
1879
2076
|
return { rect: { left: geometry.center.x - width / 2, top: geometry.center.y - height / 2,
|
|
@@ -1889,7 +2086,8 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1889
2086
|
clearBreadcrumbHover();
|
|
1890
2087
|
const entries = [];
|
|
1891
2088
|
const append = (label, action, target) => entries.push({ label, action, target });
|
|
1892
|
-
if (selectedElementFocus()
|
|
2089
|
+
if (selectedOverlay && (selectedElementFocus() || window.akari.capabilities?.elementSelection === true
|
|
2090
|
+
&& selectedIds.length === 1 && isTelopOverlay(selectedOverlay))) {
|
|
1893
2091
|
append('全体', () => { clearSelection(); publishScopedSelection(); }, { kind: 'stage' });
|
|
1894
2092
|
for (const id of lineage(selectionTree(), scopeId)) {
|
|
1895
2093
|
append(treeNode(id)?.label ?? id, () => applyScopedSelection({ selectId: id, scopeId: treeNode(id)?.parentId ?? null }),
|
|
@@ -1900,7 +2098,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1900
2098
|
const root = fragmentRoot(selectedOverlay);
|
|
1901
2099
|
const path = [];
|
|
1902
2100
|
for (let node = focusedElement(); node && node !== root; node = node.parentElement) {
|
|
1903
|
-
if (selectableElement(root, node, stage?.getBoundingClientRect())) path.unshift(node);
|
|
2101
|
+
if (selectableElement(root, node, stage?.getBoundingClientRect(), selectionOptions(root))) path.unshift(node);
|
|
1904
2102
|
}
|
|
1905
2103
|
for (const node of path) append(elementLabel(root, node), () => focusElement(node),
|
|
1906
2104
|
{ kind: 'element', element: node, overlayId: selectedId, ref: elementAddress(root, node) });
|
|
@@ -2391,6 +2589,48 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
2391
2589
|
elementGeometryCache = { element, signature, geometry };
|
|
2392
2590
|
return geometry;
|
|
2393
2591
|
}
|
|
2592
|
+
function focusedSelectionGeometry(element) {
|
|
2593
|
+
const geometry = elementScreenGeometry(element);
|
|
2594
|
+
if (!isSelectionContainer(element)) return geometry;
|
|
2595
|
+
const container = selectedOverlay ?? element.parentElement;
|
|
2596
|
+
const bounds = contentBounds(element, container);
|
|
2597
|
+
if (!bounds) return geometry;
|
|
2598
|
+
const scaleX = Math.hypot(geometry.axes.x.x, geometry.axes.x.y);
|
|
2599
|
+
const scaleY = Math.hypot(geometry.axes.y.x, geometry.axes.y.y);
|
|
2600
|
+
if (!(scaleX > 0 && scaleY > 0)) return geometry;
|
|
2601
|
+
const ux = { x: geometry.axes.x.x / scaleX, y: geometry.axes.x.y / scaleX };
|
|
2602
|
+
const uy = { x: geometry.axes.y.x / scaleY, y: geometry.axes.y.y / scaleY };
|
|
2603
|
+
const angle = Math.abs(Math.atan2(ux.y, ux.x));
|
|
2604
|
+
if (angle < 0.001) return { ...geometry,
|
|
2605
|
+
center: { x: bounds.left + bounds.width / 2, y: bounds.top + bounds.height / 2 },
|
|
2606
|
+
width: bounds.width / scaleX, height: bounds.height / scaleY };
|
|
2607
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
2608
|
+
for (const node of element.querySelectorAll('*')) {
|
|
2609
|
+
if (!drawsSelectionContent(node) || !isPaintedElement(node, container)) continue;
|
|
2610
|
+
const rect = visibleFragmentRect(node.getBoundingClientRect(), node, container);
|
|
2611
|
+
if (!rect) continue;
|
|
2612
|
+
const center = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
|
2613
|
+
const dx = center.x - geometry.center.x, dy = center.y - geometry.center.y;
|
|
2614
|
+
const x = dx * ux.x + dy * ux.y, y = dx * uy.x + dy * uy.y;
|
|
2615
|
+
const halfX = (node.offsetWidth || rect.width / scaleX) * scaleX / 2;
|
|
2616
|
+
const halfY = (node.offsetHeight || rect.height / scaleY) * scaleY / 2;
|
|
2617
|
+
minX = Math.min(minX, x - halfX); maxX = Math.max(maxX, x + halfX);
|
|
2618
|
+
minY = Math.min(minY, y - halfY); maxY = Math.max(maxY, y + halfY);
|
|
2619
|
+
}
|
|
2620
|
+
if (!Number.isFinite(minX)) return geometry;
|
|
2621
|
+
const midX = (minX + maxX) / 2, midY = (minY + maxY) / 2;
|
|
2622
|
+
return { ...geometry, center: { x: geometry.center.x + midX * ux.x + midY * uy.x,
|
|
2623
|
+
y: geometry.center.y + midX * ux.y + midY * uy.y },
|
|
2624
|
+
width: (maxX - minX) / scaleX, height: (maxY - minY) / scaleY };
|
|
2625
|
+
}
|
|
2626
|
+
function focusedSelectionRect(element) {
|
|
2627
|
+
const geometry = focusedSelectionGeometry(element);
|
|
2628
|
+
const width = geometry.width * Math.hypot(geometry.axes.x.x, geometry.axes.x.y);
|
|
2629
|
+
const height = geometry.height * Math.hypot(geometry.axes.y.x, geometry.axes.y.y);
|
|
2630
|
+
return { left: geometry.center.x - width / 2, top: geometry.center.y - height / 2,
|
|
2631
|
+
right: geometry.center.x + width / 2, bottom: geometry.center.y + height / 2,
|
|
2632
|
+
width, height };
|
|
2633
|
+
}
|
|
2394
2634
|
function moveElementPointTo(gesture, horizontal, vertical, target) {
|
|
2395
2635
|
const geometry = elementScreenGeometry(gesture.element);
|
|
2396
2636
|
const point = elementPoint(geometry, horizontal, vertical);
|
|
@@ -2756,8 +2996,8 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
2756
2996
|
&& (!scoped || scoped.selectId === container.dataset.overlayId)) {
|
|
2757
2997
|
const root = fragmentRoot(container);
|
|
2758
2998
|
const hit = event.target instanceof Element && root?.contains(event.target) ? event.target : null;
|
|
2759
|
-
const element = nearestSelectableElement(root, hit, stage?.getBoundingClientRect());
|
|
2760
|
-
const rect = element
|
|
2999
|
+
const element = nearestSelectableElement(root, hit, stage?.getBoundingClientRect(), selectionOptions(root));
|
|
3000
|
+
const rect = element ? focusedSelectionRect(element) : fragmentBounds(container);
|
|
2761
3001
|
if (!rect || selectedOverlay === container
|
|
2762
3002
|
&& selectedElementFocus()?.ref === elementAddress(root, element)) { hideHover(); return; }
|
|
2763
3003
|
showHoverFrame(rect, null, container.dataset.overlayId, false, false);
|
|
@@ -4167,10 +4407,11 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
4167
4407
|
if (activeEdit?.container === hit && eventHitsElement(event, activeEdit.element)) return;
|
|
4168
4408
|
clickOrigin.scopedHit = true;
|
|
4169
4409
|
if (activeEdit) void commitEdit();
|
|
4170
|
-
if (window.akari.capabilities?.elementSelection === true && event.isTrusted && !event.shiftKey
|
|
4410
|
+
if (window.akari.capabilities?.elementSelection === true && event.isTrusted && !event.shiftKey
|
|
4411
|
+
&& (!isTelopOverlay(hit) || canFocusElement(hit))) {
|
|
4171
4412
|
const root = fragmentRoot(hit);
|
|
4172
4413
|
const target = event.target instanceof Element && root?.contains(event.target)
|
|
4173
|
-
? nearestSelectableElement(root, event.target, stage?.getBoundingClientRect()) : null;
|
|
4414
|
+
? nearestSelectableElement(root, event.target, stage?.getBoundingClientRect(), selectionOptions(root)) : null;
|
|
4174
4415
|
clickOrigin.hitId = hit.dataset.overlayId;
|
|
4175
4416
|
clickOrigin.elementRef = target ? elementAddress(root, target) : null;
|
|
4176
4417
|
}
|
|
@@ -4185,7 +4426,11 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
4185
4426
|
if (!selectedOverlay || (window.akari.capabilities?.elementSelection === true
|
|
4186
4427
|
? selectedId !== hit.dataset.overlayId : selectedOverlay !== hit)) return;
|
|
4187
4428
|
if (!event.shiftKey) {
|
|
4188
|
-
|
|
4429
|
+
const focused = focusedElement();
|
|
4430
|
+
if (focused && isSelectionContainer(focused) && focused.contains(event.target)) {
|
|
4431
|
+
clickOrigin.preservedContainer = true;
|
|
4432
|
+
} else if ((!isTelopOverlay(selectedOverlay) || canFocusElement(selectedOverlay))
|
|
4433
|
+
&& clickOrigin?.hitId === selectedId) {
|
|
4189
4434
|
focusElement(elementByAddress(fragmentRoot(selectedOverlay), clickOrigin.elementRef));
|
|
4190
4435
|
} else focusElementAt(selectedOverlay, event);
|
|
4191
4436
|
} else if (selectedElementFocus()) focusElement(null);
|
|
@@ -4221,7 +4466,12 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
4221
4466
|
}
|
|
4222
4467
|
|
|
4223
4468
|
selectOverlay(container);
|
|
4224
|
-
if (!event.shiftKey)
|
|
4469
|
+
if (!event.shiftKey) {
|
|
4470
|
+
const focused = focusedElement();
|
|
4471
|
+
if (focused && isSelectionContainer(focused) && focused.contains(event.target)) {
|
|
4472
|
+
clickOrigin.preservedContainer = true;
|
|
4473
|
+
} else focusElementAt(container, event);
|
|
4474
|
+
}
|
|
4225
4475
|
else if (selectedElementFocus()) focusElement(null);
|
|
4226
4476
|
if (window.akari.capabilities?.elementSelection === true && clickOrigin) {
|
|
4227
4477
|
clickOrigin.hitId = selectedId;
|
|
@@ -4984,7 +5234,10 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
4984
5234
|
else selectOverlay(hit);
|
|
4985
5235
|
if (window.akari.capabilities?.elementSelection === true
|
|
4986
5236
|
&& event.isTrusted && !event.shiftKey && selectedOverlay) {
|
|
4987
|
-
if (
|
|
5237
|
+
if (clickOrigin?.preservedContainer) focusElementAt(selectedOverlay, event);
|
|
5238
|
+
else if ((!isTelopOverlay(selectedOverlay) || canFocusElement(selectedOverlay))
|
|
5239
|
+
&& (!nextId || isTelopOverlay(selectedOverlay) && telopInnerId === selectedId
|
|
5240
|
+
&& nextId === selectedId) && clickOrigin?.hitId === selectedId) {
|
|
4988
5241
|
focusElement(elementByAddress(fragmentRoot(selectedOverlay), clickOrigin.elementRef));
|
|
4989
5242
|
} else focusElementAt(selectedOverlay, event);
|
|
4990
5243
|
}
|
|
@@ -5052,7 +5305,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
5052
5305
|
if (event.key === 'Escape' || event.key === 'Enter' && event.shiftKey) {
|
|
5053
5306
|
const root = fragmentRoot(selectedOverlay);
|
|
5054
5307
|
const parent = nearestSelectableElement(root, focusedElement()?.parentElement,
|
|
5055
|
-
stage?.getBoundingClientRect());
|
|
5308
|
+
stage?.getBoundingClientRect(), selectionOptions(root));
|
|
5056
5309
|
focusElement(parent); stop(); return;
|
|
5057
5310
|
}
|
|
5058
5311
|
if ((event.key === 'Delete' || event.key === 'Backspace'
|
|
@@ -5063,9 +5316,12 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
5063
5316
|
}
|
|
5064
5317
|
}
|
|
5065
5318
|
if (event.key === 'Enter' && !event.shiftKey && !activeEdit && !selectedElementFocus()
|
|
5066
|
-
&& selectedOverlay &&
|
|
5067
|
-
|
|
5319
|
+
&& selectedOverlay && elementSelectionEnabled(selectedOverlay)
|
|
5320
|
+
&& (!isTelopOverlay(selectedOverlay) || selectedIds.length === 1)) {
|
|
5321
|
+
const root = fragmentRoot(selectedOverlay);
|
|
5322
|
+
const first = firstSelectableElement(root, stage?.getBoundingClientRect(), selectionOptions(root));
|
|
5068
5323
|
if (first) {
|
|
5324
|
+
if (isTelopOverlay(selectedOverlay)) telopInnerId = selectedId;
|
|
5069
5325
|
focusElement(first);
|
|
5070
5326
|
event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation();
|
|
5071
5327
|
return;
|
|
@@ -5113,7 +5369,8 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
5113
5369
|
if (groupSelection) {
|
|
5114
5370
|
applyScopedSelection(enterScope(selectionTree(), selectedId)); handled(); return;
|
|
5115
5371
|
}
|
|
5116
|
-
if (selectedOverlay)
|
|
5372
|
+
if (selectedOverlay && (!isTelopOverlay(selectedOverlay)
|
|
5373
|
+
|| telopInnerId === selectedId && Boolean(selectedElementFocus()))) {
|
|
5117
5374
|
const root = fragmentRoot(selectedOverlay);
|
|
5118
5375
|
const text = root && [root, ...root.querySelectorAll('*')].find(canEditText);
|
|
5119
5376
|
if (text) { beginEdit(selectedOverlay, text); handled(); }
|
|
@@ -5601,6 +5858,9 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
5601
5858
|
releasePointerOwner,
|
|
5602
5859
|
get selectedId() { return selectedId; },
|
|
5603
5860
|
get elementFocus() { return selectedElementFocus(); },
|
|
5861
|
+
get selectedTelop() { return Boolean(selectedOverlay && selectedIds.length === 1 && isTelopOverlay(selectedOverlay)); },
|
|
5862
|
+
get telopInner() { return telopInnerId !== null && telopInnerId === selectedId; },
|
|
5863
|
+
setTelopInnerSelection,
|
|
5604
5864
|
clearElementFocus() { if (selectedElementFocus()) focusElement(null, { notify: false }); },
|
|
5605
5865
|
focusElementAtPoint,
|
|
5606
5866
|
get selectedIds() { return [...selectedIds]; },
|