@aura3d/editor-runtime 1.4.0 → 1.4.1
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 +36 -5
- package/src/AnimationSceneEditor.ts +0 -368
- package/src/AssetDropZone.ts +0 -144
- package/src/CameraPathEditor.ts +0 -223
- package/src/Command.ts +0 -11
- package/src/CommandHistory.ts +0 -136
- package/src/CurveEditor.ts +0 -152
- package/src/DiagnosticsOverlayModel.ts +0 -85
- package/src/EditorPluginHost.ts +0 -106
- package/src/EditorRuntime.ts +0 -349
- package/src/EditorStateModel.ts +0 -336
- package/src/EpisodeReviewPanel.ts +0 -236
- package/src/EventTrackEditor.ts +0 -169
- package/src/Gizmo.ts +0 -101
- package/src/HierarchyModel.ts +0 -57
- package/src/InspectorModel.ts +0 -71
- package/src/KeyframeEditor.ts +0 -214
- package/src/LocalizationAccessibilityFixtures.ts +0 -368
- package/src/MaterialVariantWorkflow.ts +0 -101
- package/src/MultiUserReviewWorkflow.ts +0 -229
- package/src/NonlinearAnimationEditor.ts +0 -262
- package/src/PickingService.ts +0 -174
- package/src/PlayModeBridge.ts +0 -27
- package/src/PrefabRegistry.ts +0 -164
- package/src/ProjectSerializer.ts +0 -200
- package/src/PropertyPanel.ts +0 -82
- package/src/RenderQueuePanel.ts +0 -106
- package/src/RotateGizmo.ts +0 -35
- package/src/ScaleGizmo.ts +0 -40
- package/src/SceneOutliner.ts +0 -78
- package/src/Selection.ts +0 -63
- package/src/ShaderGraphModel.ts +0 -254
- package/src/StaticExportRuntime.ts +0 -322
- package/src/TimelineEditorController.ts +0 -525
- package/src/TimelineModel.ts +0 -466
- package/src/TimelineRuntimeBridge.ts +0 -280
- package/src/TimelineTrackTypes.ts +0 -89
- package/src/TimelineUI.ts +0 -153
- package/src/TranslateGizmo.ts +0 -39
- package/src/VisualReviewDashboard.ts +0 -87
- package/src/commands/CreateNodeCommand.ts +0 -23
- package/src/commands/DeleteNodeCommand.ts +0 -74
- package/src/commands/ReparentNodeCommand.ts +0 -55
- package/src/commands/SetPropertyCommand.ts +0 -47
- package/src/commands/TransformCommand.ts +0 -107
- package/src/index.ts +0 -140
- package/tests/editor-runtime.test.ts +0 -1087
- package/tsconfig.json +0 -7
package/src/EventTrackEditor.ts
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
// Editor authoring controller for animation event tracks (T2.2). Lets a timeline UI add / move /
|
|
2
|
-
// delete typed markers on named lanes (hitbox active-frames, footsteps, VFX, generic markers) and
|
|
3
|
-
// serialize to the canonical `animation-event-tracks/v1` shape that `@aura3d/animation`'s
|
|
4
|
-
// `deserializeAnimationEventTracks` / `sampleClipEvents` consume — so authored tracks drive gameplay
|
|
5
|
-
// (hitbox windows) and playback (footstep/VFX triggers) deterministically.
|
|
6
|
-
//
|
|
7
|
-
// Type-only imports from `@aura3d/animation` keep this controller free of any runtime coupling to
|
|
8
|
-
// that package (the types are erased at build time); the serialized output is verified to round-trip
|
|
9
|
-
// through the animation package's deserializer in tests.
|
|
10
|
-
|
|
11
|
-
import type {
|
|
12
|
-
AnimationClipEvent,
|
|
13
|
-
AnimationClipEventType,
|
|
14
|
-
AnimationEventTrack,
|
|
15
|
-
SerializedAnimationEventTracks
|
|
16
|
-
} from "@aura3d/animation";
|
|
17
|
-
|
|
18
|
-
export interface EventMarkerView {
|
|
19
|
-
readonly id: string;
|
|
20
|
-
readonly time: number;
|
|
21
|
-
readonly duration?: number;
|
|
22
|
-
readonly type: AnimationClipEventType;
|
|
23
|
-
readonly selected: boolean;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface EventTrackLane {
|
|
27
|
-
readonly name: string;
|
|
28
|
-
readonly type: AnimationClipEventType;
|
|
29
|
-
readonly markers: readonly EventMarkerView[];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface EventTrackEditorOptions {
|
|
33
|
-
readonly clipId: string;
|
|
34
|
-
readonly duration: number;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface AddEventMarkerOptions {
|
|
38
|
-
readonly id?: string;
|
|
39
|
-
readonly type?: AnimationClipEventType;
|
|
40
|
-
/** Window length (seconds). >0 makes the marker an active-frame window (e.g. hitbox on/off). */
|
|
41
|
-
readonly duration?: number;
|
|
42
|
-
readonly payload?: unknown;
|
|
43
|
-
readonly tags?: readonly string[];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
interface LaneState {
|
|
47
|
-
type: AnimationClipEventType;
|
|
48
|
-
markers: AnimationClipEvent[];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Timeline event-track authoring controller. Deterministic: marker ids come from a monotonic
|
|
53
|
-
* counter (no `Date.now`/`Math.random`), so undo/redo and serialization are reproducible.
|
|
54
|
-
*/
|
|
55
|
-
export class EventTrackEditor {
|
|
56
|
-
readonly clipId: string;
|
|
57
|
-
duration: number;
|
|
58
|
-
private readonly order: string[] = [];
|
|
59
|
-
private readonly lanesMap = new Map<string, LaneState>();
|
|
60
|
-
private selectedId: string | undefined;
|
|
61
|
-
private idCounter = 0;
|
|
62
|
-
|
|
63
|
-
constructor(options: EventTrackEditorOptions) {
|
|
64
|
-
if (!Number.isFinite(options.duration) || options.duration < 0) {
|
|
65
|
-
throw new Error("EventTrackEditor duration must be a finite, non-negative number.");
|
|
66
|
-
}
|
|
67
|
-
this.clipId = options.clipId;
|
|
68
|
-
this.duration = options.duration;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
addTrack(name: string, type: AnimationClipEventType = "marker"): this {
|
|
72
|
-
if (!this.lanesMap.has(name)) {
|
|
73
|
-
this.lanesMap.set(name, { type, markers: [] });
|
|
74
|
-
this.order.push(name);
|
|
75
|
-
}
|
|
76
|
-
return this;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
removeTrack(name: string): this {
|
|
80
|
-
if (this.lanesMap.delete(name)) this.order.splice(this.order.indexOf(name), 1);
|
|
81
|
-
return this;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Add a marker to a lane (auto-creates the lane). Returns the new marker id. */
|
|
85
|
-
addMarker(trackName: string, time: number, options: AddEventMarkerOptions = {}): string {
|
|
86
|
-
if (!Number.isFinite(time) || time < 0) {
|
|
87
|
-
throw new Error("Event marker time must be a finite, non-negative number.");
|
|
88
|
-
}
|
|
89
|
-
this.addTrack(trackName, options.type);
|
|
90
|
-
const lane = this.lanesMap.get(trackName)!;
|
|
91
|
-
const id = options.id ?? `${trackName}#${this.idCounter++}`;
|
|
92
|
-
lane.markers.push({
|
|
93
|
-
id,
|
|
94
|
-
name: trackName,
|
|
95
|
-
type: options.type ?? lane.type,
|
|
96
|
-
time,
|
|
97
|
-
...(options.duration !== undefined ? { duration: options.duration } : {}),
|
|
98
|
-
...(options.payload !== undefined ? { payload: options.payload } : {}),
|
|
99
|
-
...(options.tags !== undefined ? { tags: options.tags } : {})
|
|
100
|
-
});
|
|
101
|
-
lane.markers.sort((a, b) => a.time - b.time);
|
|
102
|
-
return id;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** Move an existing marker to a new time. */
|
|
106
|
-
moveMarker(trackName: string, markerId: string, time: number): this {
|
|
107
|
-
if (!Number.isFinite(time) || time < 0) {
|
|
108
|
-
throw new Error("Event marker time must be a finite, non-negative number.");
|
|
109
|
-
}
|
|
110
|
-
const lane = this.lanesMap.get(trackName);
|
|
111
|
-
const index = lane?.markers.findIndex((m) => m.id === markerId) ?? -1;
|
|
112
|
-
if (!lane || index < 0) throw new Error(`Marker ${markerId} not found on track ${trackName}.`);
|
|
113
|
-
lane.markers[index] = { ...lane.markers[index]!, time };
|
|
114
|
-
lane.markers.sort((a, b) => a.time - b.time);
|
|
115
|
-
return this;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** Delete a marker. */
|
|
119
|
-
removeMarker(trackName: string, markerId: string): this {
|
|
120
|
-
const lane = this.lanesMap.get(trackName);
|
|
121
|
-
if (!lane) return this;
|
|
122
|
-
const index = lane.markers.findIndex((m) => m.id === markerId);
|
|
123
|
-
if (index >= 0) lane.markers.splice(index, 1);
|
|
124
|
-
if (this.selectedId === markerId) this.selectedId = undefined;
|
|
125
|
-
return this;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
select(markerId: string | undefined): this {
|
|
129
|
-
this.selectedId = markerId;
|
|
130
|
-
return this;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
markerCount(): number {
|
|
134
|
-
let count = 0;
|
|
135
|
-
for (const lane of this.lanesMap.values()) count += lane.markers.length;
|
|
136
|
-
return count;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** View-model for the timeline UI: lanes with marker positions + selection state. */
|
|
140
|
-
lanes(): readonly EventTrackLane[] {
|
|
141
|
-
return this.order.map((name) => {
|
|
142
|
-
const lane = this.lanesMap.get(name)!;
|
|
143
|
-
return {
|
|
144
|
-
name,
|
|
145
|
-
type: lane.type,
|
|
146
|
-
markers: lane.markers.map((m) => ({
|
|
147
|
-
id: m.id!,
|
|
148
|
-
time: m.time,
|
|
149
|
-
...(m.duration !== undefined ? { duration: m.duration } : {}),
|
|
150
|
-
type: m.type ?? lane.type,
|
|
151
|
-
selected: m.id === this.selectedId
|
|
152
|
-
}))
|
|
153
|
-
};
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/** Serialize to the canonical `animation-event-tracks/v1` shape. */
|
|
158
|
-
serialize(): SerializedAnimationEventTracks {
|
|
159
|
-
const tracks: AnimationEventTrack[] = this.order.map((name) => {
|
|
160
|
-
const lane = this.lanesMap.get(name)!;
|
|
161
|
-
return { name, type: lane.type, markers: lane.markers.map((m) => ({ ...m })) };
|
|
162
|
-
});
|
|
163
|
-
return { schema: "animation-event-tracks/v1", clipId: this.clipId, duration: this.duration, tracks };
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export function createEventTrackEditor(options: EventTrackEditorOptions): EventTrackEditor {
|
|
168
|
-
return new EventTrackEditor(options);
|
|
169
|
-
}
|
package/src/Gizmo.ts
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import type { Ray } from "@aura3d/math";
|
|
2
|
-
import type { CommandHistory } from "./CommandHistory";
|
|
3
|
-
import type { TransformTarget } from "./commands/TransformCommand";
|
|
4
|
-
|
|
5
|
-
export type GizmoAxis = "x" | "y" | "z" | "uniform";
|
|
6
|
-
export type GizmoPlaneAxis = "xy" | "xz" | "yz";
|
|
7
|
-
export type GizmoHandle = GizmoAxis | GizmoPlaneAxis;
|
|
8
|
-
export type GizmoSpaceMode = "world" | "local";
|
|
9
|
-
export type GizmoPivotMode = "center" | "active" | "individual";
|
|
10
|
-
|
|
11
|
-
export interface GizmoSettings {
|
|
12
|
-
readonly snapEnabled: boolean;
|
|
13
|
-
readonly positionSnap: number;
|
|
14
|
-
readonly rotationSnapDegrees: number;
|
|
15
|
-
readonly scaleSnap: number;
|
|
16
|
-
readonly spaceMode: GizmoSpaceMode;
|
|
17
|
-
readonly pivotMode: GizmoPivotMode;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export const DEFAULT_GIZMO_SETTINGS: GizmoSettings = {
|
|
21
|
-
snapEnabled: false,
|
|
22
|
-
positionSnap: 1,
|
|
23
|
-
rotationSnapDegrees: 15,
|
|
24
|
-
scaleSnap: 0.1,
|
|
25
|
-
spaceMode: "world",
|
|
26
|
-
pivotMode: "center"
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export interface GizmoHit {
|
|
30
|
-
readonly axis: GizmoHandle;
|
|
31
|
-
readonly distance: number;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface GizmoDrag {
|
|
35
|
-
readonly axis: GizmoHandle;
|
|
36
|
-
readonly delta: number;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export abstract class Gizmo {
|
|
40
|
-
enabled = true;
|
|
41
|
-
target?: TransformTarget;
|
|
42
|
-
protected settings: GizmoSettings = DEFAULT_GIZMO_SETTINGS;
|
|
43
|
-
|
|
44
|
-
constructor(protected readonly history: CommandHistory, settings: Partial<GizmoSettings> = {}) {
|
|
45
|
-
this.configure(settings);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
setTarget(target: TransformTarget | undefined): void {
|
|
49
|
-
this.target = target;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
configure(settings: Partial<GizmoSettings>): void {
|
|
53
|
-
this.settings = normalizeGizmoSettings({ ...this.settings, ...settings });
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
snapshotSettings(): GizmoSettings {
|
|
57
|
-
return this.settings;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
hitTest(_ray: Ray): GizmoHit | undefined {
|
|
61
|
-
return undefined;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
abstract drag(input: GizmoDrag): Promise<void>;
|
|
65
|
-
|
|
66
|
-
dispose(): void {
|
|
67
|
-
this.enabled = false;
|
|
68
|
-
this.target = undefined;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
protected snapPositionDelta(value: number): number {
|
|
72
|
-
return this.settings.snapEnabled ? snap(value, this.settings.positionSnap) : value;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
protected snapRotationDelta(radians: number): number {
|
|
76
|
-
return this.settings.snapEnabled ? snap(radians, this.settings.rotationSnapDegrees * Math.PI / 180) : radians;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
protected snapScaleFactor(value: number): number {
|
|
80
|
-
return this.settings.snapEnabled ? snap(value, this.settings.scaleSnap) : value;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function normalizeGizmoSettings(settings: Partial<GizmoSettings> = {}): GizmoSettings {
|
|
85
|
-
return {
|
|
86
|
-
snapEnabled: settings.snapEnabled ?? DEFAULT_GIZMO_SETTINGS.snapEnabled,
|
|
87
|
-
positionSnap: positive(settings.positionSnap, DEFAULT_GIZMO_SETTINGS.positionSnap),
|
|
88
|
-
rotationSnapDegrees: positive(settings.rotationSnapDegrees, DEFAULT_GIZMO_SETTINGS.rotationSnapDegrees),
|
|
89
|
-
scaleSnap: positive(settings.scaleSnap, DEFAULT_GIZMO_SETTINGS.scaleSnap),
|
|
90
|
-
spaceMode: settings.spaceMode ?? DEFAULT_GIZMO_SETTINGS.spaceMode,
|
|
91
|
-
pivotMode: settings.pivotMode ?? DEFAULT_GIZMO_SETTINGS.pivotMode
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function positive(value: number | undefined, fallback: number): number {
|
|
96
|
-
return Number.isFinite(value) && Number(value) > 0 ? Number(value) : fallback;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function snap(value: number, increment: number): number {
|
|
100
|
-
return Math.round(value / increment) * increment;
|
|
101
|
-
}
|
package/src/HierarchyModel.ts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import type { SelectionId } from "./Selection";
|
|
2
|
-
|
|
3
|
-
export interface HierarchyLikeNode {
|
|
4
|
-
readonly id?: SelectionId;
|
|
5
|
-
readonly name?: string;
|
|
6
|
-
readonly visible?: boolean;
|
|
7
|
-
readonly children?: readonly HierarchyLikeNode[];
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface HierarchyNodeDescriptor {
|
|
11
|
-
readonly id: SelectionId;
|
|
12
|
-
readonly name: string;
|
|
13
|
-
readonly visible: boolean;
|
|
14
|
-
readonly selected: boolean;
|
|
15
|
-
readonly depth: number;
|
|
16
|
-
readonly childCount: number;
|
|
17
|
-
readonly children: readonly HierarchyNodeDescriptor[];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export class HierarchyModel {
|
|
21
|
-
describe(root: HierarchyLikeNode, selectedIds: ReadonlySet<SelectionId> = new Set()): HierarchyNodeDescriptor {
|
|
22
|
-
return describeNode(root, selectedIds, 0, "root");
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
flatten(root: HierarchyLikeNode, selectedIds: ReadonlySet<SelectionId> = new Set()): readonly HierarchyNodeDescriptor[] {
|
|
26
|
-
const output: HierarchyNodeDescriptor[] = [];
|
|
27
|
-
const visit = (node: HierarchyNodeDescriptor): void => {
|
|
28
|
-
output.push(node);
|
|
29
|
-
for (const child of node.children) {
|
|
30
|
-
visit(child);
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
visit(this.describe(root, selectedIds));
|
|
34
|
-
return output;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function describeNode(
|
|
39
|
-
node: HierarchyLikeNode,
|
|
40
|
-
selectedIds: ReadonlySet<SelectionId>,
|
|
41
|
-
depth: number,
|
|
42
|
-
fallbackId: SelectionId
|
|
43
|
-
): HierarchyNodeDescriptor {
|
|
44
|
-
const id = node.id ?? fallbackId;
|
|
45
|
-
const name = node.name ?? String(id);
|
|
46
|
-
const visible = node.visible ?? true;
|
|
47
|
-
const children = (node.children ?? []).map((child, index) => describeNode(child, selectedIds, depth + 1, `${id}:${index}`));
|
|
48
|
-
return {
|
|
49
|
-
id,
|
|
50
|
-
name,
|
|
51
|
-
visible,
|
|
52
|
-
selected: selectedIds.has(id),
|
|
53
|
-
depth,
|
|
54
|
-
childCount: children.length,
|
|
55
|
-
children
|
|
56
|
-
};
|
|
57
|
-
}
|
package/src/InspectorModel.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { SetPropertyCommand } from "./commands/SetPropertyCommand";
|
|
2
|
-
|
|
3
|
-
export type InspectorEditableValue = number | string | boolean;
|
|
4
|
-
|
|
5
|
-
export interface InspectorProperty {
|
|
6
|
-
readonly path: readonly string[];
|
|
7
|
-
readonly type: "number" | "string" | "boolean" | "object";
|
|
8
|
-
readonly value: unknown;
|
|
9
|
-
readonly editable: boolean;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export class InspectorModel {
|
|
13
|
-
describe(target: object): readonly InspectorProperty[] {
|
|
14
|
-
return this.describeObject(target);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
createSetPropertyCommand<T extends object>(
|
|
18
|
-
target: T,
|
|
19
|
-
path: readonly string[],
|
|
20
|
-
value: InspectorEditableValue
|
|
21
|
-
): SetPropertyCommand<T> {
|
|
22
|
-
const property = this.describe(target).find((descriptor) => samePath(descriptor.path, path));
|
|
23
|
-
if (!property) {
|
|
24
|
-
throw new Error(`Inspector property path does not exist: ${path.join(".")}`);
|
|
25
|
-
}
|
|
26
|
-
if (!property.editable) {
|
|
27
|
-
throw new Error(`Inspector property is not editable: ${path.join(".")}`);
|
|
28
|
-
}
|
|
29
|
-
this.assertValueMatchesProperty(property, value);
|
|
30
|
-
return new SetPropertyCommand(target, path, value);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
private describeObject(target: object, prefix: readonly string[] = []): InspectorProperty[] {
|
|
34
|
-
const properties: InspectorProperty[] = [];
|
|
35
|
-
for (const [key, value] of Object.entries(target)) {
|
|
36
|
-
const path = [...prefix, key];
|
|
37
|
-
const type = this.propertyType(value);
|
|
38
|
-
properties.push({
|
|
39
|
-
path,
|
|
40
|
-
type,
|
|
41
|
-
value,
|
|
42
|
-
editable: type !== "object"
|
|
43
|
-
});
|
|
44
|
-
if (type === "object" && typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
45
|
-
properties.push(...this.describeObject(value as object, path));
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return properties;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
private propertyType(value: unknown): InspectorProperty["type"] {
|
|
52
|
-
const valueType = typeof value;
|
|
53
|
-
if (valueType === "number" || valueType === "string" || valueType === "boolean") {
|
|
54
|
-
return valueType;
|
|
55
|
-
}
|
|
56
|
-
return "object";
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
private assertValueMatchesProperty(property: InspectorProperty, value: InspectorEditableValue): void {
|
|
60
|
-
if (property.type === "number" && !Number.isFinite(value)) {
|
|
61
|
-
throw new Error(`Inspector property ${property.path.join(".")} expected a finite number.`);
|
|
62
|
-
}
|
|
63
|
-
if (typeof value !== property.type) {
|
|
64
|
-
throw new Error(`Inspector property ${property.path.join(".")} expected ${property.type}, got ${typeof value}.`);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function samePath(left: readonly string[], right: readonly string[]): boolean {
|
|
70
|
-
return left.length === right.length && left.every((segment, index) => segment === right[index]);
|
|
71
|
-
}
|
package/src/KeyframeEditor.ts
DELETED
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
import type { Command } from "./Command";
|
|
2
|
-
import { CommandHistory } from "./CommandHistory";
|
|
3
|
-
import type { TimelineClip } from "./TimelineModel";
|
|
4
|
-
|
|
5
|
-
export type TimelineKeyframeValue = number | string | boolean;
|
|
6
|
-
export type TimelineKeyframeInterpolation = "hold" | "linear" | "bezier";
|
|
7
|
-
|
|
8
|
-
export interface TimelineBezierHandle {
|
|
9
|
-
readonly time: number;
|
|
10
|
-
readonly value: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface TimelineKeyframe {
|
|
14
|
-
readonly id: string;
|
|
15
|
-
readonly propertyPath: string;
|
|
16
|
-
readonly time: number;
|
|
17
|
-
readonly value: TimelineKeyframeValue;
|
|
18
|
-
readonly interpolation: TimelineKeyframeInterpolation;
|
|
19
|
-
readonly inHandle?: TimelineBezierHandle;
|
|
20
|
-
readonly outHandle?: TimelineBezierHandle;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface CreateTimelineKeyframeOptions {
|
|
24
|
-
readonly id?: string;
|
|
25
|
-
readonly propertyPath: string;
|
|
26
|
-
readonly time: number;
|
|
27
|
-
readonly value: TimelineKeyframeValue;
|
|
28
|
-
readonly interpolation?: TimelineKeyframeInterpolation;
|
|
29
|
-
readonly inHandle?: TimelineBezierHandle;
|
|
30
|
-
readonly outHandle?: TimelineBezierHandle;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface PasteTimelineKeyframesOptions {
|
|
34
|
-
readonly timeOffset?: number;
|
|
35
|
-
readonly idPrefix?: string;
|
|
36
|
-
readonly propertyPath?: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const KEYFRAME_PROPERTY = "auraKeyframes";
|
|
40
|
-
|
|
41
|
-
export function readTimelineKeyframes(clip: TimelineClip): readonly TimelineKeyframe[] {
|
|
42
|
-
const encoded = clip.properties[KEYFRAME_PROPERTY];
|
|
43
|
-
if (typeof encoded !== "string" || encoded.trim().length === 0) return [];
|
|
44
|
-
const parsed = JSON.parse(encoded) as TimelineKeyframe[];
|
|
45
|
-
if (!Array.isArray(parsed)) {
|
|
46
|
-
throw new Error(`Timeline clip keyframes must decode to an array: ${clip.id}`);
|
|
47
|
-
}
|
|
48
|
-
return parsed.map(normalizeKeyframe).sort(compareKeyframes);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function encodeTimelineKeyframes(keyframes: readonly TimelineKeyframe[]): string {
|
|
52
|
-
return JSON.stringify([...keyframes].map(normalizeKeyframe).sort(compareKeyframes));
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function timelineKeyframesByProperty(clip: TimelineClip, propertyPath: string): readonly TimelineKeyframe[] {
|
|
56
|
-
return readTimelineKeyframes(clip).filter((keyframe) => keyframe.propertyPath === propertyPath);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export class KeyframeEditor {
|
|
60
|
-
constructor(private readonly history = new CommandHistory()) {}
|
|
61
|
-
|
|
62
|
-
get commandHistory(): CommandHistory {
|
|
63
|
-
return this.history;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
keyframes(clip: TimelineClip, propertyPath?: string): readonly TimelineKeyframe[] {
|
|
67
|
-
const keyframes = readTimelineKeyframes(clip);
|
|
68
|
-
return propertyPath ? keyframes.filter((keyframe) => keyframe.propertyPath === propertyPath) : keyframes;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async addKeyframe(clip: TimelineClip, options: CreateTimelineKeyframeOptions): Promise<TimelineKeyframe> {
|
|
72
|
-
const keyframe = normalizeKeyframe({
|
|
73
|
-
id: options.id ?? stableKeyframeId(options.propertyPath, options.time),
|
|
74
|
-
propertyPath: options.propertyPath,
|
|
75
|
-
time: options.time,
|
|
76
|
-
value: options.value,
|
|
77
|
-
interpolation: options.interpolation ?? "linear",
|
|
78
|
-
inHandle: options.inHandle,
|
|
79
|
-
outHandle: options.outHandle
|
|
80
|
-
});
|
|
81
|
-
const next = [...readTimelineKeyframes(clip).filter((candidate) => candidate.id !== keyframe.id), keyframe];
|
|
82
|
-
await this.applyKeyframes(clip, next, `Add keyframe ${keyframe.id}`);
|
|
83
|
-
return keyframe;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async deleteKeyframes(clip: TimelineClip, ids: readonly string[]): Promise<void> {
|
|
87
|
-
const remove = new Set(ids);
|
|
88
|
-
await this.applyKeyframes(clip, readTimelineKeyframes(clip).filter((keyframe) => !remove.has(keyframe.id)), `Delete ${ids.length} keyframes`);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async moveKeyframes(clip: TimelineClip, ids: readonly string[], deltaTime: number): Promise<readonly TimelineKeyframe[]> {
|
|
92
|
-
assertFinite(deltaTime, "Keyframe deltaTime");
|
|
93
|
-
const move = new Set(ids);
|
|
94
|
-
const next = readTimelineKeyframes(clip).map((keyframe) => move.has(keyframe.id)
|
|
95
|
-
? normalizeKeyframe({ ...keyframe, time: Math.max(0, keyframe.time + deltaTime) })
|
|
96
|
-
: keyframe);
|
|
97
|
-
await this.applyKeyframes(clip, next, `Move ${ids.length} keyframes`);
|
|
98
|
-
return next.filter((keyframe) => move.has(keyframe.id));
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async scaleKeyframes(clip: TimelineClip, ids: readonly string[], pivotTime: number, scale: number): Promise<readonly TimelineKeyframe[]> {
|
|
102
|
-
assertFinite(pivotTime, "Keyframe pivotTime");
|
|
103
|
-
assertFinite(scale, "Keyframe scale");
|
|
104
|
-
if (scale <= 0) throw new RangeError("Keyframe scale must be positive.");
|
|
105
|
-
const scaleIds = new Set(ids);
|
|
106
|
-
const next = readTimelineKeyframes(clip).map((keyframe) => scaleIds.has(keyframe.id)
|
|
107
|
-
? normalizeKeyframe({ ...keyframe, time: Math.max(0, pivotTime + (keyframe.time - pivotTime) * scale) })
|
|
108
|
-
: keyframe);
|
|
109
|
-
await this.applyKeyframes(clip, next, `Scale ${ids.length} keyframes`);
|
|
110
|
-
return next.filter((keyframe) => scaleIds.has(keyframe.id));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
copyKeyframes(clip: TimelineClip, ids: readonly string[]): readonly TimelineKeyframe[] {
|
|
114
|
-
const copy = new Set(ids);
|
|
115
|
-
return readTimelineKeyframes(clip).filter((keyframe) => copy.has(keyframe.id)).map((keyframe) => ({ ...keyframe }));
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async pasteKeyframes(
|
|
119
|
-
clip: TimelineClip,
|
|
120
|
-
keyframes: readonly TimelineKeyframe[],
|
|
121
|
-
options: PasteTimelineKeyframesOptions = {}
|
|
122
|
-
): Promise<readonly TimelineKeyframe[]> {
|
|
123
|
-
const timeOffset = options.timeOffset ?? 0;
|
|
124
|
-
assertFinite(timeOffset, "Paste keyframe timeOffset");
|
|
125
|
-
const prefix = options.idPrefix ?? "pasted";
|
|
126
|
-
const pasted = keyframes.map((keyframe, index) => normalizeKeyframe({
|
|
127
|
-
...keyframe,
|
|
128
|
-
id: `${prefix}-${keyframe.id}-${index}`,
|
|
129
|
-
propertyPath: options.propertyPath ?? keyframe.propertyPath,
|
|
130
|
-
time: Math.max(0, keyframe.time + timeOffset)
|
|
131
|
-
}));
|
|
132
|
-
await this.applyKeyframes(clip, [...readTimelineKeyframes(clip), ...pasted], `Paste ${pasted.length} keyframes`);
|
|
133
|
-
return pasted;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async replaceKeyframes(clip: TimelineClip, keyframes: readonly TimelineKeyframe[], label = "Replace keyframes"): Promise<void> {
|
|
137
|
-
await this.applyKeyframes(clip, keyframes, label);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
private async applyKeyframes(clip: TimelineClip, keyframes: readonly TimelineKeyframe[], name: string): Promise<void> {
|
|
141
|
-
await this.history.execute(new SetClipKeyframesCommand(clip, keyframes, name));
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
class SetClipKeyframesCommand implements Command {
|
|
146
|
-
readonly name: string;
|
|
147
|
-
private readonly before: readonly TimelineKeyframe[];
|
|
148
|
-
private readonly after: readonly TimelineKeyframe[];
|
|
149
|
-
|
|
150
|
-
constructor(private readonly clip: TimelineClip, keyframes: readonly TimelineKeyframe[], name: string) {
|
|
151
|
-
this.name = name;
|
|
152
|
-
this.before = readTimelineKeyframes(clip);
|
|
153
|
-
this.after = [...keyframes].map(normalizeKeyframe).sort(compareKeyframes);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
execute(): void {
|
|
157
|
-
this.write(this.after);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
undo(): void {
|
|
161
|
-
this.write(this.before);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
private write(keyframes: readonly TimelineKeyframe[]): void {
|
|
165
|
-
if (keyframes.length === 0) {
|
|
166
|
-
delete this.clip.properties[KEYFRAME_PROPERTY];
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
this.clip.properties[KEYFRAME_PROPERTY] = encodeTimelineKeyframes(keyframes);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function normalizeKeyframe(keyframe: TimelineKeyframe): TimelineKeyframe {
|
|
174
|
-
if (!keyframe.id.trim()) throw new Error("Timeline keyframe id is required.");
|
|
175
|
-
if (!keyframe.propertyPath.trim()) throw new Error("Timeline keyframe propertyPath is required.");
|
|
176
|
-
assertFinite(keyframe.time, "Timeline keyframe time");
|
|
177
|
-
if (keyframe.time < 0) throw new RangeError("Timeline keyframe time must be non-negative.");
|
|
178
|
-
validateKeyframeValue(keyframe.value);
|
|
179
|
-
validateHandle(keyframe.inHandle, "inHandle");
|
|
180
|
-
validateHandle(keyframe.outHandle, "outHandle");
|
|
181
|
-
return {
|
|
182
|
-
id: keyframe.id,
|
|
183
|
-
propertyPath: keyframe.propertyPath,
|
|
184
|
-
time: Number(keyframe.time.toFixed(4)),
|
|
185
|
-
value: keyframe.value,
|
|
186
|
-
interpolation: keyframe.interpolation,
|
|
187
|
-
inHandle: keyframe.inHandle,
|
|
188
|
-
outHandle: keyframe.outHandle
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function validateKeyframeValue(value: TimelineKeyframeValue): void {
|
|
193
|
-
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
194
|
-
throw new Error("Timeline keyframe numeric values must be finite.");
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function validateHandle(handle: TimelineBezierHandle | undefined, label: string): void {
|
|
199
|
-
if (!handle) return;
|
|
200
|
-
assertFinite(handle.time, `Timeline keyframe ${label}.time`);
|
|
201
|
-
assertFinite(handle.value, `Timeline keyframe ${label}.value`);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function assertFinite(value: number, label: string): void {
|
|
205
|
-
if (!Number.isFinite(value)) throw new RangeError(`${label} must be finite.`);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function compareKeyframes(left: TimelineKeyframe, right: TimelineKeyframe): number {
|
|
209
|
-
return left.time - right.time || left.propertyPath.localeCompare(right.propertyPath) || left.id.localeCompare(right.id);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function stableKeyframeId(propertyPath: string, time: number): string {
|
|
213
|
-
return `key-${propertyPath.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "value"}-${Number(time.toFixed(4)).toString().replace(".", "-")}`;
|
|
214
|
-
}
|