2308 lines
73 KiB
JavaScript
2308 lines
73 KiB
JavaScript
// Actions module - extracted from main.js
|
|
// This module contains all the undo/redo-able actions for the application
|
|
|
|
// Imports for dependencies
|
|
import { context, pointerList } from '../state.js';
|
|
import { Shape } from '../models/shapes.js';
|
|
import { Bezier } from '../bezier.js';
|
|
import {
|
|
Keyframe,
|
|
AnimationCurve,
|
|
AnimationData,
|
|
Frame
|
|
} from '../models/animation.js';
|
|
import { GraphicsObject } from '../models/graphics-object.js';
|
|
import { Layer, AudioTrack } from '../models/layer.js';
|
|
import {
|
|
arraysAreEqual,
|
|
lerp,
|
|
lerpColor,
|
|
generateWaveform,
|
|
signedAngleBetweenVectors,
|
|
rotateAroundPointIncremental,
|
|
getRotatedBoundingBox,
|
|
growBoundingBox
|
|
} from '../utils.js';
|
|
|
|
// UUID generation function (keeping local version for now)
|
|
function uuidv4() {
|
|
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) =>
|
|
(
|
|
+c ^
|
|
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))
|
|
).toString(16),
|
|
);
|
|
}
|
|
|
|
// Dependencies that will be injected
|
|
let undoStack = null;
|
|
let redoStack = null;
|
|
let updateMenu = null;
|
|
let updateLayers = null;
|
|
let updateUI = null;
|
|
let updateInfopanel = null;
|
|
let invoke = null;
|
|
let config = null;
|
|
|
|
/**
|
|
* Initialize the actions module with required dependencies
|
|
* @param {Object} deps - Dependencies object
|
|
* @param {Array} deps.undoStack - Reference to the undo stack
|
|
* @param {Array} deps.redoStack - Reference to the redo stack
|
|
* @param {Function} deps.updateMenu - Function to update the menu
|
|
* @param {Function} deps.updateLayers - Function to update layers UI
|
|
* @param {Function} deps.updateUI - Function to update main UI
|
|
* @param {Function} deps.updateInfopanel - Function to update info panel
|
|
* @param {Function} deps.invoke - Tauri invoke function
|
|
* @param {Object} deps.config - Application config object
|
|
*/
|
|
export function initializeActions(deps) {
|
|
undoStack = deps.undoStack;
|
|
redoStack = deps.redoStack;
|
|
updateMenu = deps.updateMenu;
|
|
updateLayers = deps.updateLayers;
|
|
updateUI = deps.updateUI;
|
|
updateInfopanel = deps.updateInfopanel;
|
|
invoke = deps.invoke;
|
|
config = deps.config;
|
|
}
|
|
|
|
export const actions = {
|
|
addShape: {
|
|
create: (parent, shape, ctx) => {
|
|
// parent should be a GraphicsObject
|
|
if (!parent.activeLayer) return;
|
|
if (shape.curves.length == 0) return;
|
|
redoStack.length = 0; // Clear redo stack
|
|
let serializableCurves = [];
|
|
for (let curve of shape.curves) {
|
|
serializableCurves.push({ points: curve.points, color: curve.color });
|
|
}
|
|
let c = {
|
|
...context,
|
|
...ctx,
|
|
};
|
|
let action = {
|
|
parent: parent.idx,
|
|
layer: parent.activeLayer.idx,
|
|
curves: serializableCurves,
|
|
startx: shape.startx,
|
|
starty: shape.starty,
|
|
context: {
|
|
fillShape: c.fillShape,
|
|
strokeShape: c.strokeShape,
|
|
fillStyle: c.fillStyle,
|
|
sendToBack: c.sendToBack,
|
|
lineWidth: c.lineWidth,
|
|
},
|
|
uuid: uuidv4(),
|
|
time: parent.currentTime, // Use currentTime instead of currentFrame
|
|
};
|
|
undoStack.push({ name: "addShape", action: action });
|
|
actions.addShape.execute(action);
|
|
updateMenu();
|
|
updateLayers();
|
|
},
|
|
execute: (action) => {
|
|
let layer = pointerList[action.layer];
|
|
let curvesList = action.curves;
|
|
let cxt = {
|
|
...context,
|
|
...action.context,
|
|
};
|
|
let shape = new Shape(action.startx, action.starty, cxt, layer, action.uuid);
|
|
for (let curve of curvesList) {
|
|
shape.addCurve(
|
|
new Bezier(
|
|
curve.points[0].x,
|
|
curve.points[0].y,
|
|
curve.points[1].x,
|
|
curve.points[1].y,
|
|
curve.points[2].x,
|
|
curve.points[2].y,
|
|
curve.points[3].x,
|
|
curve.points[3].y,
|
|
).setColor(curve.color),
|
|
);
|
|
}
|
|
let shapes = shape.update();
|
|
for (let newShape of shapes) {
|
|
// Add shape to layer's shapes array
|
|
layer.shapes.push(newShape);
|
|
|
|
// Determine zOrder based on sendToBack
|
|
let zOrder;
|
|
if (cxt.sendToBack) {
|
|
// Insert at back (zOrder 0), shift all other shapes up
|
|
zOrder = 0;
|
|
// Increment zOrder for all existing shapes
|
|
for (let existingShape of layer.shapes) {
|
|
if (existingShape !== newShape) {
|
|
let existingZOrderCurve = layer.animationData.curves[`shape.${existingShape.shapeId}.zOrder`];
|
|
if (existingZOrderCurve) {
|
|
// Find keyframe at this time and increment it
|
|
for (let kf of existingZOrderCurve.keyframes) {
|
|
if (kf.time === action.time) {
|
|
kf.value += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Insert at front (max zOrder + 1)
|
|
zOrder = layer.shapes.length - 1;
|
|
}
|
|
|
|
// Add keyframes to AnimationData for this shape
|
|
// Use shapeId (not idx) so that multiple versions share curves
|
|
let existsKeyframe = new Keyframe(action.time, 1, "hold");
|
|
layer.animationData.addKeyframe(`shape.${newShape.shapeId}.exists`, existsKeyframe);
|
|
|
|
let zOrderKeyframe = new Keyframe(action.time, zOrder, "hold");
|
|
layer.animationData.addKeyframe(`shape.${newShape.shapeId}.zOrder`, zOrderKeyframe);
|
|
|
|
let shapeIndexKeyframe = new Keyframe(action.time, 0, "linear");
|
|
layer.animationData.addKeyframe(`shape.${newShape.shapeId}.shapeIndex`, shapeIndexKeyframe);
|
|
}
|
|
},
|
|
rollback: (action) => {
|
|
let layer = pointerList[action.layer];
|
|
let shape = pointerList[action.uuid];
|
|
|
|
// Remove shape from layer's shapes array
|
|
let shapeIndex = layer.shapes.indexOf(shape);
|
|
if (shapeIndex !== -1) {
|
|
layer.shapes.splice(shapeIndex, 1);
|
|
}
|
|
|
|
// Remove keyframes from AnimationData (use shapeId not idx)
|
|
delete layer.animationData.curves[`shape.${shape.shapeId}.exists`];
|
|
delete layer.animationData.curves[`shape.${shape.shapeId}.zOrder`];
|
|
delete layer.animationData.curves[`shape.${shape.shapeId}.shapeIndex`];
|
|
|
|
delete pointerList[action.uuid];
|
|
},
|
|
},
|
|
editShape: {
|
|
create: (shape, newCurves) => {
|
|
redoStack.length = 0; // Clear redo stack
|
|
let serializableNewCurves = [];
|
|
for (let curve of newCurves) {
|
|
serializableNewCurves.push({
|
|
points: curve.points,
|
|
color: curve.color,
|
|
});
|
|
}
|
|
let serializableOldCurves = [];
|
|
for (let curve of shape.curves) {
|
|
serializableOldCurves.push({ points: curve.points });
|
|
}
|
|
let action = {
|
|
shape: shape.idx,
|
|
oldCurves: serializableOldCurves,
|
|
newCurves: serializableNewCurves,
|
|
};
|
|
undoStack.push({ name: "editShape", action: action });
|
|
actions.editShape.execute(action);
|
|
},
|
|
execute: (action) => {
|
|
let shape = pointerList[action.shape];
|
|
let curvesList = action.newCurves;
|
|
shape.curves = [];
|
|
for (let curve of curvesList) {
|
|
shape.addCurve(
|
|
new Bezier(
|
|
curve.points[0].x,
|
|
curve.points[0].y,
|
|
curve.points[1].x,
|
|
curve.points[1].y,
|
|
curve.points[2].x,
|
|
curve.points[2].y,
|
|
curve.points[3].x,
|
|
curve.points[3].y,
|
|
).setColor(curve.color),
|
|
);
|
|
}
|
|
shape.update();
|
|
updateUI();
|
|
},
|
|
rollback: (action) => {
|
|
let shape = pointerList[action.shape];
|
|
let curvesList = action.oldCurves;
|
|
shape.curves = [];
|
|
for (let curve of curvesList) {
|
|
shape.addCurve(
|
|
new Bezier(
|
|
curve.points[0].x,
|
|
curve.points[0].y,
|
|
curve.points[1].x,
|
|
curve.points[1].y,
|
|
curve.points[2].x,
|
|
curve.points[2].y,
|
|
curve.points[3].x,
|
|
curve.points[3].y,
|
|
).setColor(curve.color),
|
|
);
|
|
}
|
|
shape.update();
|
|
},
|
|
},
|
|
colorShape: {
|
|
create: (shape, color) => {
|
|
redoStack.length = 0; // Clear redo stack
|
|
let action = {
|
|
shape: shape.idx,
|
|
oldColor: shape.fillStyle,
|
|
newColor: color,
|
|
};
|
|
undoStack.push({ name: "colorShape", action: action });
|
|
actions.colorShape.execute(action);
|
|
updateMenu();
|
|
},
|
|
execute: (action) => {
|
|
let shape = pointerList[action.shape];
|
|
shape.fillStyle = action.newColor;
|
|
},
|
|
rollback: (action) => {
|
|
let shape = pointerList[action.shape];
|
|
shape.fillStyle = action.oldColor;
|
|
},
|
|
},
|
|
addImageObject: {
|
|
create: (x, y, imgsrc, ix, parent) => {
|
|
redoStack.length = 0; // Clear redo stack
|
|
let action = {
|
|
shapeUuid: uuidv4(),
|
|
objectUuid: uuidv4(),
|
|
x: x,
|
|
y: y,
|
|
src: imgsrc,
|
|
ix: ix,
|
|
parent: parent.idx,
|
|
};
|
|
undoStack.push({ name: "addImageObject", action: action });
|
|
actions.addImageObject.execute(action);
|
|
updateMenu();
|
|
},
|
|
execute: async (action) => {
|
|
let imageObject = new GraphicsObject(action.objectUuid);
|
|
function loadImage(src) {
|
|
return new Promise((resolve, reject) => {
|
|
let img = new Image();
|
|
img.onload = () => resolve(img); // Resolve the promise with the image once loaded
|
|
img.onerror = (err) => reject(err); // Reject the promise if there's an error loading the image
|
|
img.src = src; // Start loading the image
|
|
});
|
|
}
|
|
let img = await loadImage(action.src);
|
|
// img.onload = function() {
|
|
let ct = {
|
|
...context,
|
|
fillImage: img,
|
|
strokeShape: false,
|
|
};
|
|
let imageShape = new Shape(0, 0, ct, imageObject.activeLayer, action.shapeUuid);
|
|
imageShape.addLine(img.width, 0);
|
|
imageShape.addLine(img.width, img.height);
|
|
imageShape.addLine(0, img.height);
|
|
imageShape.addLine(0, 0);
|
|
imageShape.update();
|
|
imageShape.fillImage = img;
|
|
imageShape.filled = true;
|
|
|
|
// Add shape to layer using new AnimationData-aware method
|
|
const time = imageObject.currentTime || 0;
|
|
imageObject.activeLayer.addShape(imageShape, time);
|
|
let parent = pointerList[action.parent];
|
|
parent.addObject(
|
|
imageObject,
|
|
action.x - img.width / 2 + 20 * action.ix,
|
|
action.y - img.height / 2 + 20 * action.ix,
|
|
);
|
|
updateUI();
|
|
// }
|
|
// img.src = action.src
|
|
},
|
|
rollback: (action) => {
|
|
let shape = pointerList[action.shapeUuid];
|
|
let object = pointerList[action.objectUuid];
|
|
let parent = pointerList[action.parent];
|
|
object.getFrame(0).removeShape(shape);
|
|
delete pointerList[action.shapeUuid];
|
|
parent.removeChild(object);
|
|
delete pointerList[action.objectUuid];
|
|
let selectIndex = context.selection.indexOf(object);
|
|
if (selectIndex >= 0) {
|
|
context.selection.splice(selectIndex, 1);
|
|
}
|
|
},
|
|
},
|
|
addAudio: {
|
|
create: (filePath, object, audioname) => {
|
|
redoStack.length = 0;
|
|
let action = {
|
|
filePath: filePath,
|
|
audioname: audioname,
|
|
trackuuid: uuidv4(),
|
|
object: object.idx,
|
|
};
|
|
undoStack.push({ name: "addAudio", action: action });
|
|
actions.addAudio.execute(action);
|
|
updateMenu();
|
|
},
|
|
execute: async (action) => {
|
|
// Create new AudioTrack with DAW backend
|
|
let newAudioTrack = new AudioTrack(action.trackuuid, action.audioname);
|
|
let object = pointerList[action.object];
|
|
|
|
// Add placeholder clip immediately so user sees feedback
|
|
newAudioTrack.clips.push({
|
|
clipId: 0,
|
|
poolIndex: 0,
|
|
name: 'Loading...',
|
|
startTime: 0,
|
|
duration: 10,
|
|
offset: 0,
|
|
loading: true
|
|
});
|
|
|
|
// Add track to object immediately
|
|
object.audioTracks.push(newAudioTrack);
|
|
|
|
// Update UI to show placeholder
|
|
updateLayers();
|
|
if (context.timelineWidget) {
|
|
context.timelineWidget.requestRedraw();
|
|
}
|
|
|
|
// Load audio asynchronously and update clip
|
|
try {
|
|
// Initialize track in backend and load audio file
|
|
await newAudioTrack.initializeTrack();
|
|
const metadata = await newAudioTrack.loadAudioFile(action.filePath);
|
|
|
|
// Use actual duration from the audio file metadata
|
|
const duration = metadata.duration;
|
|
|
|
// Replace placeholder clip with real clip
|
|
newAudioTrack.clips[0] = {
|
|
clipId: 0,
|
|
poolIndex: metadata.pool_index,
|
|
name: action.audioname,
|
|
startTime: 0,
|
|
duration: duration,
|
|
offset: 0,
|
|
loading: false,
|
|
waveform: metadata.waveform // Store waveform data for rendering
|
|
};
|
|
|
|
// Add clip to backend (call backend directly to avoid duplicate push)
|
|
const { invoke } = window.__TAURI__.core
|
|
await invoke('audio_add_clip', {
|
|
trackId: newAudioTrack.audioTrackId,
|
|
poolIndex: metadata.pool_index,
|
|
startTime: 0,
|
|
duration: duration,
|
|
offset: 0
|
|
});
|
|
|
|
// Update UI with real clip data
|
|
updateLayers();
|
|
if (context.timelineWidget) {
|
|
context.timelineWidget.requestRedraw();
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load audio:', error);
|
|
// Update clip to show error
|
|
newAudioTrack.clips[0].name = 'Error loading';
|
|
newAudioTrack.clips[0].loading = false;
|
|
if (context.timelineWidget) {
|
|
context.timelineWidget.requestRedraw();
|
|
}
|
|
}
|
|
},
|
|
rollback: (action) => {
|
|
let object = pointerList[action.object];
|
|
let track = pointerList[action.trackuuid];
|
|
object.audioTracks.splice(object.audioTracks.indexOf(track), 1);
|
|
updateLayers();
|
|
if (context.timelineWidget) {
|
|
context.timelineWidget.requestRedraw();
|
|
}
|
|
},
|
|
},
|
|
addMIDI: {
|
|
create: (filePath, object, midiname) => {
|
|
redoStack.length = 0;
|
|
let action = {
|
|
filePath: filePath,
|
|
midiname: midiname,
|
|
trackuuid: uuidv4(),
|
|
object: object.idx,
|
|
};
|
|
undoStack.push({ name: "addMIDI", action: action });
|
|
actions.addMIDI.execute(action);
|
|
updateMenu();
|
|
},
|
|
execute: async (action) => {
|
|
// Create new AudioTrack with type='midi' for MIDI files
|
|
let newMIDITrack = new AudioTrack(action.trackuuid, action.midiname, 'midi');
|
|
let object = pointerList[action.object];
|
|
|
|
// Note: MIDI tracks now use node-based instruments via instrument_graph
|
|
const { invoke } = window.__TAURI__.core;
|
|
|
|
// Add placeholder clip immediately so user sees feedback
|
|
newMIDITrack.clips.push({
|
|
clipId: 0,
|
|
name: 'Loading...',
|
|
startTime: 0,
|
|
duration: 10,
|
|
loading: true
|
|
});
|
|
|
|
// Add track to object immediately
|
|
object.audioTracks.push(newMIDITrack);
|
|
|
|
// Update UI to show placeholder
|
|
updateLayers();
|
|
if (context.timelineWidget) {
|
|
context.timelineWidget.requestRedraw();
|
|
}
|
|
|
|
// Load MIDI file asynchronously and update clip
|
|
try {
|
|
// Initialize track in backend
|
|
await newMIDITrack.initializeTrack();
|
|
|
|
// Load MIDI file into the track
|
|
const metadata = await invoke('audio_load_midi_file', {
|
|
trackId: newMIDITrack.audioTrackId,
|
|
path: action.filePath,
|
|
startTime: 0
|
|
});
|
|
|
|
// Replace placeholder clip with real clip including note data
|
|
newMIDITrack.clips[0] = {
|
|
clipId: 0,
|
|
name: action.midiname,
|
|
startTime: 0,
|
|
duration: metadata.duration,
|
|
notes: metadata.notes, // Store MIDI notes for visualization
|
|
loading: false
|
|
};
|
|
|
|
// Update UI with real clip data
|
|
updateLayers();
|
|
if (context.timelineWidget) {
|
|
context.timelineWidget.requestRedraw();
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load MIDI file:', error);
|
|
// Update clip to show error
|
|
newMIDITrack.clips[0].name = 'Error loading';
|
|
newMIDITrack.clips[0].loading = false;
|
|
if (context.timelineWidget) {
|
|
context.timelineWidget.requestRedraw();
|
|
}
|
|
}
|
|
},
|
|
rollback: (action) => {
|
|
let object = pointerList[action.object];
|
|
let track = pointerList[action.trackuuid];
|
|
object.audioTracks.splice(object.audioTracks.indexOf(track), 1);
|
|
updateLayers();
|
|
if (context.timelineWidget) {
|
|
context.timelineWidget.requestRedraw();
|
|
}
|
|
},
|
|
},
|
|
duplicateObject: {
|
|
create: (items) => {
|
|
redoStack.length = 0;
|
|
function deepCopyWithIdxMapping(obj, dictionary = {}) {
|
|
if (Array.isArray(obj)) {
|
|
return obj.map(item => deepCopyWithIdxMapping(item, dictionary));
|
|
}
|
|
if (obj === null || typeof obj !== 'object') {
|
|
return obj;
|
|
}
|
|
|
|
const newObj = {};
|
|
for (const key in obj) {
|
|
let value = obj[key];
|
|
|
|
if (key === 'idx' && !(value in dictionary)) {
|
|
dictionary[value] = uuidv4();
|
|
}
|
|
|
|
newObj[key] = value in dictionary ? dictionary[value] : value;
|
|
if (typeof newObj[key] === 'object' && newObj[key] !== null) {
|
|
newObj[key] = deepCopyWithIdxMapping(newObj[key], dictionary);
|
|
}
|
|
}
|
|
|
|
return newObj;
|
|
}
|
|
let action = {
|
|
items: deepCopyWithIdxMapping(items),
|
|
object: context.activeObject.idx,
|
|
layer: context.activeObject.activeLayer.idx,
|
|
time: context.activeObject.currentTime || 0,
|
|
uuid: uuidv4(),
|
|
};
|
|
undoStack.push({ name: "duplicateObject", action: action });
|
|
actions.duplicateObject.execute(action);
|
|
updateMenu();
|
|
},
|
|
execute: (action) => {
|
|
const object = pointerList[action.object];
|
|
const layer = pointerList[action.layer];
|
|
const time = action.time;
|
|
|
|
for (let item of action.items) {
|
|
if (item.type == "shape") {
|
|
const shape = Shape.fromJSON(item);
|
|
layer.addShape(shape, time);
|
|
} else if (item.type == "GraphicsObject") {
|
|
const newObj = GraphicsObject.fromJSON(item);
|
|
object.addObject(newObj);
|
|
}
|
|
}
|
|
updateUI();
|
|
},
|
|
rollback: (action) => {
|
|
const object = pointerList[action.object];
|
|
const layer = pointerList[action.layer];
|
|
|
|
for (let item of action.items) {
|
|
if (item.type == "shape") {
|
|
layer.removeShape(pointerList[item.idx]);
|
|
} else if (item.type == "GraphicsObject") {
|
|
object.removeChild(pointerList[item.idx]);
|
|
}
|
|
}
|
|
updateUI();
|
|
},
|
|
},
|
|
deleteObjects: {
|
|
create: (objects, shapes) => {
|
|
redoStack.length = 0;
|
|
const layer = context.activeObject.activeLayer;
|
|
const time = context.activeObject.currentTime || 0;
|
|
|
|
let serializableObjects = [];
|
|
let oldObjectExists = {};
|
|
for (let object of objects) {
|
|
serializableObjects.push(object.idx);
|
|
// Store old exists value for rollback
|
|
const existsValue = layer.animationData.interpolate(`object.${object.idx}.exists`, time);
|
|
oldObjectExists[object.idx] = existsValue !== null ? existsValue : 1;
|
|
}
|
|
|
|
let serializableShapes = [];
|
|
for (let shape of shapes) {
|
|
serializableShapes.push(shape.idx);
|
|
}
|
|
|
|
let action = {
|
|
objects: serializableObjects,
|
|
shapes: serializableShapes,
|
|
layer: layer.idx,
|
|
time: time,
|
|
oldObjectExists: oldObjectExists,
|
|
};
|
|
undoStack.push({ name: "deleteObjects", action: action });
|
|
actions.deleteObjects.execute(action);
|
|
updateMenu();
|
|
},
|
|
execute: (action) => {
|
|
const layer = pointerList[action.layer];
|
|
const time = action.time;
|
|
|
|
// For objects: set exists to 0 at this time
|
|
for (let objectIdx of action.objects) {
|
|
const existsCurve = layer.animationData.getCurve(`object.${objectIdx}.exists`);
|
|
const kf = existsCurve?.getKeyframeAtTime(time);
|
|
if (kf) {
|
|
kf.value = 0;
|
|
} else {
|
|
layer.animationData.addKeyframe(`object.${objectIdx}.exists`, new Keyframe(time, 0, "hold"));
|
|
}
|
|
}
|
|
|
|
// For shapes: remove them (leaves holes that can be filled on undo)
|
|
for (let shapeIdx of action.shapes) {
|
|
layer.removeShape(pointerList[shapeIdx]);
|
|
}
|
|
updateUI();
|
|
},
|
|
rollback: (action) => {
|
|
const layer = pointerList[action.layer];
|
|
const time = action.time;
|
|
|
|
// Restore old exists values for objects
|
|
for (let objectIdx of action.objects) {
|
|
const oldExists = action.oldObjectExists[objectIdx];
|
|
const existsCurve = layer.animationData.getCurve(`object.${objectIdx}.exists`);
|
|
const kf = existsCurve?.getKeyframeAtTime(time);
|
|
if (kf) {
|
|
kf.value = oldExists;
|
|
} else {
|
|
layer.animationData.addKeyframe(`object.${objectIdx}.exists`, new Keyframe(time, oldExists, "hold"));
|
|
}
|
|
}
|
|
|
|
// For shapes: restore them with their original shapeIndex (fills the holes)
|
|
for (let shapeIdx of action.shapes) {
|
|
const shape = pointerList[shapeIdx];
|
|
if (shape) {
|
|
layer.addShape(shape, time);
|
|
}
|
|
}
|
|
updateUI();
|
|
},
|
|
},
|
|
addLayer: {
|
|
create: () => {
|
|
redoStack.length = 0;
|
|
let action = {
|
|
object: context.activeObject.idx,
|
|
uuid: uuidv4(),
|
|
};
|
|
undoStack.push({ name: "addLayer", action: action });
|
|
actions.addLayer.execute(action);
|
|
updateMenu();
|
|
},
|
|
execute: (action) => {
|
|
let object = pointerList[action.object];
|
|
let layer = new Layer(action.uuid);
|
|
layer.name = `Layer ${object.layers.length + 1}`;
|
|
object.layers.push(layer);
|
|
object.currentLayer = object.layers.indexOf(layer);
|
|
updateLayers();
|
|
},
|
|
rollback: (action) => {
|
|
let object = pointerList[action.object];
|
|
let layer = pointerList[action.uuid];
|
|
object.layers.splice(object.layers.indexOf(layer), 1);
|
|
object.currentLayer = Math.min(
|
|
object.currentLayer,
|
|
object.layers.length - 1,
|
|
);
|
|
updateLayers();
|
|
},
|
|
},
|
|
deleteLayer: {
|
|
create: (layer) => {
|
|
redoStack.length = 0;
|
|
// Don't allow deleting the only layer
|
|
if (context.activeObject.layers.length == 1) return;
|
|
if (!(layer instanceof Layer)) {
|
|
layer = context.activeObject.activeLayer;
|
|
}
|
|
let action = {
|
|
object: context.activeObject.idx,
|
|
layer: layer.idx,
|
|
index: context.activeObject.layers.indexOf(layer),
|
|
};
|
|
undoStack.push({ name: "deleteLayer", action: action });
|
|
actions.deleteLayer.execute(action);
|
|
updateMenu();
|
|
},
|
|
execute: (action) => {
|
|
let object = pointerList[action.object];
|
|
let layer = pointerList[action.layer];
|
|
let changelayer = false;
|
|
if (object.activeLayer == layer) {
|
|
changelayer = true;
|
|
}
|
|
object.layers.splice(object.layers.indexOf(layer), 1);
|
|
if (changelayer) {
|
|
object.currentLayer = 0;
|
|
}
|
|
updateUI();
|
|
updateLayers();
|
|
},
|
|
rollback: (action) => {
|
|
let object = pointerList[action.object];
|
|
let layer = pointerList[action.layer];
|
|
object.layers.splice(action.index, 0, layer);
|
|
updateUI();
|
|
updateLayers();
|
|
},
|
|
},
|
|
changeLayerName: {
|
|
create: (layer, newName) => {
|
|
redoStack.length = 0;
|
|
let action = {
|
|
layer: layer.idx,
|
|
newName: newName,
|
|
oldName: layer.name,
|
|
};
|
|
undoStack.push({ name: "changeLayerName", action: action });
|
|
actions.changeLayerName.execute(action);
|
|
updateMenu();
|
|
},
|
|
execute: (action) => {
|
|
let layer = pointerList[action.layer];
|
|
layer.name = action.newName;
|
|
updateLayers();
|
|
},
|
|
rollback: (action) => {
|
|
let layer = pointerList[action.layer];
|
|
layer.name = action.oldName;
|
|
updateLayers();
|
|
},
|
|
},
|
|
importObject: {
|
|
create: (object) => {
|
|
redoStack.length = 0;
|
|
let action = {
|
|
object: object,
|
|
activeObject: context.activeObject.idx,
|
|
};
|
|
undoStack.push({ name: "importObject", action: action });
|
|
actions.importObject.execute(action);
|
|
updateMenu();
|
|
},
|
|
execute: (action) => {
|
|
const activeObject = pointerList[action.activeObject];
|
|
switch (action.object.type) {
|
|
case "GraphicsObject":
|
|
let object = GraphicsObject.fromJSON(action.object);
|
|
activeObject.addObject(object);
|
|
break;
|
|
case "Layer":
|
|
let layer = Layer.fromJSON(action.object);
|
|
activeObject.addLayer(layer);
|
|
}
|
|
updateUI();
|
|
updateLayers();
|
|
},
|
|
rollback: (action) => {
|
|
const activeObject = pointerList[action.activeObject];
|
|
switch (action.object.type) {
|
|
case "GraphicsObject":
|
|
let object = pointerList[action.object.idx];
|
|
activeObject.removeChild(object);
|
|
break;
|
|
case "Layer":
|
|
let layer = pointerList[action.object.idx];
|
|
activeObject.removeLayer(layer);
|
|
}
|
|
updateUI();
|
|
updateLayers();
|
|
},
|
|
},
|
|
transformObjects: {
|
|
initialize: (
|
|
frame,
|
|
_selection,
|
|
direction,
|
|
mouse,
|
|
transform = undefined,
|
|
) => {
|
|
let bbox = undefined;
|
|
const selection = {};
|
|
for (let item of _selection) {
|
|
if (bbox == undefined) {
|
|
bbox = getRotatedBoundingBox(item);
|
|
} else {
|
|
growBoundingBox(bbox, getRotatedBoundingBox(item));
|
|
}
|
|
selection[item.idx] = {
|
|
x: item.x,
|
|
y: item.y,
|
|
scale_x: item.scale_x,
|
|
scale_y: item.scale_y,
|
|
rotation: item.rotation,
|
|
};
|
|
}
|
|
let action = {
|
|
type: "transformObjects",
|
|
oldState: structuredClone(frame.keys),
|
|
frame: frame.idx,
|
|
transform: {
|
|
initial: {
|
|
x: { min: bbox.x.min, max: bbox.x.max },
|
|
y: { min: bbox.y.min, max: bbox.y.max },
|
|
rotation: 0,
|
|
mouse: { x: mouse.x, y: mouse.y },
|
|
selection: selection,
|
|
},
|
|
current: {
|
|
x: { min: bbox.x.min, max: bbox.x.max },
|
|
y: { min: bbox.y.min, max: bbox.y.max },
|
|
scale_x: 1,
|
|
scale_y: 1,
|
|
rotation: 0,
|
|
mouse: { x: mouse.x, y: mouse.y },
|
|
selection: structuredClone(selection),
|
|
},
|
|
},
|
|
selection: selection,
|
|
direction: direction,
|
|
};
|
|
if (transform) {
|
|
action.transform = transform;
|
|
}
|
|
return action;
|
|
},
|
|
update: (action, mouse) => {
|
|
const initial = action.transform.initial;
|
|
const current = action.transform.current;
|
|
if (action.direction.indexOf("n") != -1) {
|
|
current.y.min = mouse.y;
|
|
} else if (action.direction.indexOf("s") != -1) {
|
|
current.y.max = mouse.y;
|
|
}
|
|
if (action.direction.indexOf("w") != -1) {
|
|
current.x.min = mouse.x;
|
|
} else if (action.direction.indexOf("e") != -1) {
|
|
current.x.max = mouse.x;
|
|
}
|
|
if (context.dragDirection == "r") {
|
|
const pivot = {
|
|
x: (initial.x.min + initial.x.max) / 2,
|
|
y: (initial.y.min + initial.y.max) / 2,
|
|
};
|
|
current.rotation = signedAngleBetweenVectors(
|
|
pivot,
|
|
initial.mouse,
|
|
mouse,
|
|
);
|
|
const { dx, dy } = rotateAroundPointIncremental(
|
|
current.x.min,
|
|
current.y.min,
|
|
pivot,
|
|
current.rotation,
|
|
);
|
|
}
|
|
|
|
// Calculate the scaling factor based on the difference between current and initial values
|
|
action.transform.current.scale_x =
|
|
(current.x.max - current.x.min) / (initial.x.max - initial.x.min);
|
|
action.transform.current.scale_y =
|
|
(current.y.max - current.y.min) / (initial.y.max - initial.y.min);
|
|
return action;
|
|
},
|
|
render: (action, ctx) => {
|
|
const initial = action.transform.initial;
|
|
const current = action.transform.current;
|
|
ctx.save();
|
|
ctx.translate(
|
|
(current.x.max + current.x.min) / 2,
|
|
(current.y.max - current.y.min) / 2,
|
|
);
|
|
ctx.rotate(current.rotation);
|
|
ctx.translate(
|
|
-(current.x.max + current.x.min) / 2,
|
|
-(current.y.max - current.y.min) / 2,
|
|
);
|
|
const cxt = {
|
|
ctx: ctx,
|
|
selection: [],
|
|
shapeselection: [],
|
|
};
|
|
for (let obj in action.selection) {
|
|
const object = pointerList[obj];
|
|
const transform = ctx.getTransform()
|
|
ctx.translate(object.x, object.y)
|
|
ctx.scale(object.scale_x, object.scale_y)
|
|
ctx.rotate(object.rotation)
|
|
object.draw(ctx)
|
|
ctx.setTransform(transform)
|
|
}
|
|
ctx.strokeStyle = "#00ffff";
|
|
ctx.lineWidth = 1;
|
|
ctx.beginPath();
|
|
ctx.rect(
|
|
current.x.min,
|
|
current.y.min,
|
|
current.x.max - current.x.min,
|
|
current.y.max - current.y.min,
|
|
);
|
|
ctx.stroke();
|
|
ctx.fillStyle = "#000000";
|
|
const rectRadius = 5;
|
|
const xdiff = current.x.max - current.x.min;
|
|
const ydiff = current.y.max - current.y.min;
|
|
for (let i of [
|
|
[0, 0],
|
|
[0.5, 0],
|
|
[1, 0],
|
|
[1, 0.5],
|
|
[1, 1],
|
|
[0.5, 1],
|
|
[0, 1],
|
|
[0, 0.5],
|
|
]) {
|
|
ctx.beginPath();
|
|
ctx.rect(
|
|
current.x.min + xdiff * i[0] - rectRadius,
|
|
current.y.min + ydiff * i[1] - rectRadius,
|
|
rectRadius * 2,
|
|
rectRadius * 2,
|
|
);
|
|
ctx.fill();
|
|
}
|
|
ctx.restore();
|
|
},
|
|
finalize: (action) => {
|
|
undoStack.push({ name: "transformObjects", action: action });
|
|
actions.transformObjects.execute(action);
|
|
context.activeAction = undefined;
|
|
updateMenu();
|
|
},
|
|
execute: (action) => {
|
|
const frame = pointerList[action.frame];
|
|
const initial = action.transform.initial;
|
|
const current = action.transform.current;
|
|
const delta_x = current.x.min - initial.x.min;
|
|
const delta_y = current.y.min - initial.y.min;
|
|
const delta_rot = current.rotation - initial.rotation;
|
|
// frame.keys = structuredClone(action.newState)
|
|
for (let idx in action.selection) {
|
|
const item = frame.keys[idx];
|
|
const xoffset = action.selection[idx].x - initial.x.min;
|
|
const yoffset = action.selection[idx].y - initial.y.min;
|
|
item.x = initial.x.min + delta_x + xoffset * current.scale_x;
|
|
item.y = initial.y.min + delta_y + yoffset * current.scale_y;
|
|
item.scale_x = action.selection[idx].scale_x * current.scale_x;
|
|
item.scale_y = action.selection[idx].scale_y * current.scale_y;
|
|
item.rotation = action.selection[idx].rotation + delta_rot;
|
|
}
|
|
updateUI();
|
|
},
|
|
rollback: (action) => {
|
|
let frame = pointerList[action.frame];
|
|
frame.keys = structuredClone(action.oldState);
|
|
updateUI();
|
|
},
|
|
},
|
|
moveObjects: {
|
|
initialize: (objects, layer, time) => {
|
|
let oldPositions = {};
|
|
let hadKeyframes = {};
|
|
for (let obj of objects) {
|
|
const xCurve = layer.animationData.getCurve(`child.${obj.idx}.x`);
|
|
const yCurve = layer.animationData.getCurve(`child.${obj.idx}.y`);
|
|
const xKf = xCurve?.getKeyframeAtTime(time);
|
|
const yKf = yCurve?.getKeyframeAtTime(time);
|
|
|
|
const x = layer.animationData.interpolate(`child.${obj.idx}.x`, time);
|
|
const y = layer.animationData.interpolate(`child.${obj.idx}.y`, time);
|
|
oldPositions[obj.idx] = { x, y };
|
|
hadKeyframes[obj.idx] = { x: !!xKf, y: !!yKf };
|
|
}
|
|
let action = {
|
|
type: "moveObjects",
|
|
objects: objects.map(o => o.idx),
|
|
layer: layer.idx,
|
|
time: time,
|
|
oldPositions: oldPositions,
|
|
hadKeyframes: hadKeyframes,
|
|
};
|
|
return action;
|
|
},
|
|
finalize: (action) => {
|
|
const layer = pointerList[action.layer];
|
|
let newPositions = {};
|
|
for (let objIdx of action.objects) {
|
|
const obj = pointerList[objIdx];
|
|
newPositions[objIdx] = { x: obj.x, y: obj.y };
|
|
}
|
|
action.newPositions = newPositions;
|
|
undoStack.push({ name: "moveObjects", action: action });
|
|
actions.moveObjects.execute(action);
|
|
context.activeAction = undefined;
|
|
updateMenu();
|
|
},
|
|
render: (action, ctx) => {},
|
|
create: (objects, layer, time, oldPositions, newPositions) => {
|
|
redoStack.length = 0;
|
|
|
|
// Track which keyframes existed before the move
|
|
let hadKeyframes = {};
|
|
for (let obj of objects) {
|
|
const xCurve = layer.animationData.getCurve(`child.${obj.idx}.x`);
|
|
const yCurve = layer.animationData.getCurve(`child.${obj.idx}.y`);
|
|
const xKf = xCurve?.getKeyframeAtTime(time);
|
|
const yKf = yCurve?.getKeyframeAtTime(time);
|
|
hadKeyframes[obj.idx] = { x: !!xKf, y: !!yKf };
|
|
}
|
|
|
|
let action = {
|
|
objects: objects.map(o => o.idx),
|
|
layer: layer.idx,
|
|
time: time,
|
|
oldPositions: oldPositions,
|
|
newPositions: newPositions,
|
|
hadKeyframes: hadKeyframes,
|
|
};
|
|
undoStack.push({ name: "moveObjects", action: action });
|
|
actions.moveObjects.execute(action);
|
|
updateMenu();
|
|
},
|
|
execute: (action) => {
|
|
const layer = pointerList[action.layer];
|
|
const time = action.time;
|
|
|
|
for (let objIdx of action.objects) {
|
|
const obj = pointerList[objIdx];
|
|
const newPos = action.newPositions[objIdx];
|
|
|
|
// Update object properties
|
|
obj.x = newPos.x;
|
|
obj.y = newPos.y;
|
|
|
|
// Add/update keyframes in AnimationData
|
|
const xCurve = layer.animationData.getCurve(`child.${objIdx}.x`);
|
|
const kf = xCurve?.getKeyframeAtTime(time);
|
|
if (kf) {
|
|
kf.value = newPos.x;
|
|
} else {
|
|
layer.animationData.addKeyframe(`child.${objIdx}.x`, new Keyframe(time, newPos.x, "linear"));
|
|
}
|
|
|
|
const yCurve = layer.animationData.getCurve(`child.${objIdx}.y`);
|
|
const kfy = yCurve?.getKeyframeAtTime(time);
|
|
if (kfy) {
|
|
kfy.value = newPos.y;
|
|
} else {
|
|
layer.animationData.addKeyframe(`child.${objIdx}.y`, new Keyframe(time, newPos.y, "linear"));
|
|
}
|
|
}
|
|
updateUI();
|
|
},
|
|
rollback: (action) => {
|
|
const layer = pointerList[action.layer];
|
|
const time = action.time;
|
|
|
|
for (let objIdx of action.objects) {
|
|
const obj = pointerList[objIdx];
|
|
const oldPos = action.oldPositions[objIdx];
|
|
const hadKfs = action.hadKeyframes?.[objIdx] || { x: false, y: false };
|
|
|
|
// Restore object properties
|
|
obj.x = oldPos.x;
|
|
obj.y = oldPos.y;
|
|
|
|
// Restore or remove keyframes in AnimationData
|
|
const xCurve = layer.animationData.getCurve(`child.${objIdx}.x`);
|
|
if (hadKfs.x) {
|
|
// Had a keyframe before - restore its value
|
|
const kf = xCurve?.getKeyframeAtTime(time);
|
|
if (kf) {
|
|
kf.value = oldPos.x;
|
|
} else {
|
|
layer.animationData.addKeyframe(`child.${objIdx}.x`, new Keyframe(time, oldPos.x, "linear"));
|
|
}
|
|
} else {
|
|
// No keyframe before - remove the entire curve
|
|
if (xCurve) {
|
|
layer.animationData.removeCurve(`child.${objIdx}.x`);
|
|
}
|
|
}
|
|
|
|
const yCurve = layer.animationData.getCurve(`child.${objIdx}.y`);
|
|
if (hadKfs.y) {
|
|
// Had a keyframe before - restore its value
|
|
const kfy = yCurve?.getKeyframeAtTime(time);
|
|
if (kfy) {
|
|
kfy.value = oldPos.y;
|
|
} else {
|
|
layer.animationData.addKeyframe(`child.${objIdx}.y`, new Keyframe(time, oldPos.y, "linear"));
|
|
}
|
|
} else {
|
|
// No keyframe before - remove the entire curve
|
|
if (yCurve) {
|
|
layer.animationData.removeCurve(`child.${objIdx}.y`);
|
|
}
|
|
}
|
|
}
|
|
|
|
updateUI();
|
|
},
|
|
},
|
|
editFrame: {
|
|
// DEPRECATED: Kept for backwards compatibility
|
|
initialize: (frame) => {
|
|
console.warn("editFrame is deprecated, use moveObjects instead");
|
|
return null;
|
|
},
|
|
finalize: (action, frame) => {},
|
|
render: (action, ctx) => {},
|
|
create: (frame) => {},
|
|
execute: (action) => {},
|
|
rollback: (action) => {},
|
|
},
|
|
addFrame: {
|
|
create: () => {
|
|
redoStack.length = 0;
|
|
let frames = [];
|
|
for (
|
|
let i = context.activeObject.activeLayer.frames.length;
|
|
i <= context.activeObject.currentFrameNum;
|
|
i++
|
|
) {
|
|
frames.push(uuidv4());
|
|
}
|
|
let action = {
|
|
frames: frames,
|
|
layer: context.activeObject.activeLayer.idx,
|
|
};
|
|
undoStack.push({ name: "addFrame", action: action });
|
|
actions.addFrame.execute(action);
|
|
updateMenu();
|
|
},
|
|
execute: (action) => {
|
|
let layer = pointerList[action.layer];
|
|
for (let frame of action.frames) {
|
|
layer.frames.push(new Frame("normal", frame));
|
|
}
|
|
updateLayers();
|
|
},
|
|
rollback: (action) => {
|
|
let layer = pointerList[action.layer];
|
|
for (let _frame of action.frames) {
|
|
layer.frames.pop();
|
|
}
|
|
updateLayers();
|
|
},
|
|
},
|
|
addKeyframe: {
|
|
create: () => {
|
|
let frameNum = context.activeObject.currentFrameNum;
|
|
let layer = context.activeObject.activeLayer;
|
|
let formerType;
|
|
let addedFrames = {};
|
|
if (frameNum >= layer.frames.length) {
|
|
formerType = "none";
|
|
// for (let i = layer.frames.length; i <= frameNum; i++) {
|
|
// addedFrames[i] = uuidv4();
|
|
// }
|
|
} else if (!layer.frames[frameNum]) {
|
|
formerType = undefined
|
|
} else if (layer.frames[frameNum].frameType != "keyframe") {
|
|
formerType = layer.frames[frameNum].frameType;
|
|
} else {
|
|
return; // Already a keyframe, nothing to do
|
|
}
|
|
redoStack.length = 0;
|
|
let action = {
|
|
frameNum: frameNum,
|
|
object: context.activeObject.idx,
|
|
layer: layer.idx,
|
|
formerType: formerType,
|
|
addedFrames: addedFrames,
|
|
uuid: uuidv4(),
|
|
};
|
|
undoStack.push({ name: "addKeyframe", action: action });
|
|
actions.addKeyframe.execute(action);
|
|
updateMenu();
|
|
},
|
|
execute: (action) => {
|
|
let object = pointerList[action.object];
|
|
let layer = pointerList[action.layer];
|
|
layer.addOrChangeFrame(
|
|
action.frameNum,
|
|
"keyframe",
|
|
action.uuid,
|
|
action.addedFrames,
|
|
);
|
|
updateLayers();
|
|
updateUI();
|
|
},
|
|
rollback: (action) => {
|
|
let layer = pointerList[action.layer];
|
|
if (action.formerType == "none") {
|
|
for (let i in action.addedFrames) {
|
|
layer.frames.pop();
|
|
}
|
|
} else {
|
|
let layer = pointerList[action.layer];
|
|
if (action.formerType) {
|
|
layer.frames[action.frameNum].frameType = action.formerType;
|
|
} else {
|
|
layer.frames[action.frameNum = undefined]
|
|
}
|
|
}
|
|
updateLayers();
|
|
updateUI();
|
|
},
|
|
},
|
|
deleteFrame: {
|
|
create: (frame, layer) => {
|
|
redoStack.length = 0;
|
|
let action = {
|
|
frame: frame.idx,
|
|
layer: layer.idx,
|
|
replacementUuid: uuidv4(),
|
|
};
|
|
undoStack.push({ name: "deleteFrame", action: action });
|
|
actions.deleteFrame.execute(action);
|
|
updateMenu();
|
|
},
|
|
execute: (action) => {
|
|
let layer = pointerList[action.layer];
|
|
layer.deleteFrame(
|
|
action.frame,
|
|
undefined,
|
|
action.replacementUuid ? action.replacementUuid : uuidv4(),
|
|
);
|
|
updateLayers();
|
|
updateUI();
|
|
},
|
|
rollback: (action) => {
|
|
let layer = pointerList[action.layer];
|
|
let frame = pointerList[action.frame];
|
|
layer.addFrame(action.frameNum, frame, {});
|
|
updateLayers();
|
|
updateUI();
|
|
},
|
|
},
|
|
moveFrames: {
|
|
create: (offset) => {
|
|
redoStack.length = 0;
|
|
const selectedFrames = structuredClone(context.selectedFrames);
|
|
for (let frame of selectedFrames) {
|
|
frame.replacementUuid = uuidv4();
|
|
frame.layer = context.activeObject.layers.length - frame.layer - 1;
|
|
}
|
|
// const fillFrames = []
|
|
// for (let i=0; i<context.activeObject.layers.length;i++) {
|
|
// const fillLayer = []
|
|
// for (let j=0; j<Math.abs(offset.frames); j++) {
|
|
// fillLayer.push(uuidv4())
|
|
// }
|
|
// fillFrames.push(fillLayer)
|
|
// }
|
|
let action = {
|
|
selectedFrames: selectedFrames,
|
|
offset: offset,
|
|
object: context.activeObject.idx,
|
|
// fillFrames: fillFrames
|
|
};
|
|
undoStack.push({ name: "moveFrames", action: action });
|
|
actions.moveFrames.execute(action);
|
|
updateMenu();
|
|
},
|
|
execute: (action) => {
|
|
const object = pointerList[action.object];
|
|
const frameBuffer = [];
|
|
for (let frameObj of action.selectedFrames) {
|
|
let layer = object.layers[frameObj.layer];
|
|
let frame = layer.frames[frameObj.frameNum];
|
|
if (frameObj) {
|
|
frameBuffer.push({
|
|
frame: frame,
|
|
frameNum: frameObj.frameNum,
|
|
layer: frameObj.layer,
|
|
});
|
|
layer.deleteFrame(frame.idx, undefined, frameObj.replacementUuid);
|
|
}
|
|
}
|
|
for (let frameObj of frameBuffer) {
|
|
// TODO: figure out object tracking when moving frames between layers
|
|
const layer_idx = frameObj.layer// + action.offset.layers;
|
|
let layer = object.layers[layer_idx];
|
|
let frame = frameObj.frame;
|
|
layer.addFrame(frameObj.frameNum + action.offset.frames, frame, []); //fillFrames[layer_idx])
|
|
}
|
|
updateLayers();
|
|
updateUI();
|
|
},
|
|
rollback: (action) => {
|
|
const object = pointerList[action.object];
|
|
const frameBuffer = [];
|
|
for (let frameObj of action.selectedFrames) {
|
|
let layer = object.layers[frameObj.layer];
|
|
let frame = layer.frames[frameObj.frameNum + action.offset.frames];
|
|
if (frameObj) {
|
|
frameBuffer.push({
|
|
frame: frame,
|
|
frameNum: frameObj.frameNum,
|
|
layer: frameObj.layer,
|
|
});
|
|
layer.deleteFrame(frame.idx, "none")
|
|
}
|
|
}
|
|
for (let frameObj of frameBuffer) {
|
|
let layer = object.layers[frameObj.layer];
|
|
let frame = frameObj.frame;
|
|
if (frameObj) {
|
|
layer.addFrame(frameObj.frameNum, frame, [])
|
|
}
|
|
}
|
|
},
|
|
},
|
|
addMotionTween: {
|
|
create: () => {
|
|
redoStack.length = 0;
|
|
let frameNum = context.activeObject.currentFrameNum;
|
|
let layer = context.activeObject.activeLayer;
|
|
|
|
const frameInfo = layer.getFrameValue(frameNum)
|
|
let lastKeyframeBefore, firstKeyframeAfter
|
|
if (frameInfo.valueAtN) {
|
|
lastKeyframeBefore = frameNum
|
|
} else if (frameInfo.prev) {
|
|
lastKeyframeBefore = frameInfo.prevIndex
|
|
} else {
|
|
return
|
|
}
|
|
firstKeyframeAfter = frameInfo.nextIndex
|
|
|
|
let action = {
|
|
frameNum: frameNum,
|
|
layer: layer.idx,
|
|
lastBefore: lastKeyframeBefore,
|
|
firstAfter: firstKeyframeAfter,
|
|
};
|
|
undoStack.push({ name: "addMotionTween", action: action });
|
|
actions.addMotionTween.execute(action);
|
|
updateMenu();
|
|
},
|
|
execute: (action) => {
|
|
let layer = pointerList[action.layer];
|
|
let frames = layer.frames;
|
|
if (action.lastBefore != undefined) {
|
|
console.log("adding motion")
|
|
frames[action.lastBefore].keyTypes.add("motion")
|
|
}
|
|
updateLayers();
|
|
updateUI();
|
|
},
|
|
rollback: (action) => {
|
|
let layer = pointerList[action.layer];
|
|
let frames = layer.frames;
|
|
if (action.lastBefore != undefined) {
|
|
frames[action.lastBefore].keyTypes.delete("motion")
|
|
}
|
|
updateLayers();
|
|
updateUI();
|
|
},
|
|
},
|
|
addShapeTween: {
|
|
create: () => {
|
|
redoStack.length = 0;
|
|
let frameNum = context.activeObject.currentFrameNum;
|
|
let layer = context.activeObject.activeLayer;
|
|
|
|
const frameInfo = layer.getFrameValue(frameNum)
|
|
let lastKeyframeBefore, firstKeyframeAfter
|
|
if (frameInfo.valueAtN) {
|
|
lastKeyframeBefore = frameNum
|
|
} else if (frameInfo.prev) {
|
|
lastKeyframeBefore = frameInfo.prevIndex
|
|
} else {
|
|
return
|
|
}
|
|
firstKeyframeAfter = frameInfo.nextIndex
|
|
|
|
|
|
let action = {
|
|
frameNum: frameNum,
|
|
layer: layer.idx,
|
|
lastBefore: lastKeyframeBefore,
|
|
firstAfter: firstKeyframeAfter,
|
|
};
|
|
console.log(action)
|
|
undoStack.push({ name: "addShapeTween", action: action });
|
|
actions.addShapeTween.execute(action);
|
|
updateMenu();
|
|
},
|
|
execute: (action) => {
|
|
let layer = pointerList[action.layer];
|
|
let frames = layer.frames;
|
|
if (action.lastBefore != undefined) {
|
|
frames[action.lastBefore].keyTypes.add("shape")
|
|
}
|
|
updateLayers();
|
|
updateUI();
|
|
},
|
|
rollback: (action) => {
|
|
let layer = pointerList[action.layer];
|
|
let frames = layer.frames;
|
|
if (action.lastBefore != undefined) {
|
|
frames[action.lastBefore].keyTypes.delete("shape")
|
|
}
|
|
updateLayers();
|
|
updateUI();
|
|
},
|
|
},
|
|
group: {
|
|
create: () => {
|
|
redoStack.length = 0;
|
|
let serializableShapes = [];
|
|
let serializableObjects = [];
|
|
let bbox;
|
|
const currentTime = context.activeObject?.currentTime || 0;
|
|
const layer = context.activeObject.activeLayer;
|
|
|
|
// For shapes - use AnimationData system
|
|
for (let shape of context.shapeselection) {
|
|
serializableShapes.push(shape.idx);
|
|
if (bbox == undefined) {
|
|
bbox = shape.bbox();
|
|
} else {
|
|
growBoundingBox(bbox, shape.bbox());
|
|
}
|
|
}
|
|
|
|
// For objects - check if they exist at current time
|
|
for (let object of context.selection) {
|
|
const existsValue = layer.animationData.interpolate(`object.${object.idx}.exists`, currentTime);
|
|
if (existsValue > 0) {
|
|
serializableObjects.push(object.idx);
|
|
// TODO: rotated bbox
|
|
if (bbox == undefined) {
|
|
bbox = object.bbox();
|
|
} else {
|
|
growBoundingBox(bbox, object.bbox());
|
|
}
|
|
}
|
|
}
|
|
|
|
// If nothing was selected, don't create a group
|
|
if (!bbox) {
|
|
return;
|
|
}
|
|
|
|
context.shapeselection = [];
|
|
context.selection = [];
|
|
let action = {
|
|
shapes: serializableShapes,
|
|
objects: serializableObjects,
|
|
groupUuid: uuidv4(),
|
|
parent: context.activeObject.idx,
|
|
layer: layer.idx,
|
|
currentTime: currentTime,
|
|
position: {
|
|
x: (bbox.x.min + bbox.x.max) / 2,
|
|
y: (bbox.y.min + bbox.y.max) / 2,
|
|
},
|
|
};
|
|
undoStack.push({ name: "group", action: action });
|
|
actions.group.execute(action);
|
|
updateMenu();
|
|
updateLayers();
|
|
},
|
|
execute: (action) => {
|
|
let group = new GraphicsObject(action.groupUuid);
|
|
let parent = pointerList[action.parent];
|
|
let layer = pointerList[action.layer] || parent.activeLayer;
|
|
const currentTime = action.currentTime || 0;
|
|
|
|
// Move shapes from parent layer to group's first layer
|
|
for (let shapeIdx of action.shapes) {
|
|
let shape = pointerList[shapeIdx];
|
|
shape.translate(-action.position.x, -action.position.y);
|
|
|
|
// Remove shape from parent layer's shapes array
|
|
let shapeIndex = layer.shapes.indexOf(shape);
|
|
if (shapeIndex !== -1) {
|
|
layer.shapes.splice(shapeIndex, 1);
|
|
}
|
|
|
|
// Remove animation curves for this shape from parent layer
|
|
layer.animationData.removeCurve(`shape.${shape.shapeId}.exists`);
|
|
layer.animationData.removeCurve(`shape.${shape.shapeId}.zOrder`);
|
|
layer.animationData.removeCurve(`shape.${shape.shapeId}.shapeIndex`);
|
|
|
|
// Add shape to group's first layer
|
|
let groupLayer = group.activeLayer;
|
|
shape.parent = groupLayer;
|
|
groupLayer.shapes.push(shape);
|
|
|
|
// Add animation curves for this shape in group's layer
|
|
let existsCurve = new AnimationCurve(`shape.${shape.shapeId}.exists`);
|
|
existsCurve.addKeyframe(new Keyframe(0, 1, 'linear'));
|
|
groupLayer.animationData.setCurve(`shape.${shape.shapeId}.exists`, existsCurve);
|
|
|
|
let zOrderCurve = new AnimationCurve(`shape.${shape.shapeId}.zOrder`);
|
|
zOrderCurve.addKeyframe(new Keyframe(0, groupLayer.shapes.length - 1, 'linear'));
|
|
groupLayer.animationData.setCurve(`shape.${shape.shapeId}.zOrder`, zOrderCurve);
|
|
|
|
let shapeIndexCurve = new AnimationCurve(`shape.${shape.shapeId}.shapeIndex`);
|
|
shapeIndexCurve.addKeyframe(new Keyframe(0, 0, 'linear'));
|
|
groupLayer.animationData.setCurve(`shape.${shape.shapeId}.shapeIndex`, shapeIndexCurve);
|
|
}
|
|
|
|
// Move objects (children) to the group
|
|
for (let objectIdx of action.objects) {
|
|
let object = pointerList[objectIdx];
|
|
|
|
// Get object position from AnimationData if available
|
|
const objX = layer.animationData.interpolate(`object.${objectIdx}.x`, currentTime);
|
|
const objY = layer.animationData.interpolate(`object.${objectIdx}.y`, currentTime);
|
|
|
|
if (objX !== null && objY !== null) {
|
|
group.addObject(
|
|
object,
|
|
objX - action.position.x,
|
|
objY - action.position.y,
|
|
currentTime
|
|
);
|
|
} else {
|
|
group.addObject(object, 0, 0, currentTime);
|
|
}
|
|
parent.removeChild(object);
|
|
}
|
|
|
|
// Add group to parent using time-based API
|
|
parent.addObject(group, action.position.x, action.position.y, currentTime);
|
|
context.selection = [group];
|
|
context.activeCurve = undefined;
|
|
context.activeVertex = undefined;
|
|
updateUI();
|
|
updateInfopanel();
|
|
},
|
|
rollback: (action) => {
|
|
let group = pointerList[action.groupUuid];
|
|
let parent = pointerList[action.parent];
|
|
const layer = pointerList[action.layer] || parent.activeLayer;
|
|
const currentTime = action.currentTime || 0;
|
|
|
|
for (let shapeIdx of action.shapes) {
|
|
let shape = pointerList[shapeIdx];
|
|
shape.translate(action.position.x, action.position.y);
|
|
layer.addShape(shape, currentTime);
|
|
group.activeLayer.removeShape(shape);
|
|
}
|
|
for (let objectIdx of action.objects) {
|
|
let object = pointerList[objectIdx];
|
|
parent.addObject(object, object.x, object.y, currentTime);
|
|
group.removeChild(object);
|
|
}
|
|
parent.removeChild(group);
|
|
updateUI();
|
|
updateInfopanel();
|
|
},
|
|
},
|
|
sendToBack: {
|
|
create: () => {
|
|
redoStack.length = 0;
|
|
const currentTime = context.activeObject.currentTime || 0;
|
|
const layer = context.activeObject.activeLayer;
|
|
|
|
let serializableShapes = [];
|
|
let oldZOrders = {};
|
|
|
|
// Store current zOrder for each shape
|
|
for (let shape of context.shapeselection) {
|
|
serializableShapes.push(shape.idx);
|
|
const zOrder = layer.animationData.interpolate(`shape.${shape.shapeId}.zOrder`, currentTime);
|
|
oldZOrders[shape.idx] = zOrder !== null ? zOrder : 0;
|
|
}
|
|
|
|
let serializableObjects = [];
|
|
let formerIndices = {};
|
|
for (let object of context.selection) {
|
|
serializableObjects.push(object.idx);
|
|
formerIndices[object.idx] = layer.children.indexOf(object);
|
|
}
|
|
|
|
let action = {
|
|
shapes: serializableShapes,
|
|
objects: serializableObjects,
|
|
layer: layer.idx,
|
|
time: currentTime,
|
|
oldZOrders: oldZOrders,
|
|
formerIndices: formerIndices,
|
|
};
|
|
undoStack.push({ name: "sendToBack", action: action });
|
|
actions.sendToBack.execute(action);
|
|
updateMenu();
|
|
},
|
|
execute: (action) => {
|
|
let layer = pointerList[action.layer];
|
|
const time = action.time;
|
|
|
|
// For shapes: set zOrder to 0, increment all others
|
|
for (let shapeIdx of action.shapes) {
|
|
let shape = pointerList[shapeIdx];
|
|
|
|
// Increment zOrder for all other shapes at this time
|
|
for (let otherShape of layer.shapes) {
|
|
if (otherShape.shapeId !== shape.shapeId) {
|
|
const zOrderCurve = layer.animationData.getCurve(`shape.${otherShape.shapeId}.zOrder`);
|
|
if (zOrderCurve) {
|
|
const kf = zOrderCurve.getKeyframeAtTime(time);
|
|
if (kf) {
|
|
kf.value += 1;
|
|
} else {
|
|
// Add keyframe at current time with incremented value
|
|
const currentZOrder = layer.animationData.interpolate(`shape.${otherShape.shapeId}.zOrder`, time) || 0;
|
|
layer.animationData.addKeyframe(`shape.${otherShape.shapeId}.zOrder`, new Keyframe(time, currentZOrder + 1, "hold"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set this shape's zOrder to 0
|
|
const zOrderCurve = layer.animationData.getCurve(`shape.${shape.shapeId}.zOrder`);
|
|
const kf = zOrderCurve?.getKeyframeAtTime(time);
|
|
if (kf) {
|
|
kf.value = 0;
|
|
} else {
|
|
layer.animationData.addKeyframe(`shape.${shape.shapeId}.zOrder`, new Keyframe(time, 0, "hold"));
|
|
}
|
|
}
|
|
|
|
// For objects: move to front of children array
|
|
for (let objectIdx of action.objects) {
|
|
let object = pointerList[objectIdx];
|
|
layer.children.splice(layer.children.indexOf(object), 1);
|
|
layer.children.unshift(object);
|
|
}
|
|
updateUI();
|
|
},
|
|
rollback: (action) => {
|
|
let layer = pointerList[action.layer];
|
|
const time = action.time;
|
|
|
|
// Restore old zOrder values for shapes
|
|
for (let shapeIdx of action.shapes) {
|
|
let shape = pointerList[shapeIdx];
|
|
const oldZOrder = action.oldZOrders[shapeIdx];
|
|
|
|
const zOrderCurve = layer.animationData.getCurve(`shape.${shape.shapeId}.zOrder`);
|
|
const kf = zOrderCurve?.getKeyframeAtTime(time);
|
|
if (kf) {
|
|
kf.value = oldZOrder;
|
|
} else {
|
|
layer.animationData.addKeyframe(`shape.${shape.shapeId}.zOrder`, new Keyframe(time, oldZOrder, "hold"));
|
|
}
|
|
}
|
|
|
|
// Restore old positions for objects
|
|
for (let objectIdx of action.objects) {
|
|
let object = pointerList[objectIdx];
|
|
layer.children.splice(layer.children.indexOf(object), 1);
|
|
layer.children.splice(action.formerIndices[objectIdx], 0, object);
|
|
}
|
|
updateUI();
|
|
},
|
|
},
|
|
bringToFront: {
|
|
create: () => {
|
|
redoStack.length = 0;
|
|
const currentTime = context.activeObject.currentTime || 0;
|
|
const layer = context.activeObject.activeLayer;
|
|
|
|
let serializableShapes = [];
|
|
let oldZOrders = {};
|
|
|
|
// Store current zOrder for each shape
|
|
for (let shape of context.shapeselection) {
|
|
serializableShapes.push(shape.idx);
|
|
const zOrder = layer.animationData.interpolate(`shape.${shape.shapeId}.zOrder`, currentTime);
|
|
oldZOrders[shape.idx] = zOrder !== null ? zOrder : 0;
|
|
}
|
|
|
|
let serializableObjects = [];
|
|
let formerIndices = {};
|
|
for (let object of context.selection) {
|
|
serializableObjects.push(object.idx);
|
|
formerIndices[object.idx] = layer.children.indexOf(object);
|
|
}
|
|
|
|
let action = {
|
|
shapes: serializableShapes,
|
|
objects: serializableObjects,
|
|
layer: layer.idx,
|
|
time: currentTime,
|
|
oldZOrders: oldZOrders,
|
|
formerIndices: formerIndices,
|
|
};
|
|
undoStack.push({ name: "bringToFront", action: action });
|
|
actions.bringToFront.execute(action);
|
|
updateMenu();
|
|
},
|
|
execute: (action) => {
|
|
let layer = pointerList[action.layer];
|
|
const time = action.time;
|
|
|
|
// Find max zOrder at this time
|
|
let maxZOrder = -1;
|
|
for (let shape of layer.shapes) {
|
|
const zOrder = layer.animationData.interpolate(`shape.${shape.shapeId}.zOrder`, time);
|
|
if (zOrder !== null && zOrder > maxZOrder) {
|
|
maxZOrder = zOrder;
|
|
}
|
|
}
|
|
|
|
// For shapes: set zOrder to max+1, max+2, etc.
|
|
let newZOrder = maxZOrder + 1;
|
|
for (let shapeIdx of action.shapes) {
|
|
let shape = pointerList[shapeIdx];
|
|
|
|
const zOrderCurve = layer.animationData.getCurve(`shape.${shape.shapeId}.zOrder`);
|
|
const kf = zOrderCurve?.getKeyframeAtTime(time);
|
|
if (kf) {
|
|
kf.value = newZOrder;
|
|
} else {
|
|
layer.animationData.addKeyframe(`shape.${shape.shapeId}.zOrder`, new Keyframe(time, newZOrder, "hold"));
|
|
}
|
|
newZOrder++;
|
|
}
|
|
|
|
// For objects: move to end of children array
|
|
for (let objectIdx of action.objects) {
|
|
let object = pointerList[objectIdx];
|
|
layer.children.splice(layer.children.indexOf(object), 1);
|
|
object.parentLayer = layer;
|
|
layer.children.push(object);
|
|
}
|
|
updateUI();
|
|
},
|
|
rollback: (action) => {
|
|
let layer = pointerList[action.layer];
|
|
const time = action.time;
|
|
|
|
// Restore old zOrder values for shapes
|
|
for (let shapeIdx of action.shapes) {
|
|
let shape = pointerList[shapeIdx];
|
|
const oldZOrder = action.oldZOrders[shapeIdx];
|
|
|
|
const zOrderCurve = layer.animationData.getCurve(`shape.${shape.shapeId}.zOrder`);
|
|
const kf = zOrderCurve?.getKeyframeAtTime(time);
|
|
if (kf) {
|
|
kf.value = oldZOrder;
|
|
} else {
|
|
layer.animationData.addKeyframe(`shape.${shape.shapeId}.zOrder`, new Keyframe(time, oldZOrder, "hold"));
|
|
}
|
|
}
|
|
|
|
// Restore old positions for objects
|
|
for (let objectIdx of action.objects) {
|
|
let object = pointerList[objectIdx];
|
|
layer.children.splice(layer.children.indexOf(object), 1);
|
|
layer.children.splice(action.formerIndices[objectIdx], 0, object);
|
|
}
|
|
updateUI();
|
|
},
|
|
},
|
|
setName: {
|
|
create: (object, name) => {
|
|
redoStack.length = 0;
|
|
let action = {
|
|
object: object.idx,
|
|
newName: name,
|
|
oldName: object.name,
|
|
};
|
|
undoStack.push({ name: "setName", action: action });
|
|
actions.setName.execute(action);
|
|
updateMenu();
|
|
},
|
|
execute: (action) => {
|
|
let object = pointerList[action.object];
|
|
object.name = action.newName;
|
|
updateInfopanel();
|
|
},
|
|
rollback: (action) => {
|
|
let object = pointerList[action.object];
|
|
object.name = action.oldName;
|
|
updateInfopanel();
|
|
},
|
|
},
|
|
selectAll: {
|
|
create: () => {
|
|
redoStack.length = 0;
|
|
let selection = [];
|
|
let shapeselection = [];
|
|
const currentTime = context.activeObject.currentTime || 0;
|
|
const layer = context.activeObject.activeLayer;
|
|
for (let child of layer.children) {
|
|
let idx = child.idx;
|
|
const existsValue = layer.animationData.interpolate(`object.${idx}.exists`, currentTime);
|
|
if (existsValue > 0) {
|
|
selection.push(child.idx);
|
|
}
|
|
}
|
|
// Use getVisibleShapes instead of currentFrame.shapes
|
|
if (layer) {
|
|
for (let shape of layer.getVisibleShapes(currentTime)) {
|
|
shapeselection.push(shape.idx);
|
|
}
|
|
}
|
|
let action = {
|
|
selection: selection,
|
|
shapeselection: shapeselection,
|
|
};
|
|
undoStack.push({ name: "selectAll", action: action });
|
|
actions.selectAll.execute(action);
|
|
updateMenu();
|
|
},
|
|
execute: (action) => {
|
|
context.selection = [];
|
|
context.shapeselection = [];
|
|
for (let item of action.selection) {
|
|
context.selection.push(pointerList[item]);
|
|
}
|
|
for (let shape of action.shapeselection) {
|
|
context.shapeselection.push(pointerList[shape]);
|
|
}
|
|
updateUI();
|
|
updateMenu();
|
|
},
|
|
rollback: (action) => {
|
|
context.selection = [];
|
|
context.shapeselection = [];
|
|
updateUI();
|
|
updateMenu();
|
|
},
|
|
},
|
|
selectNone: {
|
|
create: () => {
|
|
redoStack.length = 0;
|
|
let selection = [];
|
|
let shapeselection = [];
|
|
for (let item of context.selection) {
|
|
selection.push(item.idx);
|
|
}
|
|
for (let shape of context.shapeselection) {
|
|
shapeselection.push(shape.idx);
|
|
}
|
|
let action = {
|
|
selection: selection,
|
|
shapeselection: shapeselection,
|
|
};
|
|
undoStack.push({ name: "selectNone", action: action });
|
|
actions.selectNone.execute(action);
|
|
updateMenu();
|
|
},
|
|
execute: (action) => {
|
|
context.selection = [];
|
|
context.shapeselection = [];
|
|
updateUI();
|
|
updateMenu();
|
|
},
|
|
rollback: (action) => {
|
|
context.selection = [];
|
|
context.shapeselection = [];
|
|
for (let item of action.selection) {
|
|
context.selection.push(pointerList[item]);
|
|
}
|
|
for (let shape of action.shapeselection) {
|
|
context.shapeselection.push(pointerList[shape]);
|
|
}
|
|
updateUI();
|
|
updateMenu();
|
|
},
|
|
},
|
|
select: {
|
|
create: () => {
|
|
redoStack.length = 0;
|
|
if (
|
|
arraysAreEqual(context.oldselection, context.selection) &&
|
|
arraysAreEqual(context.oldshapeselection, context.shapeselection)
|
|
)
|
|
return;
|
|
let oldselection = [];
|
|
let oldshapeselection = [];
|
|
for (let item of context.oldselection) {
|
|
oldselection.push(item.idx);
|
|
}
|
|
for (let shape of context.oldshapeselection) {
|
|
oldshapeselection.push(shape.idx);
|
|
}
|
|
let selection = [];
|
|
let shapeselection = [];
|
|
for (let item of context.selection) {
|
|
selection.push(item.idx);
|
|
}
|
|
for (let shape of context.shapeselection) {
|
|
shapeselection.push(shape.idx);
|
|
}
|
|
let action = {
|
|
selection: selection,
|
|
shapeselection: shapeselection,
|
|
oldselection: oldselection,
|
|
oldshapeselection: oldshapeselection,
|
|
};
|
|
undoStack.push({ name: "select", action: action });
|
|
actions.select.execute(action);
|
|
updateMenu();
|
|
},
|
|
execute: (action) => {
|
|
context.selection = [];
|
|
context.shapeselection = [];
|
|
for (let item of action.selection) {
|
|
context.selection.push(pointerList[item]);
|
|
}
|
|
for (let shape of action.shapeselection) {
|
|
context.shapeselection.push(pointerList[shape]);
|
|
}
|
|
updateUI();
|
|
updateMenu();
|
|
},
|
|
rollback: (action) => {
|
|
context.selection = [];
|
|
context.shapeselection = [];
|
|
for (let item of action.oldselection) {
|
|
context.selection.push(pointerList[item]);
|
|
}
|
|
for (let shape of action.oldshapeselection) {
|
|
context.shapeselection.push(pointerList[shape]);
|
|
}
|
|
updateUI();
|
|
updateMenu();
|
|
},
|
|
},
|
|
// Node graph actions
|
|
graphAddNode: {
|
|
create: (trackId, nodeType, position, nodeId, backendId) => {
|
|
redoStack.length = 0;
|
|
let action = {
|
|
trackId: trackId,
|
|
nodeType: nodeType,
|
|
position: position,
|
|
nodeId: nodeId, // Frontend node ID from Drawflow
|
|
backendId: backendId
|
|
};
|
|
undoStack.push({ name: "graphAddNode", action: action });
|
|
actions.graphAddNode.execute(action);
|
|
updateMenu();
|
|
},
|
|
execute: async (action) => {
|
|
// Re-add node via Tauri and reload frontend
|
|
const result = await invoke('graph_add_node', {
|
|
trackId: action.trackId,
|
|
nodeType: action.nodeType,
|
|
posX: action.position.x,
|
|
posY: action.position.y
|
|
});
|
|
// Reload the entire graph to show the restored node
|
|
if (context.reloadNodeEditor) {
|
|
await context.reloadNodeEditor();
|
|
}
|
|
},
|
|
rollback: async (action) => {
|
|
// Remove node from backend
|
|
await invoke('graph_remove_node', {
|
|
trackId: action.trackId,
|
|
nodeId: action.backendId
|
|
});
|
|
// Remove from frontend
|
|
if (context.nodeEditor) {
|
|
context.nodeEditor.removeNodeId(`node-${action.nodeId}`);
|
|
}
|
|
},
|
|
},
|
|
graphRemoveNode: {
|
|
create: (trackId, nodeId, backendId, nodeData) => {
|
|
redoStack.length = 0;
|
|
let action = {
|
|
trackId: trackId,
|
|
nodeId: nodeId,
|
|
backendId: backendId,
|
|
nodeData: nodeData, // Store full node data for restoration
|
|
};
|
|
undoStack.push({ name: "graphRemoveNode", action: action });
|
|
actions.graphRemoveNode.execute(action);
|
|
updateMenu();
|
|
},
|
|
execute: async (action) => {
|
|
await invoke('graph_remove_node', {
|
|
trackId: action.trackId,
|
|
nodeId: action.backendId
|
|
});
|
|
if (context.nodeEditor) {
|
|
context.nodeEditor.removeNodeId(`node-${action.nodeId}`);
|
|
}
|
|
},
|
|
rollback: async (action) => {
|
|
// Re-add node to backend
|
|
const result = await invoke('graph_add_node', {
|
|
trackId: action.trackId,
|
|
nodeType: action.nodeData.nodeType,
|
|
posX: action.nodeData.position.x,
|
|
posY: action.nodeData.position.y
|
|
});
|
|
|
|
// Store new backend ID
|
|
const newBackendId = result.node_id || result;
|
|
|
|
// Re-add to frontend via reloadGraph
|
|
if (context.reloadNodeEditor) {
|
|
await context.reloadNodeEditor();
|
|
}
|
|
},
|
|
},
|
|
graphAddConnection: {
|
|
create: (trackId, fromNode, fromPort, toNode, toPort, frontendFromId, frontendToId, fromPortClass, toPortClass) => {
|
|
redoStack.length = 0;
|
|
let action = {
|
|
trackId: trackId,
|
|
fromNode: fromNode,
|
|
fromPort: fromPort,
|
|
toNode: toNode,
|
|
toPort: toPort,
|
|
frontendFromId: frontendFromId,
|
|
frontendToId: frontendToId,
|
|
fromPortClass: fromPortClass,
|
|
toPortClass: toPortClass
|
|
};
|
|
undoStack.push({ name: "graphAddConnection", action: action });
|
|
actions.graphAddConnection.execute(action);
|
|
updateMenu();
|
|
},
|
|
execute: async (action) => {
|
|
// Suppress action recording during undo/redo
|
|
if (context.nodeEditorState) {
|
|
context.nodeEditorState.suppressActionRecording = true;
|
|
}
|
|
|
|
try {
|
|
await invoke('graph_connect', {
|
|
trackId: action.trackId,
|
|
fromNode: action.fromNode,
|
|
fromPort: action.fromPort,
|
|
toNode: action.toNode,
|
|
toPort: action.toPort
|
|
});
|
|
// Add connection in frontend only if it doesn't exist
|
|
if (context.nodeEditor) {
|
|
const inputNode = context.nodeEditor.getNodeFromId(action.frontendToId);
|
|
const inputConnections = inputNode?.inputs[action.toPortClass]?.connections;
|
|
const alreadyConnected = inputConnections?.some(conn =>
|
|
conn.node === action.frontendFromId && conn.input === action.fromPortClass
|
|
);
|
|
|
|
if (!alreadyConnected) {
|
|
context.nodeEditor.addConnection(
|
|
action.frontendFromId,
|
|
action.frontendToId,
|
|
action.fromPortClass,
|
|
action.toPortClass
|
|
);
|
|
}
|
|
}
|
|
} finally {
|
|
if (context.nodeEditorState) {
|
|
context.nodeEditorState.suppressActionRecording = false;
|
|
}
|
|
}
|
|
},
|
|
rollback: async (action) => {
|
|
// Suppress action recording during undo/redo
|
|
if (context.nodeEditorState) {
|
|
context.nodeEditorState.suppressActionRecording = true;
|
|
}
|
|
|
|
try {
|
|
await invoke('graph_disconnect', {
|
|
trackId: action.trackId,
|
|
fromNode: action.fromNode,
|
|
fromPort: action.fromPort,
|
|
toNode: action.toNode,
|
|
toPort: action.toPort
|
|
});
|
|
// Remove from frontend
|
|
if (context.nodeEditor) {
|
|
context.nodeEditor.removeSingleConnection(
|
|
action.frontendFromId,
|
|
action.frontendToId,
|
|
action.fromPortClass,
|
|
action.toPortClass
|
|
);
|
|
}
|
|
} finally {
|
|
if (context.nodeEditorState) {
|
|
context.nodeEditorState.suppressActionRecording = false;
|
|
}
|
|
}
|
|
},
|
|
},
|
|
graphRemoveConnection: {
|
|
create: (trackId, fromNode, fromPort, toNode, toPort, frontendFromId, frontendToId, fromPortClass, toPortClass) => {
|
|
redoStack.length = 0;
|
|
let action = {
|
|
trackId: trackId,
|
|
fromNode: fromNode,
|
|
fromPort: fromPort,
|
|
toNode: toNode,
|
|
toPort: toPort,
|
|
frontendFromId: frontendFromId,
|
|
frontendToId: frontendToId,
|
|
fromPortClass: fromPortClass,
|
|
toPortClass: toPortClass
|
|
};
|
|
undoStack.push({ name: "graphRemoveConnection", action: action });
|
|
actions.graphRemoveConnection.execute(action);
|
|
updateMenu();
|
|
},
|
|
execute: async (action) => {
|
|
// Suppress action recording during undo/redo
|
|
if (context.nodeEditorState) {
|
|
context.nodeEditorState.suppressActionRecording = true;
|
|
}
|
|
|
|
try {
|
|
await invoke('graph_disconnect', {
|
|
trackId: action.trackId,
|
|
fromNode: action.fromNode,
|
|
fromPort: action.fromPort,
|
|
toNode: action.toNode,
|
|
toPort: action.toPort
|
|
});
|
|
if (context.nodeEditor) {
|
|
context.nodeEditor.removeSingleConnection(
|
|
action.frontendFromId,
|
|
action.frontendToId,
|
|
action.fromPortClass,
|
|
action.toPortClass
|
|
);
|
|
}
|
|
} finally {
|
|
if (context.nodeEditorState) {
|
|
context.nodeEditorState.suppressActionRecording = false;
|
|
}
|
|
}
|
|
},
|
|
rollback: async (action) => {
|
|
// Suppress action recording during undo/redo
|
|
if (context.nodeEditorState) {
|
|
context.nodeEditorState.suppressActionRecording = true;
|
|
}
|
|
|
|
try {
|
|
await invoke('graph_connect', {
|
|
trackId: action.trackId,
|
|
fromNode: action.fromNode,
|
|
fromPort: action.fromPort,
|
|
toNode: action.toNode,
|
|
toPort: action.toPort
|
|
});
|
|
// Re-add connection in frontend
|
|
if (context.nodeEditor) {
|
|
context.nodeEditor.addConnection(
|
|
action.frontendFromId,
|
|
action.frontendToId,
|
|
action.fromPortClass,
|
|
action.toPortClass
|
|
);
|
|
}
|
|
} finally {
|
|
if (context.nodeEditorState) {
|
|
context.nodeEditorState.suppressActionRecording = false;
|
|
}
|
|
}
|
|
},
|
|
},
|
|
graphSetParameter: {
|
|
initialize: (trackId, nodeId, paramId, frontendNodeId, currentValue) => {
|
|
return {
|
|
trackId: trackId,
|
|
nodeId: nodeId,
|
|
paramId: paramId,
|
|
frontendNodeId: frontendNodeId,
|
|
oldValue: currentValue,
|
|
};
|
|
},
|
|
finalize: (action, newValue) => {
|
|
action.newValue = newValue;
|
|
// Only record if value actually changed
|
|
if (action.oldValue !== action.newValue) {
|
|
undoStack.push({ name: "graphSetParameter", action: action });
|
|
updateMenu();
|
|
}
|
|
},
|
|
create: (trackId, nodeId, paramId, frontendNodeId, newValue, oldValue) => {
|
|
redoStack.length = 0;
|
|
let action = {
|
|
trackId: trackId,
|
|
nodeId: nodeId,
|
|
paramId: paramId,
|
|
frontendNodeId: frontendNodeId,
|
|
newValue: newValue,
|
|
oldValue: oldValue,
|
|
};
|
|
undoStack.push({ name: "graphSetParameter", action: action });
|
|
actions.graphSetParameter.execute(action);
|
|
updateMenu();
|
|
},
|
|
execute: async (action) => {
|
|
await invoke('graph_set_parameter', {
|
|
trackId: action.trackId,
|
|
nodeId: action.nodeId,
|
|
paramId: action.paramId,
|
|
value: action.newValue
|
|
});
|
|
// Update frontend slider if it exists
|
|
const slider = document.querySelector(`#node-${action.frontendNodeId} input[data-param="${action.paramId}"]`);
|
|
if (slider) {
|
|
slider.value = action.newValue;
|
|
// Trigger display update
|
|
slider.dispatchEvent(new Event('input'));
|
|
}
|
|
},
|
|
rollback: async (action) => {
|
|
await invoke('graph_set_parameter', {
|
|
trackId: action.trackId,
|
|
nodeId: action.nodeId,
|
|
paramId: action.paramId,
|
|
value: action.oldValue
|
|
});
|
|
// Update frontend slider
|
|
const slider = document.querySelector(`#node-${action.frontendNodeId} input[data-param="${action.paramId}"]`);
|
|
if (slider) {
|
|
slider.value = action.oldValue;
|
|
slider.dispatchEvent(new Event('input'));
|
|
}
|
|
},
|
|
},
|
|
graphMoveNode: {
|
|
create: (trackId, nodeId, oldPosition, newPosition) => {
|
|
redoStack.length = 0;
|
|
let action = {
|
|
trackId: trackId,
|
|
nodeId: nodeId,
|
|
oldPosition: oldPosition,
|
|
newPosition: newPosition,
|
|
};
|
|
undoStack.push({ name: "graphMoveNode", action: action });
|
|
// Don't call execute - movement already happened in UI
|
|
updateMenu();
|
|
},
|
|
execute: (action) => {
|
|
// Move node in frontend
|
|
if (context.nodeEditor) {
|
|
const node = context.nodeEditor.getNodeFromId(action.nodeId);
|
|
if (node) {
|
|
context.nodeEditor.drawflow.drawflow[context.nodeEditor.module].data[action.nodeId].pos_x = action.newPosition.x;
|
|
context.nodeEditor.drawflow.drawflow[context.nodeEditor.module].data[action.nodeId].pos_y = action.newPosition.y;
|
|
// Update visual position
|
|
const nodeElement = document.getElementById(`node-${action.nodeId}`);
|
|
if (nodeElement) {
|
|
nodeElement.style.left = action.newPosition.x + 'px';
|
|
nodeElement.style.top = action.newPosition.y + 'px';
|
|
}
|
|
context.nodeEditor.updateConnectionNodes(`node-${action.nodeId}`);
|
|
}
|
|
}
|
|
},
|
|
rollback: (action) => {
|
|
// Move node back to old position
|
|
if (context.nodeEditor) {
|
|
const node = context.nodeEditor.getNodeFromId(action.nodeId);
|
|
if (node) {
|
|
context.nodeEditor.drawflow.drawflow[context.nodeEditor.module].data[action.nodeId].pos_x = action.oldPosition.x;
|
|
context.nodeEditor.drawflow.drawflow[context.nodeEditor.module].data[action.nodeId].pos_y = action.oldPosition.y;
|
|
const nodeElement = document.getElementById(`node-${action.nodeId}`);
|
|
if (nodeElement) {
|
|
nodeElement.style.left = action.oldPosition.x + 'px';
|
|
nodeElement.style.top = action.oldPosition.y + 'px';
|
|
}
|
|
context.nodeEditor.updateConnectionNodes(`node-${action.nodeId}`);
|
|
}
|
|
}
|
|
},
|
|
},
|
|
};
|