akari-video 1.2.0-beta.3 → 1.2.0-beta.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "1.2.0-beta.3",
3
+ "version": "1.2.0-beta.4",
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": {
@@ -6,8 +6,10 @@ The project stores non-destructive source selections in `.akari/material-ranges.
6
6
  { "version": 1, "ranges": { "assets/example.mp4": { "in": 2.5, "out": 8 } } }
7
7
  ```
8
8
 
9
- Keys are project-relative media paths. `in` and `out` are seconds in the original media; `out` is greater than `in`. Removing a selection deletes its key. A missing or malformed file means no selections. Updates preserve unrelated top-level keys and other material entries and replace the file by writing a temporary file and renaming it.
9
+ Keys are project-relative media paths. `in` and `out` are seconds in the original media; `out` is greater than `in`. Handle changes round both values to 0.01 seconds before saving. Removing a selection deletes its key. A missing or malformed file means no selections. Updates preserve unrelated top-level keys and other material entries and replace the file by writing a temporary file and renaming it.
10
10
 
11
- The selected list strip exposes two handles. Its minimum width is the larger of one second and 24 pixels expressed in source seconds, capped by the media duration. Dragging one handle keeps the other fixed. Double-clicking the strip or pressing × clears the selection. Changes are saved after 150 ms.
11
+ The selected list strip exposes two handles. Its minimum width is the larger of one second and 24 pixels expressed in source seconds, capped by the media duration. Dragging one handle keeps the other fixed. Pressing × clears the selection and announces the change; double-clicking the strip leaves the selection intact. An unselected strip shows an orange line at the saved range. Changes are saved after 150 ms.
12
12
 
13
- Material drag payloads and `akari.timeline.addMaterialAtPlayhead` requests may carry `in` and `out`. Drag placement uses the payload; playhead placement reads the saved range. Video and audio placement cap `out` at the media duration and use the full source when no valid range is present. `akari.materials.range.get` and `.set` expose the stored range to other views; changes emit `akari.materials.range.changed` with `{ relativePath, range }` on `window`. Range setters may include `source`; the event preserves it so a view can ignore echoes of its own changes.
13
+ Material drag payloads and placement requests may carry `in` and `out`. The range applies when a person places material through panel drag and drop, the panel's Add to Timeline action, a drop on the central output preview, or placement from the material preview. Other callers of `akari.timeline.addMaterialAtPlayhead` and `akari.timeline.addMaterialAtOutputPoint` use the full source unless their request explicitly supplies a valid pair. Video and audio placement cap `out` at the media duration; if `in` reaches or exceeds the media duration, placement uses the full source. `akari.materials.range.get` and `.set` expose the stored range to other views. Changes emit `akari.materials.range.changed` on `window` with `{ relativePath, range, source }` when the setter supplies `source`; views can use it to ignore echoes of their own changes.
14
+
15
+ Ranged audio is placed as an audio-track item by both playhead placement and output-preview drop, because the legacy `audio.sfx` form cannot store `in` and `out`.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "1.2.0-beta.3",
3
+ "version": "1.2.0-beta.4",
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": [
@@ -16,7 +16,7 @@ const TASKS_VERSION = 0;
16
16
  const PATCH_FIELDS = new Set([
17
17
  'state', 'via', 'priority', 'rank', 'outcome', 'title', 'body', 'target', 'anchor',
18
18
  'ref', 'attachments', 'gate', 'undo', 'batchId', 'sentAt', 'sentTo', 'response',
19
- 'needsConfirm', 'updatedAt', 'orphaned', 'kind', 'targetDetail', 'evidence',
19
+ 'needsConfirm', 'confirmedAt', 'updatedAt', 'orphaned', 'kind', 'targetDetail', 'evidence',
20
20
  'confidence', 'question', 'risk', 'route', 'dependsOn', 'origin', 'mergedFrom'
21
21
  ]);
22
22
  const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
@@ -132,6 +132,10 @@ function applyTaskPatch(task, patch, actor) {
132
132
  for (const key of Object.keys(patch))
133
133
  if (!PATCH_FIELDS.has(key))
134
134
  throw new Error(`${key} は変更できません。`);
135
+ if (patch.needsConfirm === false && actor !== 'human')
136
+ throw new Error('案の承認は人が行ってください。');
137
+ if (task.needsConfirm === true && patch.needsConfirm === false && !patch.confirmedAt)
138
+ throw new Error('承認時刻が必要です。');
135
139
  if (isObject(task.ref) && task.ref.kind === 'annotation') {
136
140
  for (const key of ['title', 'body', 'target', 'anchor', 'via', 'ref']) {
137
141
  if (key in patch)
@@ -0,0 +1,8 @@
1
+ export declare function elementAxes(center: {x:number;y:number}, xProbe:{x:number;y:number}, yProbe:{x:number;y:number}): {x:{x:number;y:number};y:{x:number;y:number}};
2
+ export declare function elementPoint(geometry: any, horizontal:number, vertical:number): {x:number;y:number};
3
+ export declare function elementHandlePoints(geometry:any, name:string): {dragged:{x:number;y:number};anchor:{x:number;y:number};signs:number[]} | null;
4
+ export declare function elementBoxSize(width:number, height:number, name:string, delta:{x:number;y:number}, shift:boolean): {width:number;height:number};
5
+ export declare function elementLocalDelta(screen:{x:number;y:number}, axes:any): {x:number;y:number};
6
+ export declare function elementAngle(value:number, shift:boolean): number;
7
+ export declare function elementHandleLayout(width:number, height:number): {hideHorizontalEdges:boolean;hideVerticalEdges:boolean;outsideX:boolean;outsideY:boolean;cornerOffsetX:number;cornerOffsetY:number;rotateTop:number};
8
+ export declare function elementBoxCompanions(style:CSSStyleDeclaration, parentStyle:CSSStyleDeclaration | null, width:number | null, height:number | null): Record<string,string>;
@@ -0,0 +1,75 @@
1
+ // Geometry shared with the marked copy in interaction.js. Coordinates are client pixels.
2
+ // BEGIN element-geometry
3
+ function elementAxes(center, xProbe, yProbe) {
4
+ return { x: { x: xProbe.x - center.x, y: xProbe.y - center.y },
5
+ y: { x: yProbe.x - center.x, y: yProbe.y - center.y } };
6
+ }
7
+
8
+ function elementPoint(geometry, horizontal, vertical) {
9
+ return { x: geometry.center.x + horizontal * geometry.width * geometry.axes.x.x / 2
10
+ + vertical * geometry.height * geometry.axes.y.x / 2,
11
+ y: geometry.center.y + horizontal * geometry.width * geometry.axes.x.y / 2
12
+ + vertical * geometry.height * geometry.axes.y.y / 2 };
13
+ }
14
+
15
+ function elementHandlePoints(geometry, name) {
16
+ const signs = { n: [0, -1], e: [1, 0], s: [0, 1], w: [-1, 0],
17
+ nw: [-1, -1], ne: [1, -1], se: [1, 1], sw: [-1, 1] }[name];
18
+ if (!signs) return null;
19
+ return { dragged: elementPoint(geometry, ...signs),
20
+ anchor: elementPoint(geometry, -signs[0], -signs[1]), signs };
21
+ }
22
+
23
+ function elementBoxSize(width, height, name, delta, shift) {
24
+ const signs = elementHandlePoints({ center: { x: 0, y: 0 }, width, height,
25
+ axes: { x: { x: 1, y: 0 }, y: { x: 0, y: 1 } } }, name)?.signs;
26
+ if (!signs) return { width, height };
27
+ let nextWidth = signs[0] ? Math.max(4, width + signs[0] * delta.x) : width;
28
+ let nextHeight = signs[1] ? Math.max(4, height + signs[1] * delta.y) : height;
29
+ if (signs[0] && signs[1] && !shift) {
30
+ const ratio = Math.max(4 / width, 4 / height,
31
+ (width * nextWidth + height * nextHeight) / (width * width + height * height));
32
+ nextWidth = width * ratio;
33
+ nextHeight = height * ratio;
34
+ }
35
+ return { width: Math.round(nextWidth * 100) / 100,
36
+ height: Math.round(nextHeight * 100) / 100 };
37
+ }
38
+
39
+ function elementLocalDelta(screen, axes) {
40
+ const determinant = axes.x.x * axes.y.y - axes.y.x * axes.x.y;
41
+ if (Math.abs(determinant) < 1e-6) return { x: 0, y: 0 };
42
+ return { x: (screen.x * axes.y.y - screen.y * axes.y.x) / determinant,
43
+ y: (screen.y * axes.x.x - screen.x * axes.x.y) / determinant };
44
+ }
45
+
46
+ function elementAngle(value, shift) {
47
+ const angle = ((value + 180) % 360 + 360) % 360 - 180;
48
+ return Math.round((shift ? Math.round(angle / 15) * 15 : angle) * 100) / 100;
49
+ }
50
+
51
+ function elementHandleLayout(width, height) {
52
+ return { hideHorizontalEdges: width < 30, hideVerticalEdges: height < 30,
53
+ outsideX: width < 36, outsideY: height < 36,
54
+ cornerOffsetX: width < 36 ? 14 : 0, cornerOffsetY: height < 36 ? 14 : 0,
55
+ rotateTop: -32 };
56
+ }
57
+
58
+ function elementBoxCompanions(style, parentStyle, width, height) {
59
+ const result = {};
60
+ if (style.boxSizing !== 'border-box') result['box-sizing'] = 'border-box';
61
+ if (style.display === 'inline') result.display = 'inline-block';
62
+ if (['flex', 'inline-flex'].includes(parentStyle?.display) && style.flex !== '0 0 auto') result.flex = '0 0 auto';
63
+ if (width !== null) {
64
+ if (Number.parseFloat(style.minWidth) > width) result['min-width'] = '0px';
65
+ if (Number.parseFloat(style.maxWidth) < width) result['max-width'] = 'none';
66
+ }
67
+ if (height !== null) {
68
+ if (Number.parseFloat(style.minHeight) > height) result['min-height'] = '0px';
69
+ if (Number.parseFloat(style.maxHeight) < height) result['max-height'] = 'none';
70
+ }
71
+ return result;
72
+ }
73
+ // END element-geometry
74
+ export { elementAxes, elementPoint, elementHandlePoints, elementBoxSize,
75
+ elementLocalDelta, elementAngle, elementHandleLayout, elementBoxCompanions };
@@ -117,6 +117,7 @@
117
117
  .akari-interaction-handle.is-w { top: 50%; left: 0; transform: translate(-50%, -50%); cursor: ew-resize; }
118
118
  .akari-interaction-selection-frame:not([data-akari-selection-kind="leaf"]) .is-edge { display: none; }
119
119
  .akari-interaction-selection-frame.is-telop .is-edge { display: none; }
120
+ .akari-interaction-selection-frame.is-element-focus.is-telop .is-edge { display: block; }
120
121
  .akari-interaction-selection-frame.is-line .akari-interaction-handle:not(.is-line-start):not(.is-line-end):not(.akari-interaction-action) { display: none; }
121
122
  .akari-interaction-selection-frame:not(.is-line) .is-line-start,
122
123
  .akari-interaction-selection-frame:not(.is-line) .is-line-end { display: none; }
@@ -163,6 +163,79 @@ function elementLabel(root, element) {
163
163
  }
164
164
  // END element-selection
165
165
 
166
+ // BEGIN element-geometry
167
+ function elementAxes(center, xProbe, yProbe) {
168
+ return { x: { x: xProbe.x - center.x, y: xProbe.y - center.y },
169
+ y: { x: yProbe.x - center.x, y: yProbe.y - center.y } };
170
+ }
171
+
172
+ function elementPoint(geometry, horizontal, vertical) {
173
+ return { x: geometry.center.x + horizontal * geometry.width * geometry.axes.x.x / 2
174
+ + vertical * geometry.height * geometry.axes.y.x / 2,
175
+ y: geometry.center.y + horizontal * geometry.width * geometry.axes.x.y / 2
176
+ + vertical * geometry.height * geometry.axes.y.y / 2 };
177
+ }
178
+
179
+ function elementHandlePoints(geometry, name) {
180
+ const signs = { n: [0, -1], e: [1, 0], s: [0, 1], w: [-1, 0],
181
+ nw: [-1, -1], ne: [1, -1], se: [1, 1], sw: [-1, 1] }[name];
182
+ if (!signs) return null;
183
+ return { dragged: elementPoint(geometry, ...signs),
184
+ anchor: elementPoint(geometry, -signs[0], -signs[1]), signs };
185
+ }
186
+
187
+ function elementBoxSize(width, height, name, delta, shift) {
188
+ const signs = elementHandlePoints({ center: { x: 0, y: 0 }, width, height,
189
+ axes: { x: { x: 1, y: 0 }, y: { x: 0, y: 1 } } }, name)?.signs;
190
+ if (!signs) return { width, height };
191
+ let nextWidth = signs[0] ? Math.max(4, width + signs[0] * delta.x) : width;
192
+ let nextHeight = signs[1] ? Math.max(4, height + signs[1] * delta.y) : height;
193
+ if (signs[0] && signs[1] && !shift) {
194
+ const ratio = Math.max(4 / width, 4 / height,
195
+ (width * nextWidth + height * nextHeight) / (width * width + height * height));
196
+ nextWidth = width * ratio;
197
+ nextHeight = height * ratio;
198
+ }
199
+ return { width: Math.round(nextWidth * 100) / 100,
200
+ height: Math.round(nextHeight * 100) / 100 };
201
+ }
202
+
203
+ function elementLocalDelta(screen, axes) {
204
+ const determinant = axes.x.x * axes.y.y - axes.y.x * axes.x.y;
205
+ if (Math.abs(determinant) < 1e-6) return { x: 0, y: 0 };
206
+ return { x: (screen.x * axes.y.y - screen.y * axes.y.x) / determinant,
207
+ y: (screen.y * axes.x.x - screen.x * axes.x.y) / determinant };
208
+ }
209
+
210
+ function elementAngle(value, shift) {
211
+ const angle = ((value + 180) % 360 + 360) % 360 - 180;
212
+ return Math.round((shift ? Math.round(angle / 15) * 15 : angle) * 100) / 100;
213
+ }
214
+
215
+ function elementHandleLayout(width, height) {
216
+ return { hideHorizontalEdges: width < 30, hideVerticalEdges: height < 30,
217
+ outsideX: width < 36, outsideY: height < 36,
218
+ cornerOffsetX: width < 36 ? 14 : 0, cornerOffsetY: height < 36 ? 14 : 0,
219
+ rotateTop: -32 };
220
+ }
221
+
222
+ function elementBoxCompanions(style, parentStyle, width, height) {
223
+ const result = {};
224
+ if (style.boxSizing !== 'border-box') result['box-sizing'] = 'border-box';
225
+ if (style.display === 'inline') result.display = 'inline-block';
226
+ if (['flex', 'inline-flex'].includes(parentStyle?.display) && style.flex !== '0 0 auto') result.flex = '0 0 auto';
227
+ if (width !== null) {
228
+ if (Number.parseFloat(style.minWidth) > width) result['min-width'] = '0px';
229
+ if (Number.parseFloat(style.maxWidth) < width) result['max-width'] = 'none';
230
+ }
231
+ if (height !== null) {
232
+ if (Number.parseFloat(style.minHeight) > height) result['min-height'] = '0px';
233
+ if (Number.parseFloat(style.maxHeight) < height) result['max-height'] = 'none';
234
+ }
235
+ return result;
236
+ }
237
+ // END element-geometry
238
+
166
239
  const stage = document.getElementById("overlay-stage");
167
240
  const dragStartDistance = 4;
168
241
  const SNAP_DISTANCE = 6;
@@ -216,6 +289,7 @@ function elementLabel(root, element) {
216
289
  let selectedId = null;
217
290
  let selectedIds = [];
218
291
  let elementFocus = null;
292
+ let elementGeometryCache = null;
219
293
  let elementNudge = null;
220
294
  let elementNudgeTimer = null;
221
295
  let scopeId = null;
@@ -223,6 +297,7 @@ function elementLabel(root, element) {
223
297
  scopeId = floorScopeId;
224
298
  let groupSelection = false;
225
299
  let selectionFrame = null;
300
+ let elementHandlePlacementFrame = null;
226
301
  let selectionTrackingFrame = null;
227
302
  let activeDrag = null;
228
303
  let activeResize = null;
@@ -1305,7 +1380,13 @@ function elementLabel(root, element) {
1305
1380
  const transform = readTransform(selectedOverlay);
1306
1381
  const lineRect = lineFrameGeometry(selectedOverlay);
1307
1382
  const focused = focusedElement();
1308
- const rect = focused ? focused.getBoundingClientRect() : selectedOverlay.dataset.role === 'shape-line' ? lineRect
1383
+ const focusedGeometry = focused ? elementScreenGeometry(focused) : null;
1384
+ const focusedWidth = focusedGeometry && focusedGeometry.width * Math.hypot(focusedGeometry.axes.x.x, focusedGeometry.axes.x.y);
1385
+ const focusedHeight = focusedGeometry && focusedGeometry.height * Math.hypot(focusedGeometry.axes.y.x, focusedGeometry.axes.y.y);
1386
+ const focusedAngle = focusedGeometry && Math.atan2(focusedGeometry.axes.x.y, focusedGeometry.axes.x.x) * 180 / Math.PI;
1387
+ const rect = focusedGeometry ? { left: focusedGeometry.center.x - focusedWidth / 2,
1388
+ top: focusedGeometry.center.y - focusedHeight / 2, width: focusedWidth, height: focusedHeight }
1389
+ : selectedOverlay.dataset.role === 'shape-line' ? lineRect
1309
1390
  : transform.rotate ? unrotatedLeafBounds(selectedOverlay) : fragmentBounds(selectedOverlay);
1310
1391
  if (!rect) {
1311
1392
  if (selectionFrame) selectionFrame.hidden = true;
@@ -1335,15 +1416,46 @@ function elementLabel(root, element) {
1335
1416
  // 背景・ロック中は動かせない選択であることを視覚でも伝える(拡縮ハンドルを消し、枠を破線にする。
1336
1417
  // interaction.css の .is-locked)。isMovable ではなく isLockedItem を見るのは、
1337
1418
  // 選択自体は許すが移動系操作だけを塞ぐという役割分担を CSS 側にも一致させるため。
1419
+ if (!focused && elementHandlePlacementFrame === selectionFrame) {
1420
+ const replacement = createSelectionFrame();
1421
+ selectionFrame.replaceWith(replacement);
1422
+ selectionFrame = replacement;
1423
+ elementHandlePlacementFrame = null;
1424
+ }
1338
1425
  selectionFrame.classList.toggle("is-locked", isLockedItem(selectedOverlay));
1339
1426
  selectionFrame.classList.toggle('is-busy', Boolean(activeDrag || activeResize || activeRotate || activeLine));
1340
1427
  selectionFrame.classList.toggle('is-moving', Boolean(activeDrag || activeRotate));
1341
1428
  selectionFrame.classList.toggle('is-text', selectedOverlay.dataset.role === 'text');
1342
1429
  selectionFrame.classList.toggle('is-telop', isTelopOverlay(selectedOverlay));
1343
1430
  selectionFrame.classList.toggle('is-line', selectedOverlay.dataset.role === 'shape-line');
1431
+ if (focused) elementHandlePlacementFrame = selectionFrame;
1432
+ selectionFrame.classList.toggle('is-element-focus', Boolean(focused));
1344
1433
  if (window.akari.capabilities?.elementSelection === true) {
1345
1434
  for (const handle of selectionFrame.querySelectorAll('.akari-interaction-handle')) {
1346
- handle.style.display = focused ? 'none' : '';
1435
+ if (!focused) {
1436
+ continue;
1437
+ }
1438
+ const layout = elementHandleLayout(focusedWidth, focusedHeight);
1439
+ const name = [...handle.classList].find(token => /^is-(?:n|e|s|w|nw|ne|se|sw|rotate|move)$/.test(token))?.slice(3);
1440
+ handle.style.display = name === 'move' ||
1441
+ (['n', 's'].includes(name) && layout.hideHorizontalEdges) ||
1442
+ (['e', 'w'].includes(name) && layout.hideVerticalEdges) ? 'none' : '';
1443
+ if (name === 'rotate') {
1444
+ Object.assign(handle.style, { top: `${layout.rotateTop}px`, bottom: 'auto',
1445
+ left: '50%', transform: 'translate(-50%, -50%)' });
1446
+ } else if (name && name.length === 2) {
1447
+ const x = name.includes('w') ? -layout.cornerOffsetX : layout.cornerOffsetX;
1448
+ const y = name.includes('n') ? -layout.cornerOffsetY : layout.cornerOffsetY;
1449
+ if (x || y) handle.style.translate = `${x}px ${y}px`;
1450
+ else handle.style.removeProperty('translate');
1451
+ }
1452
+ if (name && name !== 'move' && name !== 'rotate') {
1453
+ const directions = ['ew-resize', 'nwse-resize', 'ns-resize', 'nesw-resize'];
1454
+ const base = name.length === 2
1455
+ ? (name === 'ne' || name === 'sw' ? 3 : 1)
1456
+ : name === 'n' || name === 's' ? 2 : 0;
1457
+ handle.style.cursor = directions[((base + Math.round(focusedAngle / 45)) % 4 + 4) % 4];
1458
+ }
1347
1459
  }
1348
1460
  }
1349
1461
  for (const endpoint of selectionFrame.querySelectorAll('.is-line-start, .is-line-end')) {
@@ -1371,9 +1483,9 @@ function elementLabel(root, element) {
1371
1483
  return;
1372
1484
  }
1373
1485
  const pivot = leafPivotClient(transform);
1374
- selectionFrame.style.transformOrigin = pivot
1486
+ selectionFrame.style.transformOrigin = focused ? 'center' : pivot
1375
1487
  ? `${pivot.x - rect.left}px ${pivot.y - rect.top}px` : 'center';
1376
- selectionFrame.style.transform = focused ? '' : transform.rotate ? `rotate(${transform.rotate}deg)` : '';
1488
+ selectionFrame.style.transform = focused ? `rotate(${focusedAngle}deg)` : transform.rotate ? `rotate(${transform.rotate}deg)` : '';
1377
1489
  }
1378
1490
 
1379
1491
  function trackSelectionFrame() {
@@ -2005,6 +2117,220 @@ function elementLabel(root, element) {
2005
2117
  const parts = String(value ?? '').match(/[-+]?(?:\d*\.)?\d+(?:e[-+]?\d+)?px/giu) ?? [];
2006
2118
  return { x: Number.parseFloat(parts[0]) || 0, y: Number.parseFloat(parts[1]) || 0 };
2007
2119
  }
2120
+ function elementScreenGeometry(element) {
2121
+ const rect = element.getBoundingClientRect();
2122
+ const center = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
2123
+ const style = getComputedStyle(element);
2124
+ const size = { width: element.offsetWidth || Number.parseFloat(style.width) || rect.width,
2125
+ height: element.offsetHeight || Number.parseFloat(style.height) || rect.height };
2126
+ const stageRect = stage?.getBoundingClientRect();
2127
+ const ancestors = [];
2128
+ for (let node = element.parentElement; node; node = node.parentElement) {
2129
+ const computed = getComputedStyle(node);
2130
+ ancestors.push([computed.translate, computed.rotate, computed.scale, computed.transform,
2131
+ computed.transformOrigin, node.getAttribute('style')]);
2132
+ }
2133
+ const signature = JSON.stringify([rect.left, rect.top, rect.width, rect.height,
2134
+ size.width, size.height, style.translate, style.rotate, style.scale, style.transform,
2135
+ style.transformOrigin, element.getAttribute('style'), selectedOverlay?.getAttribute('style'),
2136
+ stageRect && [stageRect.left, stageRect.top, stageRect.width, stageRect.height], ancestors]);
2137
+ if (elementGeometryCache?.element === element && elementGeometryCache.signature === signature) {
2138
+ return elementGeometryCache.geometry;
2139
+ }
2140
+ const original = element.style.translate;
2141
+ const base = parseElementTranslate(style.translate);
2142
+ element.style.translate = `${base.x + 1}px ${base.y}px`;
2143
+ const xRect = element.getBoundingClientRect();
2144
+ element.style.translate = `${base.x}px ${base.y + 1}px`;
2145
+ const yRect = element.getBoundingClientRect();
2146
+ element.style.translate = original;
2147
+ const axes = elementAxes(center,
2148
+ { x: xRect.left + xRect.width / 2, y: xRect.top + xRect.height / 2 },
2149
+ { x: yRect.left + yRect.width / 2, y: yRect.top + yRect.height / 2 });
2150
+ if (Math.hypot(axes.x.x, axes.x.y) < 0.01 || Math.hypot(axes.y.x, axes.y.y) < 0.01) {
2151
+ axes.x = { x: rect.width / size.width, y: 0 };
2152
+ axes.y = { x: 0, y: rect.height / size.height };
2153
+ }
2154
+ const scale = style.scale === 'none' ? [1, 1] : style.scale.split(/\s+/u).map(Number);
2155
+ const own = new DOMMatrix().rotate(Number.parseFloat(style.rotate) || 0)
2156
+ .scale(scale[0] || 1, scale[1] || scale[0] || 1)
2157
+ .multiply(new DOMMatrix(style.transform === 'none' ? undefined : style.transform));
2158
+ const baseX = axes.x, baseY = axes.y;
2159
+ axes.x = { x: baseX.x * own.a + baseY.x * own.b,
2160
+ y: baseX.y * own.a + baseY.y * own.b };
2161
+ axes.y = { x: baseX.x * own.c + baseY.x * own.d,
2162
+ y: baseX.y * own.c + baseY.y * own.d };
2163
+ const geometry = { center, axes, width: size.width, height: size.height };
2164
+ elementGeometryCache = { element, signature, geometry };
2165
+ return geometry;
2166
+ }
2167
+ function moveElementPointTo(gesture, horizontal, vertical, target) {
2168
+ const geometry = elementScreenGeometry(gesture.element);
2169
+ const point = elementPoint(geometry, horizontal, vertical);
2170
+ if (Math.hypot(target.x - point.x, target.y - point.y) <= 0.2) return;
2171
+ moveElementToCenter(gesture, geometry.center.x + target.x - point.x,
2172
+ geometry.center.y + target.y - point.y);
2173
+ }
2174
+ const elementInlineProperties = ['width', 'height', 'rotate', 'translate', 'display',
2175
+ 'box-sizing', 'min-width', 'min-height', 'max-width', 'max-height', 'flex'];
2176
+ function snapshotElementInline(element) {
2177
+ return Object.fromEntries(elementInlineProperties.map(name => [name,
2178
+ [element.style.getPropertyValue(name), element.style.getPropertyPriority(name)]]));
2179
+ }
2180
+ function restoreElementInline(element, snapshot) {
2181
+ for (const [name, [value, priority]] of Object.entries(snapshot)) {
2182
+ if (value) element.style.setProperty(name, value, priority);
2183
+ else element.style.removeProperty(name);
2184
+ }
2185
+ }
2186
+ function elementWriteStyle(gesture, dimensionNames = []) {
2187
+ const style = {};
2188
+ for (const name of dimensionNames) style[name] = gesture.element.style.getPropertyValue(name);
2189
+ for (const name of elementInlineProperties) {
2190
+ if (name === 'translate' || dimensionNames.includes(name)) continue;
2191
+ const now = gesture.element.style.getPropertyValue(name);
2192
+ if (now && now !== gesture.originalInline[name][0]) style[name] = now;
2193
+ }
2194
+ const translate = gesture.element.style.translate;
2195
+ if (translate && translate !== gesture.originalInline.translate[0]) style.translate = translate;
2196
+ return style;
2197
+ }
2198
+ function beginElementGesture(event, handleEl) {
2199
+ const element = focusedElement();
2200
+ if (!element || !elementFocus) return null;
2201
+ hideSnapGuides();
2202
+ const before = elementScreenGeometry(element);
2203
+ const originalInline = snapshotElementInline(element);
2204
+ const computed = getComputedStyle(element);
2205
+ const startStyle = Object.fromEntries(['boxSizing', 'display', 'flex', 'minWidth', 'minHeight',
2206
+ 'maxWidth', 'maxHeight'].map(name => [name, computed[name]]));
2207
+ const source = window.akari.state?.summary?.overlays?.find(overlay => overlay.id === selectedId);
2208
+ const declared = source?.elements?.[elementFocus.ref]?.style ?? {};
2209
+ const translate = parseElementTranslate(declared.translate ?? computed.translate);
2210
+ const gesture = { element, elementRef: elementFocus.ref, elementTag: elementFocus.tag,
2211
+ container: selectedOverlay, overlayId: selectedId, handleEl, pointerId: event.pointerId,
2212
+ originalInline, startStyle, parentStyle: element.parentElement
2213
+ ? { display: getComputedStyle(element.parentElement).display } : null,
2214
+ startGeometry: before, startClientX: event.clientX, startClientY: event.clientY,
2215
+ startWidth: Number.parseFloat(declared.width) || before.width,
2216
+ startHeight: Number.parseFloat(declared.height) || before.height,
2217
+ x: translate.x, y: translate.y, moved: false, writeContext: captureWriteContext() };
2218
+ if (startStyle.display === 'inline') {
2219
+ element.style.display = 'inline-block';
2220
+ const next = element.getBoundingClientRect();
2221
+ if (Math.hypot(next.left + next.width / 2 - before.center.x,
2222
+ next.top + next.height / 2 - before.center.y) > 0.2) {
2223
+ moveElementToCenter(gesture, before.center.x, before.center.y);
2224
+ }
2225
+ }
2226
+ try { handleEl.setPointerCapture?.(event.pointerId); } catch { /* synthetic pointer */ }
2227
+ if (event.cancelable) event.preventDefault();
2228
+ return gesture;
2229
+ }
2230
+ function showElementHandleHint(event, label) {
2231
+ handleHint?.remove();
2232
+ handleHint = document.createElement('div');
2233
+ handleHint.className = 'akari-interaction-hint';
2234
+ handleHint.setAttribute('data-akari-interaction', 'handle-hint');
2235
+ handleHint.textContent = label;
2236
+ handleHint.style.left = `${event.clientX + 12}px`;
2237
+ handleHint.style.top = `${event.clientY + 12}px`;
2238
+ document.body.appendChild(handleHint);
2239
+ }
2240
+ function beginElementResize(event, handleEl) {
2241
+ if (activeEdit) void commitEdit();
2242
+ const gesture = beginElementGesture(event, handleEl);
2243
+ if (!gesture) return;
2244
+ const name = [...handleEl.classList].find(token => /^is-(?:n|e|s|w|nw|ne|se|sw)$/.test(token))?.slice(3);
2245
+ if (!name) { restoreElementInline(gesture.element, gesture.originalInline); return; }
2246
+ gesture.name = name;
2247
+ gesture.anchor = elementHandlePoints(gesture.startGeometry, name).anchor;
2248
+ activeResize = gesture;
2249
+ showElementHandleHint(event, name.length === 2 ? '大きさ' : ['n', 's'].includes(name) ? '高さ' : '幅');
2250
+ refreshSelectionFrame();
2251
+ }
2252
+ function updateElementResize(event) {
2253
+ const resize = activeResize;
2254
+ const delta = elementLocalDelta({ x: event.clientX - resize.startClientX,
2255
+ y: event.clientY - resize.startClientY }, resize.startGeometry.axes);
2256
+ const size = elementBoxSize(resize.startWidth, resize.startHeight, resize.name, delta, event.shiftKey);
2257
+ const dimensionNames = resize.name.length === 2 ? ['width', 'height']
2258
+ : ['n', 's'].includes(resize.name) ? ['height'] : ['width'];
2259
+ const companions = elementBoxCompanions(resize.startStyle, resize.parentStyle,
2260
+ dimensionNames.includes('width') ? size.width : null,
2261
+ dimensionNames.includes('height') ? size.height : null);
2262
+ for (const name of elementInlineProperties) {
2263
+ if (['translate', 'rotate', 'width', 'height'].includes(name)) continue;
2264
+ const [original, priority] = resize.originalInline[name];
2265
+ if (companions[name]) resize.element.style.setProperty(name, companions[name]);
2266
+ else if (original) resize.element.style.setProperty(name, original, priority);
2267
+ else resize.element.style.removeProperty(name);
2268
+ }
2269
+ if (dimensionNames.includes('width')) resize.element.style.width = `${size.width}px`;
2270
+ if (dimensionNames.includes('height')) resize.element.style.height = `${size.height}px`;
2271
+ const signs = elementHandlePoints(resize.startGeometry, resize.name).signs;
2272
+ moveElementPointTo(resize, -signs[0], -signs[1], resize.anchor);
2273
+ resize.moved = Math.abs(size.width - resize.startWidth) > 0.01
2274
+ || Math.abs(size.height - resize.startHeight) > 0.01;
2275
+ handleHint.style.left = `${event.clientX + 12}px`;
2276
+ handleHint.style.top = `${event.clientY + 12}px`;
2277
+ refreshSelectionFrame();
2278
+ if (event.cancelable) event.preventDefault();
2279
+ }
2280
+ function finishElementGesture(gesture, dimensions = []) {
2281
+ handleHint?.remove(); handleHint = null;
2282
+ releaseResizePointer(gesture);
2283
+ if (!gesture.moved) {
2284
+ restoreElementInline(gesture.element, gesture.originalInline);
2285
+ refreshSelectionFrame();
2286
+ return null;
2287
+ }
2288
+ const style = elementWriteStyle(gesture, dimensions);
2289
+ const record = enqueueWrite(gesture.writeContext, gesture.overlayId,
2290
+ { element: { ref: gesture.elementRef, tag: gesture.elementTag, style } }, 'element');
2291
+ record.promise.catch(() => {
2292
+ restoreElementInline(gesture.element, gesture.originalInline);
2293
+ refreshSelectionFrame();
2294
+ });
2295
+ refreshSelectionFrame();
2296
+ return record;
2297
+ }
2298
+ function cancelElementGesture(gesture) {
2299
+ handleHint?.remove(); handleHint = null;
2300
+ releaseResizePointer(gesture);
2301
+ restoreElementInline(gesture.element, gesture.originalInline);
2302
+ refreshSelectionFrame();
2303
+ }
2304
+ function beginElementRotate(event, handleEl) {
2305
+ if (activeEdit) void commitEdit();
2306
+ const gesture = beginElementGesture(event, handleEl);
2307
+ if (!gesture) return;
2308
+ const geometry = elementScreenGeometry(gesture.element);
2309
+ const source = window.akari.state?.summary?.overlays?.find(overlay => overlay.id === selectedId);
2310
+ const declared = source?.elements?.[elementFocus.ref]?.style?.rotate;
2311
+ gesture.startRotate = Number.parseFloat(declared ?? getComputedStyle(gesture.element).rotate) || 0;
2312
+ gesture.center = geometry.center;
2313
+ gesture.startAngle = Math.atan2(event.clientY - geometry.center.y, event.clientX - geometry.center.x);
2314
+ activeRotate = gesture;
2315
+ showElementHandleHint(event, '回転');
2316
+ }
2317
+ function updateElementRotate(event) {
2318
+ const rotation = activeRotate;
2319
+ const currentAngle = Math.atan2(event.clientY - rotation.center.y, event.clientX - rotation.center.x);
2320
+ const degrees = rotation.startRotate + (currentAngle - rotation.startAngle) * 180 / Math.PI;
2321
+ const angle = elementAngle(degrees, event.shiftKey);
2322
+ rotation.element.style.rotate = `${angle}deg`;
2323
+ const rect = rotation.element.getBoundingClientRect();
2324
+ if (Math.hypot(rect.left + rect.width / 2 - rotation.center.x,
2325
+ rect.top + rect.height / 2 - rotation.center.y) > 0.2) {
2326
+ moveElementToCenter(rotation, rotation.center.x, rotation.center.y);
2327
+ }
2328
+ rotation.moved = Math.abs(angle - rotation.startRotate) > 0.01;
2329
+ handleHint.style.left = `${event.clientX + 12}px`;
2330
+ handleHint.style.top = `${event.clientY + 12}px`;
2331
+ refreshSelectionFrame();
2332
+ if (event.cancelable) event.preventDefault();
2333
+ }
2008
2334
  function moveElementToCenter(gesture, targetX, targetY) {
2009
2335
  const element = gesture.element;
2010
2336
  const center = () => {
@@ -2038,12 +2364,16 @@ function elementLabel(root, element) {
2038
2364
  if (!gesture) return;
2039
2365
  if (Math.abs(gesture.x - gesture.startX) < 0.5 && Math.abs(gesture.y - gesture.startY) < 0.5) {
2040
2366
  gesture.element.style.translate = gesture.originalInline;
2367
+ if (gesture.originalDisplay !== undefined) gesture.element.style.display = gesture.originalDisplay;
2041
2368
  return;
2042
2369
  }
2370
+ const style = { translate: `${gesture.x}px ${gesture.y}px` };
2371
+ if (gesture.originalDisplay !== undefined) style.display = 'inline-block';
2043
2372
  enqueueWrite(gesture.writeContext, gesture.overlayId,
2044
2373
  { element: { ref: gesture.ref, tag: gesture.tag,
2045
- style: { translate: `${gesture.x}px ${gesture.y}px` } } }, 'element').promise.catch(() => {
2374
+ style } }, 'element').promise.catch(() => {
2046
2375
  gesture.element.style.translate = gesture.originalInline;
2376
+ if (gesture.originalDisplay !== undefined) gesture.element.style.display = gesture.originalDisplay;
2047
2377
  refreshSelectionFrame();
2048
2378
  });
2049
2379
  }
@@ -2088,6 +2418,13 @@ function elementLabel(root, element) {
2088
2418
  elementNudge = { element, ref: elementFocus.ref, tag: elementFocus.tag,
2089
2419
  originalInline: element.style.translate, startX: current.x, startY: current.y,
2090
2420
  x: current.x, y: current.y, overlayId: selectedId, writeContext: captureWriteContext() };
2421
+ if (getComputedStyle(element).display === 'inline') {
2422
+ elementNudge.originalDisplay = element.style.display;
2423
+ const before = element.getBoundingClientRect();
2424
+ element.style.display = 'inline-block';
2425
+ moveElementToCenter(elementNudge, before.left + before.width / 2,
2426
+ before.top + before.height / 2);
2427
+ }
2091
2428
  }
2092
2429
  const step = event.shiftKey ? 10 : 1;
2093
2430
  const rect = elementNudge.element.getBoundingClientRect();
@@ -2223,6 +2560,7 @@ function elementLabel(root, element) {
2223
2560
  activeDrag = null;
2224
2561
  if (drag.element) {
2225
2562
  drag.element.style.translate = drag.originalInline;
2563
+ if (drag.originalDisplay !== undefined) drag.element.style.display = drag.originalDisplay;
2226
2564
  releasePointer(drag);
2227
2565
  refreshSelectionFrame();
2228
2566
  return;
@@ -2256,14 +2594,18 @@ function elementLabel(root, element) {
2256
2594
  const outputDy = (rect.top + rect.height / 2 - drag.startCenterY) / displayScale;
2257
2595
  if (!drag.moved || (Math.abs(outputDx) < 0.5 && Math.abs(outputDy) < 0.5)) {
2258
2596
  drag.element.style.translate = drag.originalInline;
2597
+ if (drag.originalDisplay !== undefined) drag.element.style.display = drag.originalDisplay;
2259
2598
  refreshSelectionFrame();
2260
2599
  return null;
2261
2600
  }
2601
+ const style = { translate: `${drag.x}px ${drag.y}px` };
2602
+ if (drag.originalDisplay !== undefined) style.display = 'inline-block';
2262
2603
  const record = enqueueWrite(drag.writeContext, drag.overlayId,
2263
2604
  { element: { ref: drag.elementRef, tag: drag.elementTag,
2264
- style: { translate: `${drag.x}px ${drag.y}px` } } }, 'element');
2605
+ style } }, 'element');
2265
2606
  record.promise.catch(() => {
2266
2607
  drag.element.style.translate = drag.originalInline;
2608
+ if (drag.originalDisplay !== undefined) drag.element.style.display = drag.originalDisplay;
2267
2609
  refreshSelectionFrame();
2268
2610
  });
2269
2611
  return record;
@@ -2918,6 +3260,10 @@ function elementLabel(root, element) {
2918
3260
  }
2919
3261
 
2920
3262
  function beginResize(event, container, handleEl) {
3263
+ if (selectedElementFocus() && focusedElement()) {
3264
+ beginElementResize(event, handleEl);
3265
+ return;
3266
+ }
2921
3267
  if (isTelopOverlay(container) && handleEdge(handleEl)) return;
2922
3268
  if (activeEdit) void commitEdit();
2923
3269
 
@@ -3024,6 +3370,10 @@ function elementLabel(root, element) {
3024
3370
  }
3025
3371
 
3026
3372
  function beginRotate(event, handleEl) {
3373
+ if (selectedElementFocus() && focusedElement()) {
3374
+ beginElementRotate(event, handleEl);
3375
+ return;
3376
+ }
3027
3377
  if (activeEdit) void commitEdit();
3028
3378
  const group = groupSelection;
3029
3379
  const members = group ? selectionMembers().map(element => ({ element, transform: readTransform(element) })) : [];
@@ -3052,6 +3402,7 @@ function elementLabel(root, element) {
3052
3402
  function updateRotate(event) {
3053
3403
  const rotation = activeRotate;
3054
3404
  if (!rotation || event.pointerId !== rotation.pointerId) return;
3405
+ if (rotation.element) { updateElementRotate(event); return; }
3055
3406
  const pointer = stageLocalPoint(event.clientX, event.clientY);
3056
3407
  if (!pointer) return;
3057
3408
  let angle = (Math.atan2(pointer.y - rotation.center.y, pointer.x - rotation.center.x)
@@ -3101,6 +3452,11 @@ function elementLabel(root, element) {
3101
3452
  function cancelRotate() {
3102
3453
  if (!activeRotate) return;
3103
3454
  const rotation = activeRotate;
3455
+ if (rotation.element) {
3456
+ activeRotate = null;
3457
+ cancelElementGesture(rotation);
3458
+ return;
3459
+ }
3104
3460
  reportLiveValues(rotation.overlayId, undefined, true);
3105
3461
  activeRotate = null;
3106
3462
  rotationBadge?.remove(); rotationBadge = null;
@@ -3119,6 +3475,7 @@ function elementLabel(root, element) {
3119
3475
  if (!activeRotate) return null;
3120
3476
  const rotation = activeRotate;
3121
3477
  activeRotate = null;
3478
+ if (rotation.element) return finishElementGesture(rotation);
3122
3479
  rotationBadge?.remove(); rotationBadge = null;
3123
3480
  document.body.style.cursor = '';
3124
3481
  releaseResizePointer(rotation);
@@ -3332,6 +3689,7 @@ function elementLabel(root, element) {
3332
3689
  function updateResize(event) {
3333
3690
  const resize = activeResize;
3334
3691
  if (!resize || event.pointerId !== resize.pointerId) return;
3692
+ if (resize.element) { updateElementResize(event); return; }
3335
3693
 
3336
3694
  const pointer = stageLocalPoint(event.clientX, event.clientY);
3337
3695
  if (!pointer) return;
@@ -3430,6 +3788,11 @@ function elementLabel(root, element) {
3430
3788
  if (!activeResize) return;
3431
3789
 
3432
3790
  const resize = activeResize;
3791
+ if (resize.element) {
3792
+ activeResize = null;
3793
+ cancelElementGesture(resize);
3794
+ return;
3795
+ }
3433
3796
  reportLiveValues(resize.overlayId, undefined, true);
3434
3797
  activeResize = null;
3435
3798
  handleHint?.remove(); handleHint = null;
@@ -3456,6 +3819,8 @@ function elementLabel(root, element) {
3456
3819
 
3457
3820
  const resize = activeResize;
3458
3821
  activeResize = null;
3822
+ if (resize.element) return finishElementGesture(resize,
3823
+ resize.name.length === 2 ? ['width', 'height'] : ['n', 's'].includes(resize.name) ? ['height'] : ['width']);
3459
3824
  handleHint?.remove(); handleHint = null;
3460
3825
  releaseResizePointer(resize);
3461
3826
  hideSnapGuides();
@@ -3657,6 +4022,11 @@ function elementLabel(root, element) {
3657
4022
  startCenterY: rect.top + rect.height / 2, startX: initial.x, startY: initial.y,
3658
4023
  x: initial.x, y: initial.y,
3659
4024
  originalInline, moved: false, writeContext: captureWriteContext() };
4025
+ if (getComputedStyle(element).display === 'inline') {
4026
+ activeDrag.originalDisplay = element.style.display;
4027
+ element.style.display = 'inline-block';
4028
+ moveElementToCenter(activeDrag, activeDrag.startCenterX, activeDrag.startCenterY);
4029
+ }
3660
4030
  try { container.setPointerCapture?.(event.pointerId); } catch { /* synthetic pointer */ }
3661
4031
  return;
3662
4032
  }
@@ -4437,6 +4807,8 @@ function elementLabel(root, element) {
4437
4807
  if (selectedElementFocus() && !activeEdit) {
4438
4808
  const stop = () => { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); };
4439
4809
  if (event.key === 'Escape' && activeDrag) { cancelDrag(); stop(); return; }
4810
+ if (event.key === 'Escape' && activeResize) { cancelResize(); stop(); return; }
4811
+ if (event.key === 'Escape' && activeRotate) { cancelRotate(); stop(); return; }
4440
4812
  if (event.key === 'Escape' || event.key === 'Enter' && event.shiftKey) {
4441
4813
  const root = fragmentRoot(selectedOverlay);
4442
4814
  const parent = nearestSelectableElement(root, focusedElement()?.parentElement,