akari-video 1.3.0-beta.1 → 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 +253 -27
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.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.0-beta.
|
|
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) {
|
|
@@ -576,6 +709,21 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
576
709
|
return null;
|
|
577
710
|
}
|
|
578
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
|
+
|
|
579
727
|
// 断片ルートは「inset:0 全画面ラッパー + flex/絶対配置」パターン(overlay-authoring
|
|
580
728
|
// 規約・text-behind-person.md 等)を許容するため、ルート自身の矩形がコンテナ(断片の
|
|
581
729
|
// 親 = [data-overlay-id] 要素。overlay-runtime.js の mount() が inset:0 で全画面付与)
|
|
@@ -594,7 +742,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
594
742
|
);
|
|
595
743
|
}
|
|
596
744
|
|
|
597
|
-
function
|
|
745
|
+
function legacyFragmentBounds(container) {
|
|
598
746
|
const root = fragmentRoot(container);
|
|
599
747
|
if (!root) return null;
|
|
600
748
|
|
|
@@ -684,6 +832,13 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
684
832
|
return { left, top, right, bottom, width: right - left, height: bottom - top };
|
|
685
833
|
}
|
|
686
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
|
+
|
|
687
842
|
function svgReferenceRect(element, value) {
|
|
688
843
|
const id = String(value ?? '').match(/url\(["']?(?:[^#)]*#)([^)"']+)["']?\)/)?.[1];
|
|
689
844
|
const svg = element.namespaceURI === 'http://www.w3.org/2000/svg'
|
|
@@ -1507,7 +1662,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1507
1662
|
const transform = readTransform(selectedOverlay);
|
|
1508
1663
|
const lineRect = lineFrameGeometry(selectedOverlay);
|
|
1509
1664
|
const focused = focusedElement();
|
|
1510
|
-
const focusedGeometry = focused ?
|
|
1665
|
+
const focusedGeometry = focused ? focusedSelectionGeometry(focused) : null;
|
|
1511
1666
|
const focusedWidth = focusedGeometry && focusedGeometry.width * Math.hypot(focusedGeometry.axes.x.x, focusedGeometry.axes.x.y);
|
|
1512
1667
|
const focusedHeight = focusedGeometry && focusedGeometry.height * Math.hypot(focusedGeometry.axes.y.x, focusedGeometry.axes.y.y);
|
|
1513
1668
|
const focusedAngle = focusedGeometry && Math.atan2(focusedGeometry.axes.x.y, focusedGeometry.axes.x.x) * 180 / Math.PI;
|
|
@@ -1564,7 +1719,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1564
1719
|
}
|
|
1565
1720
|
const layout = elementHandleLayout(focusedWidth, focusedHeight);
|
|
1566
1721
|
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' ||
|
|
1722
|
+
handle.style.display = name === 'move' || (isSelectionContainer(focused) && name !== 'rotate') ||
|
|
1568
1723
|
(['n', 's'].includes(name) && layout.hideHorizontalEdges) ||
|
|
1569
1724
|
(['e', 'w'].includes(name) && layout.hideVerticalEdges) ? 'none' : '';
|
|
1570
1725
|
if (name === 'rotate') {
|
|
@@ -1575,6 +1730,11 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1575
1730
|
const y = name.includes('n') ? -layout.cornerOffsetY : layout.cornerOffsetY;
|
|
1576
1731
|
if (x || y) handle.style.translate = `${x}px ${y}px`;
|
|
1577
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');
|
|
1578
1738
|
}
|
|
1579
1739
|
if (name && name !== 'move' && name !== 'rotate') {
|
|
1580
1740
|
const directions = ['ew-resize', 'nwse-resize', 'ns-resize', 'nesw-resize'];
|
|
@@ -1739,7 +1899,8 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1739
1899
|
}
|
|
1740
1900
|
function focusedElement() {
|
|
1741
1901
|
if (!selectedElementFocus() || !selectedOverlay) return null;
|
|
1742
|
-
|
|
1902
|
+
const root = fragmentRoot(selectedOverlay);
|
|
1903
|
+
return elementByAddress(root, elementFocus.ref);
|
|
1743
1904
|
}
|
|
1744
1905
|
function selectedElementFocus() {
|
|
1745
1906
|
return elementFocus?.overlayId === selectedId
|
|
@@ -1748,10 +1909,23 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1748
1909
|
function reconcileElementFocus() {
|
|
1749
1910
|
if (!selectedElementFocus()) return;
|
|
1750
1911
|
const root = fragmentRoot(selectedOverlay);
|
|
1751
|
-
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
|
+
}
|
|
1752
1925
|
}
|
|
1753
1926
|
function focusElement(element, { notify = true } = {}) {
|
|
1754
1927
|
const root = fragmentRoot(selectedOverlay);
|
|
1928
|
+
if (element && !selectableElement(root, element, stage?.getBoundingClientRect(), selectionOptions(root))) element = null;
|
|
1755
1929
|
const ref = element && elementAddress(root, element);
|
|
1756
1930
|
const next = ref ? { overlayId: selectedId, ref, tag: element.tagName.toLowerCase(),
|
|
1757
1931
|
label: elementLabel(root, element) } : null;
|
|
@@ -1770,7 +1944,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1770
1944
|
const root = fragmentRoot(container);
|
|
1771
1945
|
const hit = event.target instanceof Element && root?.contains(event.target) ? event.target
|
|
1772
1946
|
: document.elementsFromPoint(event.clientX, event.clientY).find(element => root?.contains(element)) ?? null;
|
|
1773
|
-
focusElement(nearestSelectableElement(root, hit, stage?.getBoundingClientRect()));
|
|
1947
|
+
focusElement(nearestSelectableElement(root, hit, stage?.getBoundingClientRect(), selectionOptions(root)));
|
|
1774
1948
|
}
|
|
1775
1949
|
function focusElementAtPoint(overlayId, clientX, clientY, attempt = 0) {
|
|
1776
1950
|
if (window.akari.capabilities?.elementSelection !== true || !Number.isFinite(clientX)
|
|
@@ -1789,7 +1963,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1789
1963
|
return false;
|
|
1790
1964
|
}
|
|
1791
1965
|
const hit = document.elementsFromPoint(clientX, clientY).find(element => root.contains(element)) ?? null;
|
|
1792
|
-
focusElement(nearestSelectableElement(root, hit, stage?.getBoundingClientRect()));
|
|
1966
|
+
focusElement(nearestSelectableElement(root, hit, stage?.getBoundingClientRect(), selectionOptions(root)));
|
|
1793
1967
|
return true;
|
|
1794
1968
|
}
|
|
1795
1969
|
|
|
@@ -1873,7 +2047,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1873
2047
|
const element = target.element?.isConnected ? target.element
|
|
1874
2048
|
: elementByAddress(fragmentRoot(containerById(target.overlayId)), target.ref);
|
|
1875
2049
|
if (!element?.isConnected) return null;
|
|
1876
|
-
const geometry =
|
|
2050
|
+
const geometry = focusedSelectionGeometry(element);
|
|
1877
2051
|
const width = geometry.width * Math.hypot(geometry.axes.x.x, geometry.axes.x.y);
|
|
1878
2052
|
const height = geometry.height * Math.hypot(geometry.axes.y.x, geometry.axes.y.y);
|
|
1879
2053
|
return { rect: { left: geometry.center.x - width / 2, top: geometry.center.y - height / 2,
|
|
@@ -1900,7 +2074,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
1900
2074
|
const root = fragmentRoot(selectedOverlay);
|
|
1901
2075
|
const path = [];
|
|
1902
2076
|
for (let node = focusedElement(); node && node !== root; node = node.parentElement) {
|
|
1903
|
-
if (selectableElement(root, node, stage?.getBoundingClientRect())) path.unshift(node);
|
|
2077
|
+
if (selectableElement(root, node, stage?.getBoundingClientRect(), selectionOptions(root))) path.unshift(node);
|
|
1904
2078
|
}
|
|
1905
2079
|
for (const node of path) append(elementLabel(root, node), () => focusElement(node),
|
|
1906
2080
|
{ kind: 'element', element: node, overlayId: selectedId, ref: elementAddress(root, node) });
|
|
@@ -2391,6 +2565,48 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
2391
2565
|
elementGeometryCache = { element, signature, geometry };
|
|
2392
2566
|
return geometry;
|
|
2393
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
|
+
}
|
|
2394
2610
|
function moveElementPointTo(gesture, horizontal, vertical, target) {
|
|
2395
2611
|
const geometry = elementScreenGeometry(gesture.element);
|
|
2396
2612
|
const point = elementPoint(geometry, horizontal, vertical);
|
|
@@ -2756,8 +2972,8 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
2756
2972
|
&& (!scoped || scoped.selectId === container.dataset.overlayId)) {
|
|
2757
2973
|
const root = fragmentRoot(container);
|
|
2758
2974
|
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
|
|
2975
|
+
const element = nearestSelectableElement(root, hit, stage?.getBoundingClientRect(), selectionOptions(root));
|
|
2976
|
+
const rect = element ? focusedSelectionRect(element) : fragmentBounds(container);
|
|
2761
2977
|
if (!rect || selectedOverlay === container
|
|
2762
2978
|
&& selectedElementFocus()?.ref === elementAddress(root, element)) { hideHover(); return; }
|
|
2763
2979
|
showHoverFrame(rect, null, container.dataset.overlayId, false, false);
|
|
@@ -4170,7 +4386,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
4170
4386
|
if (window.akari.capabilities?.elementSelection === true && event.isTrusted && !event.shiftKey) {
|
|
4171
4387
|
const root = fragmentRoot(hit);
|
|
4172
4388
|
const target = event.target instanceof Element && root?.contains(event.target)
|
|
4173
|
-
? nearestSelectableElement(root, event.target, stage?.getBoundingClientRect()) : null;
|
|
4389
|
+
? nearestSelectableElement(root, event.target, stage?.getBoundingClientRect(), selectionOptions(root)) : null;
|
|
4174
4390
|
clickOrigin.hitId = hit.dataset.overlayId;
|
|
4175
4391
|
clickOrigin.elementRef = target ? elementAddress(root, target) : null;
|
|
4176
4392
|
}
|
|
@@ -4185,7 +4401,10 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
4185
4401
|
if (!selectedOverlay || (window.akari.capabilities?.elementSelection === true
|
|
4186
4402
|
? selectedId !== hit.dataset.overlayId : selectedOverlay !== hit)) return;
|
|
4187
4403
|
if (!event.shiftKey) {
|
|
4188
|
-
|
|
4404
|
+
const focused = focusedElement();
|
|
4405
|
+
if (focused && isSelectionContainer(focused) && focused.contains(event.target)) {
|
|
4406
|
+
clickOrigin.preservedContainer = true;
|
|
4407
|
+
} else if (clickOrigin?.hitId === selectedId) {
|
|
4189
4408
|
focusElement(elementByAddress(fragmentRoot(selectedOverlay), clickOrigin.elementRef));
|
|
4190
4409
|
} else focusElementAt(selectedOverlay, event);
|
|
4191
4410
|
} else if (selectedElementFocus()) focusElement(null);
|
|
@@ -4221,7 +4440,12 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
4221
4440
|
}
|
|
4222
4441
|
|
|
4223
4442
|
selectOverlay(container);
|
|
4224
|
-
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
|
+
}
|
|
4225
4449
|
else if (selectedElementFocus()) focusElement(null);
|
|
4226
4450
|
if (window.akari.capabilities?.elementSelection === true && clickOrigin) {
|
|
4227
4451
|
clickOrigin.hitId = selectedId;
|
|
@@ -4984,7 +5208,8 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
4984
5208
|
else selectOverlay(hit);
|
|
4985
5209
|
if (window.akari.capabilities?.elementSelection === true
|
|
4986
5210
|
&& event.isTrusted && !event.shiftKey && selectedOverlay) {
|
|
4987
|
-
if (
|
|
5211
|
+
if (clickOrigin?.preservedContainer) focusElementAt(selectedOverlay, event);
|
|
5212
|
+
else if (!nextId && clickOrigin?.hitId === selectedId) {
|
|
4988
5213
|
focusElement(elementByAddress(fragmentRoot(selectedOverlay), clickOrigin.elementRef));
|
|
4989
5214
|
} else focusElementAt(selectedOverlay, event);
|
|
4990
5215
|
}
|
|
@@ -5052,7 +5277,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
5052
5277
|
if (event.key === 'Escape' || event.key === 'Enter' && event.shiftKey) {
|
|
5053
5278
|
const root = fragmentRoot(selectedOverlay);
|
|
5054
5279
|
const parent = nearestSelectableElement(root, focusedElement()?.parentElement,
|
|
5055
|
-
stage?.getBoundingClientRect());
|
|
5280
|
+
stage?.getBoundingClientRect(), selectionOptions(root));
|
|
5056
5281
|
focusElement(parent); stop(); return;
|
|
5057
5282
|
}
|
|
5058
5283
|
if ((event.key === 'Delete' || event.key === 'Backspace'
|
|
@@ -5064,7 +5289,8 @@ function elementBoxCompanions(style, parentStyle, width, height) {
|
|
|
5064
5289
|
}
|
|
5065
5290
|
if (event.key === 'Enter' && !event.shiftKey && !activeEdit && !selectedElementFocus()
|
|
5066
5291
|
&& selectedOverlay && canFocusElement(selectedOverlay)) {
|
|
5067
|
-
const
|
|
5292
|
+
const root = fragmentRoot(selectedOverlay);
|
|
5293
|
+
const first = firstSelectableElement(root, stage?.getBoundingClientRect(), selectionOptions(root));
|
|
5068
5294
|
if (first) {
|
|
5069
5295
|
focusElement(first);
|
|
5070
5296
|
event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation();
|