akari-video 0.1.86 → 0.1.88
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/asset-resolver/test/service-urls.test.mjs +1 -1
- package/vendor/packages/edit-store/lib/edit-v2-item-write.js +9 -15
- package/vendor/packages/edit-store/lib/index.d.ts +13 -0
- package/vendor/packages/edit-store/lib/index.js +60 -1
- package/vendor/packages/edit-store/lib/motion-position-write.js +2 -14
- package/vendor/packages/edit-store/lib/transform-keyframe-edit.d.ts +17 -2
- package/vendor/packages/edit-store/lib/transform-keyframe-edit.js +208 -68
- package/vendor/packages/edit-store/lib/write-gate.d.ts +7 -0
- package/vendor/packages/edit-store/lib/write-gate.js +40 -1
- package/vendor/packages/overlay-runtime/src/interaction.js +43 -1
- package/vendor/packages/overlay-runtime/src/overlay-runtime.js +85 -5
- package/vendor/packages/project-scaffold/src/index.mjs +2 -2
- package/vendor/packages/project-scaffold/test/hidden-adapters.test.mjs +16 -0
- package/vendor/packages/project-scaffold/test/kit-skill-adapter.test.mjs +2 -2
- package/vendor/packages/project-scaffold/test/skill-adapter-link.test.mjs +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.88",
|
|
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": "0.1.
|
|
3
|
+
"version": "0.1.88",
|
|
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": [
|
|
@@ -20,7 +20,7 @@ test('tracked sources and documentation cannot reintroduce the retired domain',
|
|
|
20
20
|
'packages/akari-launcher/src/service-urls.cjs',
|
|
21
21
|
'packages/asset-resolver/test/service-urls.test.mjs',
|
|
22
22
|
]);
|
|
23
|
-
const files = execFileSync('git', ['ls-files', '-z'], { cwd: repo, encoding: 'utf8' }).split('\0').filter(Boolean);
|
|
23
|
+
const files = execFileSync('git', ['ls-files', '-z'], { cwd: repo, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }).split('\0').filter(Boolean);
|
|
24
24
|
const targets = files.filter(file => {
|
|
25
25
|
// Evidence and changelogs record historical observations, not current defaults.
|
|
26
26
|
if (/(^|\/)evidence\//.test(file) || /(^|\/)CHANGELOG[^/]*$/i.test(file)) return false;
|
|
@@ -7,7 +7,6 @@ const transform_1 = require("./transform");
|
|
|
7
7
|
const transform_keyframe_edit_1 = require("./transform-keyframe-edit");
|
|
8
8
|
const item_motion_js_1 = require("../../overlay-runtime/src/item-motion.js");
|
|
9
9
|
const motion_keyframe_replace_1 = require("./motion-keyframe-replace");
|
|
10
|
-
const motion_position_write_1 = require("./motion-position-write");
|
|
11
10
|
const isRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
12
11
|
const recordOf = (value) => isRecord(value) ? value : {};
|
|
13
12
|
const stringifyEdit = (value) => `${JSON.stringify(value, undefined, 2)}\n`;
|
|
@@ -130,19 +129,10 @@ function resolveV2Write(parsed, command) {
|
|
|
130
129
|
const seconds = command.playheadSeconds;
|
|
131
130
|
const positionOnly = Object.keys(patch).length > 0
|
|
132
131
|
&& Object.keys(patch).every(key => key === 'x' || key === 'y');
|
|
133
|
-
if (positionOnly) {
|
|
132
|
+
if (positionOnly || Number.isFinite(seconds)) {
|
|
134
133
|
const start = [...target.ancestors, item].reduce((sum, entry) => sum + entry.at, 0);
|
|
135
134
|
const frame = Number.isFinite(seconds)
|
|
136
135
|
? Math.max(0, Math.min(item.duration, Math.round(seconds * edit.output.fps) - start)) : 0;
|
|
137
|
-
const updated = (0, motion_position_write_1.writeItemPositionAt)(item, frame, patch);
|
|
138
|
-
item.transform = updated.transform;
|
|
139
|
-
item.keyframes = updated.keyframes;
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
if (Number.isFinite(seconds) && Array.isArray(item.keyframes)
|
|
143
|
-
&& item.keyframes.some(point => point.transform)) {
|
|
144
|
-
const start = [...target.ancestors, item].reduce((sum, entry) => sum + entry.at, 0);
|
|
145
|
-
const frame = Math.round(seconds * edit.output.fps) - start;
|
|
146
136
|
const updated = (0, transform_keyframe_edit_1.writeItemTransformAt)(item, frame, patch);
|
|
147
137
|
item.transform = updated.transform;
|
|
148
138
|
item.keyframes = updated.keyframes;
|
|
@@ -219,7 +209,8 @@ function resolveV2Write(parsed, command) {
|
|
|
219
209
|
throw new Error(`グループアイテムには HTML 本文・vars・HTML params を書き戻せません: ${itemId}`);
|
|
220
210
|
}
|
|
221
211
|
if (command.patch.xyKeyframes)
|
|
222
|
-
item.keyframes = (0,
|
|
212
|
+
item.keyframes = (0, transform_keyframe_edit_1.normalizeItemKeyframeGroup)({ ...item,
|
|
213
|
+
keyframes: (0, motion_keyframe_replace_1.replaceXYKeyframes)(item.keyframes, command.patch.xyKeyframes, item.duration) }, 'position').keyframes;
|
|
223
214
|
if (command.patch.transform)
|
|
224
215
|
writeTransform(command.patch.transform);
|
|
225
216
|
if (!command.patch.transform && !command.patch.xyKeyframes)
|
|
@@ -264,13 +255,15 @@ function resolveV2Write(parsed, command) {
|
|
|
264
255
|
editChanged = true;
|
|
265
256
|
}
|
|
266
257
|
if (command.patch.xyKeyframes) {
|
|
267
|
-
item.keyframes = (0,
|
|
258
|
+
item.keyframes = (0, transform_keyframe_edit_1.normalizeItemKeyframeGroup)({ ...item,
|
|
259
|
+
keyframes: (0, motion_keyframe_replace_1.replaceXYKeyframes)(item.keyframes, command.patch.xyKeyframes, item.duration) }, 'position').keyframes;
|
|
268
260
|
editChanged = true;
|
|
269
261
|
}
|
|
270
262
|
}
|
|
271
263
|
else if (command.kind === 'layer') {
|
|
272
264
|
if (command.patch.xyKeyframes) {
|
|
273
|
-
item.keyframes = (0,
|
|
265
|
+
item.keyframes = (0, transform_keyframe_edit_1.normalizeItemKeyframeGroup)({ ...item,
|
|
266
|
+
keyframes: (0, motion_keyframe_replace_1.replaceXYKeyframes)(item.keyframes, command.patch.xyKeyframes, item.duration) }, 'position').keyframes;
|
|
274
267
|
editChanged = true;
|
|
275
268
|
}
|
|
276
269
|
if (command.patch.transform) {
|
|
@@ -298,7 +291,8 @@ function resolveV2Write(parsed, command) {
|
|
|
298
291
|
throw new Error(`映像アイテムではありません: ${itemId}`);
|
|
299
292
|
}
|
|
300
293
|
if (command.patch.xyKeyframes) {
|
|
301
|
-
item.keyframes = (0,
|
|
294
|
+
item.keyframes = (0, transform_keyframe_edit_1.normalizeItemKeyframeGroup)({ ...item,
|
|
295
|
+
keyframes: (0, motion_keyframe_replace_1.replaceXYKeyframes)(item.keyframes, command.patch.xyKeyframes, item.duration) }, 'position').keyframes;
|
|
302
296
|
editChanged = true;
|
|
303
297
|
}
|
|
304
298
|
if (command.patch.transform) {
|
|
@@ -42,3 +42,16 @@ export { LegacyEditVersionError } from './migrate/error';
|
|
|
42
42
|
export * from './adjust-css-visual';
|
|
43
43
|
export { effectiveScale, normalizeTransform } from './transform';
|
|
44
44
|
export * from './transform-keyframe-edit';
|
|
45
|
+
export declare const SAVED_BY_PATH = ".akari/saved-by.json";
|
|
46
|
+
export interface SavedByStamp {
|
|
47
|
+
version: 1;
|
|
48
|
+
app: 'akari-video';
|
|
49
|
+
appVersion: string;
|
|
50
|
+
savedAt: string;
|
|
51
|
+
}
|
|
52
|
+
export declare function parseSavedBy(text: string | undefined): SavedByStamp | undefined;
|
|
53
|
+
export declare function newerSavedByVersion(text: string | undefined, currentVersion: string | undefined): string | undefined;
|
|
54
|
+
export declare function isUnknownKeyEditError(error: unknown): boolean;
|
|
55
|
+
export declare function newerVersionOpenNotice(savedVersion: string, currentVersion: string): string;
|
|
56
|
+
export declare function newerVersionLintPrefix(savedVersion: string): string;
|
|
57
|
+
export declare function withNewerVersionLintPrefix(message: string, savedVersion?: string): string;
|
|
@@ -14,7 +14,13 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.normalizeTransform = exports.effectiveScale = exports.LegacyEditVersionError = exports.parseEdit = void 0;
|
|
17
|
+
exports.SAVED_BY_PATH = exports.normalizeTransform = exports.effectiveScale = exports.LegacyEditVersionError = exports.parseEdit = void 0;
|
|
18
|
+
exports.parseSavedBy = parseSavedBy;
|
|
19
|
+
exports.newerSavedByVersion = newerSavedByVersion;
|
|
20
|
+
exports.isUnknownKeyEditError = isUnknownKeyEditError;
|
|
21
|
+
exports.newerVersionOpenNotice = newerVersionOpenNotice;
|
|
22
|
+
exports.newerVersionLintPrefix = newerVersionLintPrefix;
|
|
23
|
+
exports.withNewerVersionLintPrefix = withNewerVersionLintPrefix;
|
|
18
24
|
/**
|
|
19
25
|
* ブラウザ安全なエントリポイント(テキスト手術のみ)。
|
|
20
26
|
* lint ゲート付き書き込み(Node 専用)は './write-gate' を明示的に import すること
|
|
@@ -67,3 +73,56 @@ var transform_1 = require("./transform");
|
|
|
67
73
|
Object.defineProperty(exports, "effectiveScale", { enumerable: true, get: function () { return transform_1.effectiveScale; } });
|
|
68
74
|
Object.defineProperty(exports, "normalizeTransform", { enumerable: true, get: function () { return transform_1.normalizeTransform; } });
|
|
69
75
|
__exportStar(require("./transform-keyframe-edit"), exports);
|
|
76
|
+
// write-gate.ts の SAVED_BY_PATH と同値に保つ。
|
|
77
|
+
exports.SAVED_BY_PATH = '.akari/saved-by.json';
|
|
78
|
+
const SAVED_BY_SCHEMA_VERSION = 1;
|
|
79
|
+
function parseSavedBy(text) {
|
|
80
|
+
if (!text)
|
|
81
|
+
return undefined;
|
|
82
|
+
try {
|
|
83
|
+
const value = JSON.parse(text);
|
|
84
|
+
if (!value || typeof value !== 'object')
|
|
85
|
+
return undefined;
|
|
86
|
+
const stamp = value;
|
|
87
|
+
return stamp.version === SAVED_BY_SCHEMA_VERSION && stamp.app === 'akari-video'
|
|
88
|
+
&& typeof stamp.appVersion === 'string' && /^\d+\.\d+\.\d+(?:[-+][\w.-]+)?$/.test(stamp.appVersion)
|
|
89
|
+
&& typeof stamp.savedAt === 'string' && !Number.isNaN(Date.parse(stamp.savedAt))
|
|
90
|
+
? stamp : undefined;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function compareSavedByVersions(left, right) {
|
|
97
|
+
const leftParts = left.trim().match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
98
|
+
const rightParts = right.trim().match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
99
|
+
if (!leftParts || !rightParts)
|
|
100
|
+
return 0;
|
|
101
|
+
for (let i = 1; i <= 3; i++) {
|
|
102
|
+
const a = Number(leftParts[i]);
|
|
103
|
+
const b = Number(rightParts[i]);
|
|
104
|
+
if (a !== b)
|
|
105
|
+
return a < b ? -1 : 1;
|
|
106
|
+
}
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
function newerSavedByVersion(text, currentVersion) {
|
|
110
|
+
const savedVersion = parseSavedBy(text)?.appVersion;
|
|
111
|
+
return savedVersion && currentVersion && compareSavedByVersions(savedVersion, currentVersion) > 0
|
|
112
|
+
? savedVersion : undefined;
|
|
113
|
+
}
|
|
114
|
+
function isUnknownKeyEditError(error) {
|
|
115
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
116
|
+
return /edit\.json[^\n]*未定義キーを使用できません/.test(message);
|
|
117
|
+
}
|
|
118
|
+
function newerVersionOpenNotice(savedVersion, currentVersion) {
|
|
119
|
+
return `このプロジェクトは新しい版の AKARI Video(v${savedVersion})で保存されています。`
|
|
120
|
+
+ `いまの版(v${currentVersion})では開けない機能が使われています。AKARI Video を更新してください。`;
|
|
121
|
+
}
|
|
122
|
+
function newerVersionLintPrefix(savedVersion) {
|
|
123
|
+
return `このプロジェクトは新しい版(v${savedVersion})で保存されています。`
|
|
124
|
+
+ 'いまの版の検証は新しい機能を知らないため、誤ってエラーを出すことがあります。';
|
|
125
|
+
}
|
|
126
|
+
function withNewerVersionLintPrefix(message, savedVersion) {
|
|
127
|
+
return savedVersion ? `${newerVersionLintPrefix(savedVersion)} ${message}` : message;
|
|
128
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.writeItemPositionAt = writeItemPositionAt;
|
|
4
|
+
const transform_keyframe_edit_1 = require("./transform-keyframe-edit");
|
|
4
5
|
/** A direct move writes only the requested position axes at the item-local frame. */
|
|
5
6
|
function writeItemPositionAt(item, frame, position) {
|
|
6
7
|
if (!Number.isInteger(frame) || frame < 0 || frame > item.duration
|
|
@@ -9,18 +10,5 @@ function writeItemPositionAt(item, frame, position) {
|
|
|
9
10
|
|| !Number.isFinite(position[axis]))) {
|
|
10
11
|
throw new Error('位置または時刻が正しくありません');
|
|
11
12
|
}
|
|
12
|
-
|
|
13
|
-
if (Array.isArray(copy.keyframes) && copy.keyframes.length >= 2) {
|
|
14
|
-
let point = copy.keyframes.find(entry => entry.t === frame);
|
|
15
|
-
if (!point) {
|
|
16
|
-
point = { t: frame };
|
|
17
|
-
copy.keyframes.push(point);
|
|
18
|
-
copy.keyframes.sort((left, right) => left.t - right.t);
|
|
19
|
-
}
|
|
20
|
-
point.transform = { ...point.transform, ...position };
|
|
21
|
-
}
|
|
22
|
-
else {
|
|
23
|
-
copy.transform = { ...copy.transform, ...position };
|
|
24
|
-
}
|
|
25
|
-
return copy;
|
|
13
|
+
return (0, transform_keyframe_edit_1.writeItemTransformAt)(item, frame, position);
|
|
26
14
|
}
|
|
@@ -2,8 +2,23 @@ import type { ItemV2, TransformV2 } from './edit-v2';
|
|
|
2
2
|
export type TransformField = 'x' | 'y' | 'scale' | 'scaleX' | 'scaleY' | 'rotate';
|
|
3
3
|
/** Effective local pose, in pixels, unitless scale factors, and degrees. */
|
|
4
4
|
export declare function evaluatedItemTransform(item: Pick<ItemV2, 'source' | 'transform' | 'keyframes' | 'duration'>, frame: number): Required<TransformV2>;
|
|
5
|
+
export type ItemKeyframeGroup = 'position' | 'size' | 'rotation' | 'opacity';
|
|
6
|
+
export declare function hasItemKeyframeGroup(item: Pick<ItemV2, 'keyframes'>, group: ItemKeyframeGroup): boolean;
|
|
5
7
|
export declare function hasTransformKeyframe(item: Pick<ItemV2, 'keyframes'>, field: TransformField): boolean;
|
|
6
|
-
/**
|
|
8
|
+
/** The same declared-point/hold/interpolation rule as the transform evaluator. */
|
|
9
|
+
export declare function evaluatedItemOpacity(item: Pick<ItemV2, 'opacity' | 'keyframes' | 'duration'>, frame: number): number;
|
|
10
|
+
/** Backfill old sparse points only when this group is written. Values are read before mutation. */
|
|
11
|
+
export declare function normalizeItemKeyframeGroup(item: ItemV2, group: ItemKeyframeGroup): ItemV2;
|
|
12
|
+
/** Add one visible-pose point. A lone group point also becomes its static value. */
|
|
13
|
+
export declare function activateItemKeyframeGroup(item: ItemV2, frame: number, group: ItemKeyframeGroup): ItemV2;
|
|
14
|
+
/** Item-wide diamond: all four groups share one playhead point and one item result. */
|
|
15
|
+
export declare function activateItemKeyframe(item: ItemV2, frame: number): ItemV2;
|
|
7
16
|
export declare function activateItemTransformKeyframe(item: ItemV2, frame: number, field: TransformField): ItemV2;
|
|
8
|
-
/**
|
|
17
|
+
/** A gesture returns one updated item; every animated group auto-keys at the playhead. */
|
|
9
18
|
export declare function writeItemTransformAt(item: ItemV2, frame: number, input: TransformV2): ItemV2;
|
|
19
|
+
export declare function writeItemOpacityAt(item: ItemV2, frame: number, opacity: number): ItemV2;
|
|
20
|
+
/** Removing the last group point freezes the pose visible immediately before deletion. */
|
|
21
|
+
export declare function removeItemKeyframeGroup(item: ItemV2, frame: number, group: ItemKeyframeGroup): ItemV2;
|
|
22
|
+
/** Timeline point deletion is deliberately whole-point, independent of the selected row. */
|
|
23
|
+
export declare function removeItemKeyframePoint(item: ItemV2, frame: number): ItemV2;
|
|
24
|
+
export declare function moveItemKeyframeGroup(item: ItemV2, fromFrame: number, toFrame: number, group: ItemKeyframeGroup): ItemV2;
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.evaluatedItemTransform = evaluatedItemTransform;
|
|
4
|
+
exports.hasItemKeyframeGroup = hasItemKeyframeGroup;
|
|
4
5
|
exports.hasTransformKeyframe = hasTransformKeyframe;
|
|
6
|
+
exports.evaluatedItemOpacity = evaluatedItemOpacity;
|
|
7
|
+
exports.normalizeItemKeyframeGroup = normalizeItemKeyframeGroup;
|
|
8
|
+
exports.activateItemKeyframeGroup = activateItemKeyframeGroup;
|
|
9
|
+
exports.activateItemKeyframe = activateItemKeyframe;
|
|
5
10
|
exports.activateItemTransformKeyframe = activateItemTransformKeyframe;
|
|
6
11
|
exports.writeItemTransformAt = writeItemTransformAt;
|
|
12
|
+
exports.writeItemOpacityAt = writeItemOpacityAt;
|
|
13
|
+
exports.removeItemKeyframeGroup = removeItemKeyframeGroup;
|
|
14
|
+
exports.removeItemKeyframePoint = removeItemKeyframePoint;
|
|
15
|
+
exports.moveItemKeyframeGroup = moveItemKeyframeGroup;
|
|
7
16
|
const FIELDS = ['x', 'y', 'scale', 'scaleX', 'scaleY', 'rotate'];
|
|
8
|
-
const DEFAULTS = { x: 0, y: 0, scale: 1, scaleX: 1, scaleY: 1, rotate: 0 };
|
|
9
17
|
const finite = (value) => typeof value === 'number' && Number.isFinite(value);
|
|
10
18
|
const isMedia = (item) => item.source.kind === 'media';
|
|
11
19
|
const pointsOf = (item) => Array.isArray(item.keyframes) ? item.keyframes.slice().sort((a, b) => a.t - b.t) : [];
|
|
@@ -92,8 +100,6 @@ function valueAt(points, frame, field, fallback, media, statics) {
|
|
|
92
100
|
if (!finite(value) && (field === 'scaleX' || field === 'scaleY')) {
|
|
93
101
|
value = transform.scale ?? (media ? statics[field] ?? statics.scale ?? 1 : fallback);
|
|
94
102
|
}
|
|
95
|
-
if (!finite(value) && media)
|
|
96
|
-
value = DEFAULTS[field];
|
|
97
103
|
return finite(value) ? [{ point, value }] : [];
|
|
98
104
|
});
|
|
99
105
|
if (!declared.length)
|
|
@@ -122,15 +128,59 @@ function evaluatedItemTransform(item, frame) {
|
|
|
122
128
|
scaleY: staticValue.scaleY ?? staticValue.scale ?? 1, rotate: staticValue.rotate ?? 0
|
|
123
129
|
};
|
|
124
130
|
const points = pointsOf(item), media = isMedia(item);
|
|
125
|
-
if (
|
|
131
|
+
if (!points.some(point => point.transform))
|
|
126
132
|
return base;
|
|
127
133
|
const at = frameOf(item, frame);
|
|
128
134
|
return Object.fromEntries(FIELDS.map(field => [field,
|
|
129
|
-
valueAt(points, at, field,
|
|
130
|
-
|
|
135
|
+
valueAt(points, at, field, base[field], media, staticValue)]));
|
|
136
|
+
}
|
|
137
|
+
const GROUP_FIELDS = {
|
|
138
|
+
position: ['x', 'y'], size: ['scale', 'scaleX', 'scaleY'], rotation: ['rotate']
|
|
139
|
+
};
|
|
140
|
+
const groupOf = (field) => field === 'x' || field === 'y' ? 'position'
|
|
141
|
+
: field === 'rotate' ? 'rotation' : 'size';
|
|
142
|
+
const declares = (point, group) => group === 'opacity'
|
|
143
|
+
? finite(point.opacity) : GROUP_FIELDS[group].some(field => finite(point.transform?.[field]));
|
|
144
|
+
const groupPoints = (item, group) => pointsOf(item).filter(point => declares(point, group));
|
|
145
|
+
const hasPointValue = (point) => Object.entries(point)
|
|
146
|
+
.some(([key, value]) => key !== 't' && key !== 'easing' && value !== undefined);
|
|
147
|
+
function legalPointArray(item, points) {
|
|
148
|
+
const meaningful = points.filter(hasPointValue).sort((a, b) => a.t - b.t);
|
|
149
|
+
if (!meaningful.length)
|
|
150
|
+
return undefined;
|
|
151
|
+
if (meaningful.length > 1)
|
|
152
|
+
return meaningful;
|
|
153
|
+
// The existing file schema requires two array entries. This empty entry has no animated
|
|
154
|
+
// property; every evaluator and diamond list ignores it.
|
|
155
|
+
const t = meaningful[0].t < item.duration ? meaningful[0].t + 1 : meaningful[0].t - 1;
|
|
156
|
+
return [...meaningful, { t }].sort((a, b) => a.t - b.t);
|
|
157
|
+
}
|
|
158
|
+
function hasItemKeyframeGroup(item, group) {
|
|
159
|
+
return groupPoints(item, group).length > 0;
|
|
131
160
|
}
|
|
132
161
|
function hasTransformKeyframe(item, field) {
|
|
133
|
-
return
|
|
162
|
+
return hasItemKeyframeGroup(item, groupOf(field));
|
|
163
|
+
}
|
|
164
|
+
/** The same declared-point/hold/interpolation rule as the transform evaluator. */
|
|
165
|
+
function evaluatedItemOpacity(item, frame) {
|
|
166
|
+
const points = groupPoints(item, 'opacity');
|
|
167
|
+
const fallback = item.opacity ?? 1;
|
|
168
|
+
if (!points.length)
|
|
169
|
+
return fallback;
|
|
170
|
+
const at = frameOf(item, frame);
|
|
171
|
+
if (at <= points[0].t)
|
|
172
|
+
return points[0].opacity;
|
|
173
|
+
const last = points[points.length - 1];
|
|
174
|
+
if (at >= last.t)
|
|
175
|
+
return last.opacity;
|
|
176
|
+
for (let index = 1; index < points.length; index++) {
|
|
177
|
+
const right = points[index], left = points[index - 1];
|
|
178
|
+
if (at > right.t)
|
|
179
|
+
continue;
|
|
180
|
+
const u = eased(right, 'opacity', (at - left.t) / (right.t - left.t || 1), false);
|
|
181
|
+
return left.opacity + (right.opacity - left.opacity) * u;
|
|
182
|
+
}
|
|
183
|
+
return fallback;
|
|
134
184
|
}
|
|
135
185
|
function validPatch(patch) {
|
|
136
186
|
for (const [field, value] of Object.entries(patch)) {
|
|
@@ -147,76 +197,166 @@ function normalizedAxisPatch(current, patch) {
|
|
|
147
197
|
const ratio = previous > 0 ? patch.scale / previous : 1;
|
|
148
198
|
return { ...patch, scaleX: current.scaleX * ratio, scaleY: current.scaleY * ratio };
|
|
149
199
|
}
|
|
200
|
+
function valuesAt(item, frame, group) {
|
|
201
|
+
if (group === 'opacity')
|
|
202
|
+
return evaluatedItemOpacity(item, frame);
|
|
203
|
+
const pose = evaluatedItemTransform(item, frame);
|
|
204
|
+
if (group === 'position')
|
|
205
|
+
return { x: pose.x, y: pose.y };
|
|
206
|
+
if (group === 'rotation')
|
|
207
|
+
return { rotate: pose.rotate };
|
|
208
|
+
return { scale: pose.scale, scaleX: pose.scaleX, scaleY: pose.scaleY };
|
|
209
|
+
}
|
|
210
|
+
function withStaticGroup(item, group, value) {
|
|
211
|
+
if (group === 'opacity')
|
|
212
|
+
return { ...item, opacity: value };
|
|
213
|
+
return { ...item, transform: { ...item.transform, ...value } };
|
|
214
|
+
}
|
|
215
|
+
/** Backfill old sparse points only when this group is written. Values are read before mutation. */
|
|
216
|
+
function normalizeItemKeyframeGroup(item, group) {
|
|
217
|
+
const keyframes = pointsOf(item).map(point => {
|
|
218
|
+
const copy = structuredClone(point);
|
|
219
|
+
if (!declares(point, group))
|
|
220
|
+
return copy;
|
|
221
|
+
const value = valuesAt(item, point.t, group);
|
|
222
|
+
if (group === 'opacity')
|
|
223
|
+
copy.opacity = value;
|
|
224
|
+
else
|
|
225
|
+
copy.transform = { ...copy.transform, ...value };
|
|
226
|
+
return copy;
|
|
227
|
+
});
|
|
228
|
+
return keyframes.length ? { ...item, keyframes } : item;
|
|
229
|
+
}
|
|
150
230
|
function fullMediaPoint(item, point) {
|
|
151
231
|
if (!point.transform)
|
|
152
232
|
return { ...point };
|
|
153
233
|
return { ...point, transform: evaluatedItemTransform(item, point.t) };
|
|
154
234
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
? { scale: Math.sqrt(before.scaleX * before.scaleY), scaleX: before.scaleX, scaleY: before.scaleY }
|
|
164
|
-
: { [field]: before[field] };
|
|
165
|
-
const seat = next.find(point => point.t === at);
|
|
166
|
-
if (seat)
|
|
167
|
-
seat.transform = { ...seat.transform, ...value };
|
|
168
|
-
else
|
|
169
|
-
next.push({ t: at, transform: value });
|
|
170
|
-
if (next.length === 1) {
|
|
171
|
-
next.push({ t: at === 0 ? item.duration : 0, transform: { ...value } });
|
|
235
|
+
function addGroupPoint(item, frame, group, value) {
|
|
236
|
+
const at = frameOf(item, frame);
|
|
237
|
+
const normalized = normalizeItemKeyframeGroup(item, group);
|
|
238
|
+
const keyframes = pointsOf(normalized).map(point => isMedia(item) ? fullMediaPoint(normalized, point) : structuredClone(point));
|
|
239
|
+
let seat = keyframes.find(point => point.t === at);
|
|
240
|
+
if (!seat) {
|
|
241
|
+
seat = { t: at };
|
|
242
|
+
keyframes.push(seat);
|
|
172
243
|
}
|
|
173
|
-
|
|
244
|
+
if (group === 'opacity') {
|
|
245
|
+
seat.opacity = value;
|
|
246
|
+
if (isMedia(item))
|
|
247
|
+
seat.transform = evaluatedItemTransform(item, at);
|
|
248
|
+
}
|
|
249
|
+
else
|
|
250
|
+
seat.transform = { ...(isMedia(item) ? evaluatedItemTransform(item, at) : seat.transform),
|
|
251
|
+
...value };
|
|
252
|
+
return { ...normalized, keyframes: legalPointArray(item, keyframes) };
|
|
253
|
+
}
|
|
254
|
+
/** Add one visible-pose point. A lone group point also becomes its static value. */
|
|
255
|
+
function activateItemKeyframeGroup(item, frame, group) {
|
|
256
|
+
const at = frameOf(item, frame);
|
|
257
|
+
const hadGroup = hasItemKeyframeGroup(item, group);
|
|
258
|
+
const value = valuesAt(item, at, group);
|
|
259
|
+
if (groupPoints(item, group).some(point => point.t === at))
|
|
260
|
+
return normalizeItemKeyframeGroup(item, group);
|
|
261
|
+
const updated = addGroupPoint(item, at, group, value);
|
|
262
|
+
return hadGroup ? updated : withStaticGroup(updated, group, value);
|
|
263
|
+
}
|
|
264
|
+
/** Item-wide diamond: all four groups share one playhead point and one item result. */
|
|
265
|
+
function activateItemKeyframe(item, frame) {
|
|
266
|
+
return ['position', 'size', 'rotation', 'opacity']
|
|
267
|
+
.reduce((current, group) => activateItemKeyframeGroup(current, frame, group), item);
|
|
174
268
|
}
|
|
175
|
-
|
|
269
|
+
function activateItemTransformKeyframe(item, frame, field) {
|
|
270
|
+
return activateItemKeyframeGroup(item, frame, groupOf(field));
|
|
271
|
+
}
|
|
272
|
+
function patchedGroupValue(item, frame, group, patch) {
|
|
273
|
+
const current = evaluatedItemTransform(item, frame);
|
|
274
|
+
if (group === 'position')
|
|
275
|
+
return { x: patch.x ?? current.x, y: patch.y ?? current.y };
|
|
276
|
+
if (group === 'rotation')
|
|
277
|
+
return { rotate: patch.rotate ?? current.rotate };
|
|
278
|
+
const adjusted = normalizedAxisPatch(current, patch);
|
|
279
|
+
const scaleX = adjusted.scaleX ?? current.scaleX, scaleY = adjusted.scaleY ?? current.scaleY;
|
|
280
|
+
return { scale: Math.sqrt(scaleX * scaleY), scaleX, scaleY };
|
|
281
|
+
}
|
|
282
|
+
function writeGroup(item, frame, group, patch) {
|
|
283
|
+
const value = patchedGroupValue(item, frame, group, patch);
|
|
284
|
+
if (!hasItemKeyframeGroup(item, group))
|
|
285
|
+
return withStaticGroup(item, group, value);
|
|
286
|
+
const initialPoint = groupPoints(item, group)[0];
|
|
287
|
+
const updated = addGroupPoint(item, frame, group, value);
|
|
288
|
+
return groupPoints(item, group).length === 1
|
|
289
|
+
? withStaticGroup(updated, group, valuesAt(updated, initialPoint.t, group)) : updated;
|
|
290
|
+
}
|
|
291
|
+
/** A gesture returns one updated item; every animated group auto-keys at the playhead. */
|
|
176
292
|
function writeItemTransformAt(item, frame, input) {
|
|
177
293
|
validPatch(input);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (
|
|
182
|
-
|
|
183
|
-
if (
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (
|
|
200
|
-
|
|
201
|
-
|
|
294
|
+
return [...new Set(Object.keys(input).map(field => groupOf(field)))].reduce((current, group) => writeGroup(current, frame, group, input), item);
|
|
295
|
+
}
|
|
296
|
+
function writeItemOpacityAt(item, frame, opacity) {
|
|
297
|
+
if (!finite(opacity) || opacity < 0 || opacity > 1)
|
|
298
|
+
throw new Error('Invalid opacity');
|
|
299
|
+
if (!hasItemKeyframeGroup(item, 'opacity'))
|
|
300
|
+
return withStaticGroup(item, 'opacity', opacity);
|
|
301
|
+
const initialPoint = groupPoints(item, 'opacity')[0];
|
|
302
|
+
const updated = addGroupPoint(item, frame, 'opacity', opacity);
|
|
303
|
+
return groupPoints(item, 'opacity').length === 1
|
|
304
|
+
? withStaticGroup(updated, 'opacity', valuesAt(updated, initialPoint.t, 'opacity')) : updated;
|
|
305
|
+
}
|
|
306
|
+
/** Removing the last group point freezes the pose visible immediately before deletion. */
|
|
307
|
+
function removeItemKeyframeGroup(item, frame, group) {
|
|
308
|
+
const at = frameOf(item, frame);
|
|
309
|
+
if (!groupPoints(item, group).some(point => point.t === at))
|
|
310
|
+
return item;
|
|
311
|
+
const before = valuesAt(item, at, group);
|
|
312
|
+
const normalized = normalizeItemKeyframeGroup(item, group);
|
|
313
|
+
const keyframes = pointsOf(normalized).map(point => {
|
|
314
|
+
const copy = structuredClone(point);
|
|
315
|
+
if (copy.t !== at)
|
|
316
|
+
return copy;
|
|
317
|
+
if (group === 'opacity')
|
|
318
|
+
delete copy.opacity;
|
|
319
|
+
else if (copy.transform) {
|
|
320
|
+
for (const field of GROUP_FIELDS[group])
|
|
321
|
+
delete copy.transform[field];
|
|
322
|
+
if (!Object.keys(copy.transform).length)
|
|
323
|
+
delete copy.transform;
|
|
202
324
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
325
|
+
return copy;
|
|
326
|
+
}).filter(point => point.transform || point.opacity !== undefined || point.crop || point.perspective
|
|
327
|
+
|| point.animator || point.gain_db !== undefined);
|
|
328
|
+
let updated = { ...normalized, keyframes: legalPointArray(item, keyframes) };
|
|
329
|
+
const remaining = groupPoints(updated, group);
|
|
330
|
+
if (!remaining.length)
|
|
331
|
+
updated = withStaticGroup(updated, group, before);
|
|
332
|
+
else if (remaining.length === 1)
|
|
333
|
+
updated = withStaticGroup(updated, group, valuesAt(normalized, remaining[0].t, group));
|
|
334
|
+
return updated;
|
|
335
|
+
}
|
|
336
|
+
/** Timeline point deletion is deliberately whole-point, independent of the selected row. */
|
|
337
|
+
function removeItemKeyframePoint(item, frame) {
|
|
338
|
+
const at = frameOf(item, frame);
|
|
339
|
+
const point = pointsOf(item).find(entry => entry.t === at);
|
|
340
|
+
if (!point)
|
|
341
|
+
return item;
|
|
342
|
+
const values = ['position', 'size', 'rotation', 'opacity']
|
|
343
|
+
.filter(group => declares(point, group)).map(group => [group, valuesAt(item, at, group)]);
|
|
344
|
+
const keyframes = pointsOf(item).filter(entry => entry.t !== at).map(entry => structuredClone(entry));
|
|
345
|
+
let updated = { ...item, keyframes: legalPointArray(item, keyframes) };
|
|
346
|
+
for (const [group, value] of values)
|
|
347
|
+
if (!hasItemKeyframeGroup(updated, group)) {
|
|
348
|
+
updated = withStaticGroup(updated, group, value);
|
|
217
349
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
350
|
+
return updated;
|
|
351
|
+
}
|
|
352
|
+
function moveItemKeyframeGroup(item, fromFrame, toFrame, group) {
|
|
353
|
+
const from = frameOf(item, fromFrame), to = frameOf(item, toFrame);
|
|
354
|
+
if (from === to)
|
|
355
|
+
return item;
|
|
356
|
+
const point = groupPoints(item, group).find(entry => entry.t === from);
|
|
357
|
+
if (!point)
|
|
358
|
+
return item;
|
|
359
|
+
const value = valuesAt(item, from, group);
|
|
360
|
+
const removed = removeItemKeyframeGroup(item, from, group);
|
|
361
|
+
return addGroupPoint(removed, to, group, value);
|
|
222
362
|
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* 編集不能より lint なし保存の方が被害が小さいという判断(型不正は各書き込みの
|
|
17
17
|
* ローカル検証が別途残るため安全側は保たれる)。
|
|
18
18
|
*/
|
|
19
|
+
export declare const SAVED_BY_PATH = ".akari/saved-by.json";
|
|
19
20
|
export interface EditLintFinding {
|
|
20
21
|
severity?: string;
|
|
21
22
|
message?: string;
|
|
@@ -30,6 +31,8 @@ export interface EditLintGateResult {
|
|
|
30
31
|
/** 候補ファイル名(プロジェクト直下からの相対パス)→ 書き込み予定の全文。null は不在扱い。 */
|
|
31
32
|
export type LintCandidates = Record<string, string | null>;
|
|
32
33
|
export interface DeferredLintOptions {
|
|
34
|
+
/** edit.json を保存した AKARI Video の版。不明なら既存スタンプを消す。 */
|
|
35
|
+
appVersion?: string;
|
|
33
36
|
debounceMs?: number;
|
|
34
37
|
onLintResult?: (result: EditLintGateResult) => void | Promise<void>;
|
|
35
38
|
/** Deterministic test seam; production callers use runEditLint. */
|
|
@@ -77,6 +80,10 @@ export declare function lintProjectCandidates(projectRoot: string, candidates: L
|
|
|
77
80
|
export declare function lintProjectCandidatesOnDisk(projectRoot: string, candidates: LintCandidates, hooks?: ShadowLintHooks): Promise<EditLintGateResult>;
|
|
78
81
|
/** 互換 API。保存後 lint への移行後も、明示的に検証したい呼び出し側向けに残す。 */
|
|
79
82
|
export declare function assertLintPasses(projectRoot: string, candidates: LintCandidates): Promise<void>;
|
|
83
|
+
/** CLI / preview-server 等のプロセス内で書き手の版を 1 回だけ設定する。 */
|
|
84
|
+
export declare function setDefaultSavedByAppVersion(version: string | undefined): void;
|
|
85
|
+
/** CLI の既存 edit.json 保存直後に、スタンプだけを atomic に更新する。 */
|
|
86
|
+
export declare function writeSavedByStamp(projectRoot: string, appVersion: string | undefined): Promise<void>;
|
|
80
87
|
/** atomic 保存を即時完了し、lint は末尾 debounce で非同期に実行する。 */
|
|
81
88
|
export declare function writeProjectFilesGuarded(projectRoot: string, candidates: LintCandidates, options?: DeferredLintOptions): Promise<void>;
|
|
82
89
|
/** Web UI 旧版が生成した camelCase は schema が閉じていない legacy edit でも保存させない。 */
|
|
@@ -18,9 +18,12 @@
|
|
|
18
18
|
* ローカル検証が別途残るため安全側は保たれる)。
|
|
19
19
|
*/
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.SAVED_BY_PATH = void 0;
|
|
21
22
|
exports.lintProjectCandidates = lintProjectCandidates;
|
|
22
23
|
exports.lintProjectCandidatesOnDisk = lintProjectCandidatesOnDisk;
|
|
23
24
|
exports.assertLintPasses = assertLintPasses;
|
|
25
|
+
exports.setDefaultSavedByAppVersion = setDefaultSavedByAppVersion;
|
|
26
|
+
exports.writeSavedByStamp = writeSavedByStamp;
|
|
24
27
|
exports.writeProjectFilesGuarded = writeProjectFilesGuarded;
|
|
25
28
|
exports.assertNoCamelCaseTransitionOut = assertNoCamelCaseTransitionOut;
|
|
26
29
|
exports.scheduleProjectLint = scheduleProjectLint;
|
|
@@ -32,6 +35,16 @@ const fs_1 = require("fs");
|
|
|
32
35
|
const path_1 = require("path");
|
|
33
36
|
const url_1 = require("url");
|
|
34
37
|
const os_1 = require("os");
|
|
38
|
+
// index.ts の SAVED_BY_PATH と同値に保つ(Node 専用入口は index を import しない)。
|
|
39
|
+
exports.SAVED_BY_PATH = '.akari/saved-by.json';
|
|
40
|
+
const SAVED_BY_SCHEMA_VERSION = 1;
|
|
41
|
+
const APP_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:[-+][\w.-]+)?$/;
|
|
42
|
+
function isValidSavedByAppVersion(version) {
|
|
43
|
+
return typeof version === 'string' && APP_VERSION_PATTERN.test(version);
|
|
44
|
+
}
|
|
45
|
+
function serializeSavedByStamp(appVersion, savedAt = new Date().toISOString()) {
|
|
46
|
+
return `${JSON.stringify({ version: SAVED_BY_SCHEMA_VERSION, app: 'akari-video', appVersion, savedAt }, null, 2)}\n`;
|
|
47
|
+
}
|
|
35
48
|
/**
|
|
36
49
|
* 「OS / ファイルシステムがリンク作成自体を拒んだ」エラーコード。入力が誤っている系
|
|
37
50
|
* (EEXIST・ENOENT・ENOTDIR 等)は含めない — それらは従来どおり throw して原因を隠さない。
|
|
@@ -183,6 +196,22 @@ async function assertLintPasses(projectRoot, candidates) {
|
|
|
183
196
|
throw new Error(result.errors[0] ?? 'edit-lint が変更を拒否しました');
|
|
184
197
|
}
|
|
185
198
|
}
|
|
199
|
+
let defaultSavedByAppVersion;
|
|
200
|
+
/** CLI / preview-server 等のプロセス内で書き手の版を 1 回だけ設定する。 */
|
|
201
|
+
function setDefaultSavedByAppVersion(version) {
|
|
202
|
+
defaultSavedByAppVersion = isValidSavedByAppVersion(version) ? version : undefined;
|
|
203
|
+
}
|
|
204
|
+
/** CLI の既存 edit.json 保存直後に、スタンプだけを atomic に更新する。 */
|
|
205
|
+
async function writeSavedByStamp(projectRoot, appVersion) {
|
|
206
|
+
const destination = (0, path_1.join)(projectRoot, exports.SAVED_BY_PATH);
|
|
207
|
+
if (isValidSavedByAppVersion(appVersion)) {
|
|
208
|
+
await writeAtomic(destination, serializeSavedByStamp(appVersion));
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
// 版不明の書き手は、以前の書き手の版を主張できない。
|
|
212
|
+
await fs_1.promises.rm(destination, { force: true });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
186
215
|
/** atomic 保存を即時完了し、lint は末尾 debounce で非同期に実行する。 */
|
|
187
216
|
async function writeProjectFilesGuarded(projectRoot, candidates, options = {}) {
|
|
188
217
|
const editCandidate = candidates['edit.json'];
|
|
@@ -195,7 +224,17 @@ async function writeProjectFilesGuarded(projectRoot, candidates, options = {}) {
|
|
|
195
224
|
}
|
|
196
225
|
const destination = (0, path_1.join)(projectRoot, name);
|
|
197
226
|
await writeAtomic(destination, text);
|
|
198
|
-
|
|
227
|
+
if (name === 'edit.json') {
|
|
228
|
+
const version = options.appVersion ?? defaultSavedByAppVersion;
|
|
229
|
+
if (isValidSavedByAppVersion(version)) {
|
|
230
|
+
await writeAtomic((0, path_1.join)(projectRoot, exports.SAVED_BY_PATH), serializeSavedByStamp(version));
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
// An unknown writer cannot keep a previous writer's version claim.
|
|
234
|
+
await fs_1.promises.rm((0, path_1.join)(projectRoot, exports.SAVED_BY_PATH), { force: true });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// edit.json とスタンプの rename が完了したら同期で通知する。lint スケジュールより前に出すことで、
|
|
199
238
|
// 購読側が watcher(実測 42〜1183ms のばらつき)を待たずに済む。
|
|
200
239
|
if (options.onDidWrite) {
|
|
201
240
|
try {
|
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
window.akari = window.akari || {};
|
|
4
4
|
|
|
5
5
|
window.akari.interaction = (() => {
|
|
6
|
+
// A pointer gesture belongs to one controller at a time.
|
|
7
|
+
function canBeginPointerInteraction(owner) { return owner == null; }
|
|
8
|
+
let pointerOwner = null;
|
|
9
|
+
function setPointerOwner(owner) {
|
|
10
|
+
if (owner == null) { pointerOwner = null; return; }
|
|
11
|
+
if (pointerOwner != null && pointerOwner !== owner) return false;
|
|
12
|
+
pointerOwner = owner;
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
function releasePointerOwner(owner) {
|
|
16
|
+
if (pointerOwner !== owner) return false;
|
|
17
|
+
pointerOwner = null;
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
|
|
6
21
|
// BEGIN selection-scope
|
|
7
22
|
function lineage(tree, id) {
|
|
8
23
|
const nodes = new Map(tree.map(node => [node.id, node]));
|
|
@@ -144,6 +159,20 @@ function marqueeHits(candidates, rect) {
|
|
|
144
159
|
let activeDrag = null;
|
|
145
160
|
let activeResize = null;
|
|
146
161
|
let activeRotate = null;
|
|
162
|
+
function reportLiveValues(id, values, clear = false) {
|
|
163
|
+
if (!id || typeof window.akari?.reportLiveValues !== 'function') return;
|
|
164
|
+
window.akari.reportLiveValues({ id, ...(clear ? { clear: true } : { values }) });
|
|
165
|
+
}
|
|
166
|
+
function reportLivePose(gesture) {
|
|
167
|
+
if (!gesture || !gesture.moved) return;
|
|
168
|
+
const pose = gesture.group
|
|
169
|
+
? gesture.pose ?? { ...gesture.transform, x: gesture.startX + gesture.dx,
|
|
170
|
+
y: gesture.startY + gesture.dy }
|
|
171
|
+
: readTransform(gesture.container);
|
|
172
|
+
reportLiveValues(gesture.overlayId, { x: pose.x, y: pose.y,
|
|
173
|
+
scale: pose.scale, scaleX: pose.scaleX ?? pose.scale,
|
|
174
|
+
scaleY: pose.scaleY ?? pose.scale, rotate: pose.rotate });
|
|
175
|
+
}
|
|
147
176
|
let activeLine = null;
|
|
148
177
|
let rotationBadge = null;
|
|
149
178
|
let handleHint = null;
|
|
@@ -1588,6 +1617,7 @@ function marqueeHits(candidates, rect) {
|
|
|
1588
1617
|
if (!activeDrag) return;
|
|
1589
1618
|
|
|
1590
1619
|
const drag = activeDrag;
|
|
1620
|
+
reportLiveValues(drag.overlayId, undefined, true);
|
|
1591
1621
|
activeDrag = null;
|
|
1592
1622
|
if (drag.group) {
|
|
1593
1623
|
moveGroupMembers(drag, 0, 0);
|
|
@@ -2275,6 +2305,7 @@ function marqueeHits(candidates, rect) {
|
|
|
2275
2305
|
function cancelRotate() {
|
|
2276
2306
|
if (!activeRotate) return;
|
|
2277
2307
|
const rotation = activeRotate;
|
|
2308
|
+
reportLiveValues(rotation.overlayId, undefined, true);
|
|
2278
2309
|
activeRotate = null;
|
|
2279
2310
|
rotationBadge?.remove(); rotationBadge = null;
|
|
2280
2311
|
document.body.style.cursor = '';
|
|
@@ -2620,6 +2651,7 @@ function marqueeHits(candidates, rect) {
|
|
|
2620
2651
|
if (!activeResize) return;
|
|
2621
2652
|
|
|
2622
2653
|
const resize = activeResize;
|
|
2654
|
+
reportLiveValues(resize.overlayId, undefined, true);
|
|
2623
2655
|
activeResize = null;
|
|
2624
2656
|
handleHint?.remove(); handleHint = null;
|
|
2625
2657
|
if (resize.group) {
|
|
@@ -2695,7 +2727,7 @@ function marqueeHits(candidates, rect) {
|
|
|
2695
2727
|
}
|
|
2696
2728
|
|
|
2697
2729
|
function onPointerDown(event) {
|
|
2698
|
-
if (!interactionEnabled) return;
|
|
2730
|
+
if (!interactionEnabled || !canBeginPointerInteraction(pointerOwner)) return;
|
|
2699
2731
|
if (event.button !== 0 || activeDrag || activeResize || activeRotate || activeLine) return;
|
|
2700
2732
|
if (selectedId && stage && event.target instanceof Element) {
|
|
2701
2733
|
const bounds = stage.getBoundingClientRect();
|
|
@@ -2860,10 +2892,12 @@ function marqueeHits(candidates, rect) {
|
|
|
2860
2892
|
}
|
|
2861
2893
|
if (activeRotate && event.pointerId === activeRotate.pointerId) {
|
|
2862
2894
|
updateRotate(event);
|
|
2895
|
+
reportLivePose(activeRotate);
|
|
2863
2896
|
return;
|
|
2864
2897
|
}
|
|
2865
2898
|
if (activeResize && event.pointerId === activeResize.pointerId) {
|
|
2866
2899
|
updateResize(event);
|
|
2900
|
+
reportLivePose(activeResize);
|
|
2867
2901
|
return;
|
|
2868
2902
|
}
|
|
2869
2903
|
|
|
@@ -2903,6 +2937,7 @@ function marqueeHits(candidates, rect) {
|
|
|
2903
2937
|
? { x: videoDeltaX, y: 0 }
|
|
2904
2938
|
: event.shiftKey ? { x: 0, y: videoDeltaY } : { x: videoDeltaX, y: videoDeltaY });
|
|
2905
2939
|
moveGroupDrag(drag, locked.x, locked.y, event.metaKey || event.ctrlKey, lockedAxis);
|
|
2940
|
+
reportLivePose(drag);
|
|
2906
2941
|
if (event.cancelable) event.preventDefault();
|
|
2907
2942
|
return;
|
|
2908
2943
|
}
|
|
@@ -2917,6 +2952,7 @@ function marqueeHits(candidates, rect) {
|
|
|
2917
2952
|
event.metaKey || event.ctrlKey,
|
|
2918
2953
|
lockedAxis
|
|
2919
2954
|
);
|
|
2955
|
+
reportLivePose(drag);
|
|
2920
2956
|
|
|
2921
2957
|
if (event.cancelable) event.preventDefault();
|
|
2922
2958
|
}
|
|
@@ -3374,6 +3410,7 @@ function marqueeHits(candidates, rect) {
|
|
|
3374
3410
|
}
|
|
3375
3411
|
|
|
3376
3412
|
function onKeyDown(event) {
|
|
3413
|
+
if (!canBeginPointerInteraction(pointerOwner)) return;
|
|
3377
3414
|
if (event.isComposing) return;
|
|
3378
3415
|
if (handleNudge(event)) return;
|
|
3379
3416
|
if (selectionTree().length) {
|
|
@@ -3825,6 +3862,11 @@ function marqueeHits(candidates, rect) {
|
|
|
3825
3862
|
}
|
|
3826
3863
|
|
|
3827
3864
|
return {
|
|
3865
|
+
get pointerOwner() { return pointerOwner; },
|
|
3866
|
+
get activePointerOperation() { return Boolean(activeDrag || activeResize || activeRotate || activeLine || marqueeFrame); },
|
|
3867
|
+
canBeginPointerInteraction,
|
|
3868
|
+
setPointerOwner,
|
|
3869
|
+
releasePointerOwner,
|
|
3828
3870
|
get selectedId() { return selectedId; },
|
|
3829
3871
|
get selectedIds() { return [...selectedIds]; },
|
|
3830
3872
|
get selectionKind() { return selectionKind(); },
|
|
@@ -35,6 +35,12 @@ const PREVIEW_3D_MAX_RENDER_SIZE = 720;
|
|
|
35
35
|
// getAnimations() 一覧のキャッシュ寿命(ms。app.js と同値)。詳細は tick() の注記。
|
|
36
36
|
const ANIMATIONS_CACHE_MS = 250;
|
|
37
37
|
|
|
38
|
+
// 再生中の CSS アニメは走らせたままにし、タイムラインとのずれがこの幅を超えたときだけ
|
|
39
|
+
// currentTime を書き戻す(ms。出力 30fps の 1.5 フレーム相当)。詳細は syncAnimation() の注記。
|
|
40
|
+
const PLAYBACK_DRIFT_MS = 50;
|
|
41
|
+
// 再生速度の推定に使う最短の観測窓(ms)。これより短い区間の Δ時刻/Δ壁時計 はフレーム量子化で暴れる。
|
|
42
|
+
const PLAYBACK_RATE_WINDOW_MS = 200;
|
|
43
|
+
|
|
38
44
|
// maxRenderSize の正規化: 未指定 → 既定 720 / null・0・false → 無効(等倍) /
|
|
39
45
|
// 正の有限数 → その値。それ以外の不正値(負数・NaN・文字列)は既定へ戻す。
|
|
40
46
|
function resolveMaxRenderSize(value) {
|
|
@@ -130,6 +136,82 @@ function createOverlayRuntime(options = {}) {
|
|
|
130
136
|
return true;
|
|
131
137
|
}
|
|
132
138
|
|
|
139
|
+
// CSS アニメ 1 本をタイムラインのローカル時刻へ合わせる。
|
|
140
|
+
//
|
|
141
|
+
// 停止・スクラブ中は従来どおり pause + currentTime で毎 tick その時刻の姿勢に固定する
|
|
142
|
+
// (シーク後の絵の決定性はここで担保する)。
|
|
143
|
+
//
|
|
144
|
+
// 再生中は毎フレームのシークをやめ、Animation を走らせたまま(play)にする。毎フレーム
|
|
145
|
+
// currentTime を書くとアニメがメインスレッド駆動になり、拡大縮小を含む層が毎フレーム
|
|
146
|
+
// ラスタし直される(実測: CSS アニメ 339 本の合成断片で GPU ラスタ ~115 回/フレーム・
|
|
147
|
+
// 60Hz 表示で 30〜34fps。走らせたままにすると ~58fps・ラスタ 1/4.5)。ずれが
|
|
148
|
+
// PLAYBACK_DRIFT_MS を超えたとき(動画側の停滞・速度変更の直後など)だけ書き戻す。
|
|
149
|
+
//
|
|
150
|
+
// 有限アニメの終端以降は play() しない(finished な Animation への play() は先頭へ
|
|
151
|
+
// 巻き戻る)。pause して fill の姿勢で止め、以後は同じ値を書き直さない。
|
|
152
|
+
//
|
|
153
|
+
// 走っていた Animation の pause() / play() は次のフレームまで保留(pending)され、保留が
|
|
154
|
+
// 解けたときの時刻で currentTime が上書きされることがある(実測: 巻き戻し再生の直後に
|
|
155
|
+
// 止めると、まれに 1 フレーム 16.6ms 進んだ姿勢で止まる)。止めた時刻を pausedTargets に
|
|
156
|
+
// 記録し、保留が解けた(ready)後にもう一度書き戻す。再生へ戻ったら記録を消す。
|
|
157
|
+
const pausedTargets = new WeakMap();
|
|
158
|
+
function holdPausedTime(animation, localTimeMs) {
|
|
159
|
+
pausedTargets.set(animation, localTimeMs);
|
|
160
|
+
if (!animation.pending || !animation.ready?.then) return;
|
|
161
|
+
animation.ready.then(() => {
|
|
162
|
+
if (pausedTargets.get(animation) !== localTimeMs || animation.playState !== "paused") return;
|
|
163
|
+
if (Number(animation.currentTime) !== localTimeMs) animation.currentTime = localTimeMs;
|
|
164
|
+
}, () => {});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function syncAnimation(animation, localTimeMs, playing, playbackRate) {
|
|
168
|
+
const endTime = Number(animation.effect?.getComputedTiming?.().endTime);
|
|
169
|
+
const current = Number(animation.currentTime);
|
|
170
|
+
if (playing && !(localTimeMs >= endTime)) {
|
|
171
|
+
pausedTargets.delete(animation);
|
|
172
|
+
if (animation.playState !== "running") {
|
|
173
|
+
animation.currentTime = localTimeMs;
|
|
174
|
+
animation.playbackRate = playbackRate;
|
|
175
|
+
animation.play();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (animation.playbackRate !== playbackRate) animation.playbackRate = playbackRate;
|
|
179
|
+
if (!Number.isFinite(current) || Math.abs(current - localTimeMs) > PLAYBACK_DRIFT_MS) {
|
|
180
|
+
animation.currentTime = localTimeMs;
|
|
181
|
+
}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (playing && animation.playState === "paused" && current >= endTime) return;
|
|
185
|
+
if (animation.playState !== "paused") animation.pause();
|
|
186
|
+
if (current !== localTimeMs) animation.currentTime = localTimeMs;
|
|
187
|
+
holdPausedTime(animation, localTimeMs);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 再生速度(タイムライン秒 / 壁時計秒)の推定。ホストは tick(t, playing) しか渡さないので、
|
|
191
|
+
// 再生中の連続 tick から測る。停止・逆行・大きな飛び(シーク)で観測をやり直す。
|
|
192
|
+
let playbackAnchor = null;
|
|
193
|
+
let playbackRate = 1;
|
|
194
|
+
function observePlaybackRate(timelineTime, playing, nowMs) {
|
|
195
|
+
if (!playing) {
|
|
196
|
+
playbackAnchor = null;
|
|
197
|
+
return playbackRate;
|
|
198
|
+
}
|
|
199
|
+
const expected = playbackAnchor
|
|
200
|
+
? playbackAnchor.time + ((nowMs - playbackAnchor.wall) / 1000) * playbackRate
|
|
201
|
+
: timelineTime;
|
|
202
|
+
if (!playbackAnchor || timelineTime < playbackAnchor.time || Math.abs(timelineTime - expected) > 1) {
|
|
203
|
+
playbackAnchor = { time: timelineTime, wall: nowMs };
|
|
204
|
+
return playbackRate;
|
|
205
|
+
}
|
|
206
|
+
const elapsed = nowMs - playbackAnchor.wall;
|
|
207
|
+
if (elapsed >= PLAYBACK_RATE_WINDOW_MS) {
|
|
208
|
+
const measured = Math.round(((timelineTime - playbackAnchor.time) * 1000 / elapsed) * 20) / 20;
|
|
209
|
+
if (measured > 0 && Number.isFinite(measured)) playbackRate = measured;
|
|
210
|
+
playbackAnchor = { time: timelineTime, wall: nowMs };
|
|
211
|
+
}
|
|
212
|
+
return playbackRate;
|
|
213
|
+
}
|
|
214
|
+
|
|
133
215
|
function unmount() {
|
|
134
216
|
for (const overlay of mountedOverlays) {
|
|
135
217
|
for (const runtime of renderingRuntimes(overlay.container)) runtime.dispose(overlay.container);
|
|
@@ -324,6 +406,7 @@ function createOverlayRuntime(options = {}) {
|
|
|
324
406
|
function tick(t, playing) {
|
|
325
407
|
const timelineTime = finiteNumber(t, 0);
|
|
326
408
|
if (premount && !premountConfigured) applyPremountConfiguration();
|
|
409
|
+
const rate = observePlaybackRate(timelineTime, Boolean(playing), performance.now());
|
|
327
410
|
|
|
328
411
|
for (const overlay of mountedOverlays) {
|
|
329
412
|
const visible =
|
|
@@ -386,7 +469,7 @@ function createOverlayRuntime(options = {}) {
|
|
|
386
469
|
// 総数」にほぼ比例する(上の注記)。断片のアニメは `[data-akari-active]` ゲートで宣言する
|
|
387
470
|
// 規約なので、可視の間は顔ぶれが変わらない。毎 tick 引き直さず、可視化フリップ直後の
|
|
388
471
|
// tick と 250ms ごとだけ引き直す(遅れて生える animation も拾える。app.js と同じ)。
|
|
389
|
-
// Animation オブジェクトはライブなので、キャッシュ済みでも
|
|
472
|
+
// Animation オブジェクトはライブなので、キャッシュ済みでも syncAnimation() の書き込みと
|
|
390
473
|
// 下の entryAnimationsSettled() の読み取りは現在値で動く。
|
|
391
474
|
//
|
|
392
475
|
// 3D 断片も必ずここを通す。three のシーンは three 側が時刻を持つ(mixer.setTime)が、
|
|
@@ -405,10 +488,7 @@ function createOverlayRuntime(options = {}) {
|
|
|
405
488
|
overlay.animationsAt = nowMs;
|
|
406
489
|
}
|
|
407
490
|
const animations = overlay.animations;
|
|
408
|
-
for (const animation of animations)
|
|
409
|
-
animation.pause();
|
|
410
|
-
animation.currentTime = localTimeMs;
|
|
411
|
-
}
|
|
491
|
+
for (const animation of animations) syncAnimation(animation, localTimeMs, Boolean(playing), rate);
|
|
412
492
|
for (const runtime of renderingRuntimes(overlay.container)) {
|
|
413
493
|
// playing はランタイム側の動画テクスチャの同期方式(再生中は <video> を走らせ、停止・スクラブ中はシーク)に使う
|
|
414
494
|
runtime.render(overlay.container, localTimeMs / 1000, { syncVideos: true, maxRenderSize, playing: Boolean(playing) });
|
|
@@ -87,7 +87,7 @@ const FALLBACK_WORKFLOW = {
|
|
|
87
87
|
{ path: 'exports', label: '書き出し', kind: 'exports' }
|
|
88
88
|
],
|
|
89
89
|
tree: {
|
|
90
|
-
hidden: ['.claude', '.agents', '.codex', '.cursor', '.akari', 'CLAUDE.md', 'AGENTS.md', '.gitignore', '.gitkeep'],
|
|
90
|
+
hidden: ['.claude', '.agents', '.codex', '.cursor', '.opencode', '.devin', '.akari', 'CLAUDE.md', 'AGENTS.md', '.gitignore', '.gitkeep'],
|
|
91
91
|
sidecarSuffixes: ['.meta.json', '.decisions.json', '.analysis.json'],
|
|
92
92
|
developerModePreference: 'akari.developerMode'
|
|
93
93
|
},
|
|
@@ -216,7 +216,7 @@ export async function writeFallbackTemplate(destinationDir) {
|
|
|
216
216
|
return { writtenFiles, skippedExisting };
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
const SKILL_ADAPTER_DIRECTORIES = ['.agents', '.codex', '.cursor', '.opencode'];
|
|
219
|
+
const SKILL_ADAPTER_DIRECTORIES = ['.agents', '.codex', '.cursor', '.opencode', '.devin'];
|
|
220
220
|
|
|
221
221
|
function isPermissionDenied(error) {
|
|
222
222
|
return error && typeof error === 'object' && (error.code === 'EPERM' || error.code === 'EACCES');
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { writeFallbackTemplate } from '../src/index.mjs';
|
|
7
|
+
|
|
8
|
+
test('scaffold の fallback workflow は全スキルリンク置き場を隠す', async t => {
|
|
9
|
+
const root = await mkdtemp(join(tmpdir(), 'akari-hidden-adapters-'));
|
|
10
|
+
t.after(() => rm(root, { recursive: true, force: true }));
|
|
11
|
+
await writeFallbackTemplate(root);
|
|
12
|
+
const workflow = JSON.parse(await readFile(join(root, '.akari', 'workflow.json'), 'utf8'));
|
|
13
|
+
for (const adapter of ['.agents', '.codex', '.cursor', '.opencode', '.devin']) {
|
|
14
|
+
assert.ok(workflow.tree.hidden.includes(adapter), adapter);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
@@ -17,7 +17,7 @@ test('installSkillAdapters: kits の skill を全アダプタへ合成し、重
|
|
|
17
17
|
await mkdir(path.join(kitSkills, 'official'), { recursive: true });
|
|
18
18
|
|
|
19
19
|
const report = await installSkillAdapters(root, { env: { AKARI_HOME: home } });
|
|
20
|
-
for (const adapter of ['.agents', '.codex', '.cursor', '.opencode']) {
|
|
20
|
+
for (const adapter of ['.agents', '.codex', '.cursor', '.opencode', '.devin']) {
|
|
21
21
|
const kitLink = path.join(root, adapter, 'skills', 'kit-only');
|
|
22
22
|
assert.equal(
|
|
23
23
|
path.resolve(path.dirname(kitLink), await readlink(kitLink)),
|
|
@@ -29,7 +29,7 @@ test('installSkillAdapters: kits の skill を全アダプタへ合成し、重
|
|
|
29
29
|
path.join(root, '.claude', 'skills', 'official')
|
|
30
30
|
);
|
|
31
31
|
}
|
|
32
|
-
assert.equal(report.warnings.length,
|
|
32
|
+
assert.equal(report.warnings.length, 5);
|
|
33
33
|
assert.ok(report.warnings.every((line) => line.includes('純正スキルを優先')));
|
|
34
34
|
} finally {
|
|
35
35
|
await rm(root, { recursive: true, force: true });
|
|
@@ -182,10 +182,11 @@ test("installSkillAdapters: on win32 with symlink EPERM, all adapters degrade to
|
|
|
182
182
|
".agents/skills/analyze-footage",
|
|
183
183
|
".codex/skills/analyze-footage",
|
|
184
184
|
".cursor/skills/analyze-footage",
|
|
185
|
+
".devin/skills/analyze-footage",
|
|
185
186
|
".opencode/skills/analyze-footage"
|
|
186
187
|
]);
|
|
187
188
|
assert.deepEqual(report.skippedExisting, []);
|
|
188
|
-
assert.equal(report.degraded.length,
|
|
189
|
+
assert.equal(report.degraded.length, 5);
|
|
189
190
|
assert.ok(report.degraded.every((entry) => entry.method === "junction"));
|
|
190
191
|
});
|
|
191
192
|
});
|
|
@@ -201,6 +202,7 @@ test("installSkillAdapters: real fs on the current platform still creates plain
|
|
|
201
202
|
".agents/skills/analyze-footage",
|
|
202
203
|
".codex/skills/analyze-footage",
|
|
203
204
|
".cursor/skills/analyze-footage",
|
|
205
|
+
".devin/skills/analyze-footage",
|
|
204
206
|
".opencode/skills/analyze-footage"
|
|
205
207
|
]);
|
|
206
208
|
assert.deepEqual(report.degraded, []);
|