239 lines
8.1 KiB
JavaScript
239 lines
8.1 KiB
JavaScript
/**
|
|
* Curve management functions
|
|
*/
|
|
|
|
import { generateUUID } from "../utils/uuid.js";
|
|
import { deepCopy } from "../utils/helpers.js";
|
|
import { evaluateFrame } from "./frameEvaluator.js";
|
|
|
|
/**
|
|
* Get the next available shape number
|
|
* @param {Object} state - Editor state
|
|
* @returns {number} Next available shape number
|
|
*/
|
|
function getNextShapeNumber(state) {
|
|
const existingNumbers = new Set();
|
|
|
|
// Extract numbers from existing shape names
|
|
Object.values(state.shapes || {}).forEach(shape => {
|
|
if (shape && shape.name) {
|
|
const match = shape.name.match(/^Shape\s+(\d+)$/);
|
|
if (match) {
|
|
existingNumbers.add(parseInt(match[1], 10));
|
|
}
|
|
}
|
|
});
|
|
|
|
// Find the next available number
|
|
let nextNumber = 1;
|
|
while (existingNumbers.has(nextNumber)) {
|
|
nextNumber++;
|
|
}
|
|
|
|
return nextNumber;
|
|
}
|
|
|
|
/**
|
|
* Initialize default shape in state
|
|
* @param {Object} state - Editor state
|
|
*/
|
|
export function initializeDefaultShape(state) {
|
|
const shapeId = generateUUID();
|
|
const shapeNumber = getNextShapeNumber(state);
|
|
state.activeShapeId = shapeId;
|
|
state.shapes[shapeId] = {
|
|
name: `Shape ${shapeNumber}`,
|
|
frames: {}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get the active shape, initializing if needed
|
|
* @param {Object} state - Editor state
|
|
* @returns {Object} Active shape object
|
|
*/
|
|
export function getActiveShape(state) {
|
|
if (!state.activeShapeId || !state.shapes[state.activeShapeId]) {
|
|
initializeDefaultShape(state);
|
|
}
|
|
return state.shapes[state.activeShapeId];
|
|
}
|
|
|
|
/**
|
|
* Get curves for a specific frame
|
|
* @param {Object} state - Editor state
|
|
* @param {number} frame - Frame number
|
|
* @returns {Array} Curves for the frame
|
|
*/
|
|
export function getCurvesForFrame(state, frame) {
|
|
const shape = getActiveShape(state);
|
|
const frameKey = String(frame);
|
|
return shape.frames[frameKey] || [];
|
|
}
|
|
|
|
/**
|
|
* Promote a specific curve to the current frame as a keyframe.
|
|
* If curveId is provided, only that curve is promoted.
|
|
* If not provided, only interpolated curves are promoted (not held ones).
|
|
*
|
|
* @param {Object} state - Editor state
|
|
* @param {Function} evaluateFrameFn - Function to evaluate frame (to avoid circular dependency)
|
|
* @param {Function} deepCopyFn - Deep copy function
|
|
* @param {string|null} curveId - Optional curve ID to promote
|
|
*/
|
|
export function promoteToKeyframe(state, evaluateFrameFn, deepCopyFn, curveId = null) {
|
|
const shape = getActiveShape(state);
|
|
const frameKey = String(state.currentFrame);
|
|
|
|
// If already a keyframe, check if we need to add a specific curve
|
|
if (shape.frames[frameKey]) {
|
|
if (curveId) {
|
|
// Check if this curve already exists at this keyframe
|
|
const exists = shape.frames[frameKey].some(c => c.id === curveId);
|
|
if (!exists) {
|
|
// Find this curve from evaluated frame and add it
|
|
const allCurves = evaluateFrameFn(shape, state.currentFrame, { hold: true });
|
|
const curve = allCurves.find(c => c.id === curveId);
|
|
if (curve) {
|
|
shape.frames[frameKey].push(deepCopyFn(curve));
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Frame doesn't exist yet - create it
|
|
if (curveId) {
|
|
// Only promote the specific curve being edited
|
|
const allCurves = evaluateFrameFn(shape, state.currentFrame, { hold: true });
|
|
const curve = allCurves.find(c => c.id === curveId);
|
|
shape.frames[frameKey] = curve ? [deepCopyFn(curve)] : [];
|
|
} else {
|
|
// Get interpolated curves only (not held curves)
|
|
const curves = evaluateFrameFn(shape, state.currentFrame, { hold: false });
|
|
shape.frames[frameKey] = deepCopyFn(curves);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a point to a curve at a specific edge.
|
|
* IMPORTANT: Adds point to ALL keyframes to keep point counts in sync.
|
|
*
|
|
* @param {Object} state - Editor state
|
|
* @param {string} curveId - Curve ID
|
|
* @param {number} edgeIndex - Index of the edge (point index)
|
|
* @param {{x: number, y: number}|null} position - Optional position to add point at
|
|
* @param {Function} promoteToKeyframeFn - Function to promote to keyframe
|
|
* @param {Function} evaluateFrameFn - Function to evaluate frame
|
|
* @param {Function} deepCopyFn - Deep copy function
|
|
* @param {Function} commitFrameFn - Function to commit frame
|
|
* @param {Function} redrawFn - Function to redraw
|
|
* @param {Function} updateAddPointButtonFn - Function to update add point button
|
|
*/
|
|
export function addPointToCurveAtEdge(
|
|
state,
|
|
curveId,
|
|
edgeIndex,
|
|
position,
|
|
promoteToKeyframeFn,
|
|
evaluateFrameFn,
|
|
deepCopyFn,
|
|
commitFrameFn,
|
|
redrawFn,
|
|
updateAddPointButtonFn
|
|
) {
|
|
const shape = getActiveShape(state);
|
|
const frameKey = String(state.currentFrame);
|
|
|
|
// First, ensure current frame has this curve as a keyframe
|
|
promoteToKeyframeFn(state, evaluateFrameFn, deepCopyFn, curveId);
|
|
|
|
// Get the actual keyframe curve (not interpolated) - refresh after promotion
|
|
if (!shape.frames[frameKey]) {
|
|
shape.frames[frameKey] = [];
|
|
}
|
|
const curves = shape.frames[frameKey];
|
|
let curve = curves.find(c => c.id === curveId);
|
|
|
|
// If curve still doesn't exist, something went wrong - try to get from evaluated
|
|
if (!curve) {
|
|
const evaluatedCurves = evaluateFrameFn(shape, state.currentFrame, { hold: true });
|
|
const evaluatedCurve = evaluatedCurves.find(c => c.id === curveId);
|
|
if (evaluatedCurve) {
|
|
// Add it to keyframe
|
|
curve = deepCopyFn(evaluatedCurve);
|
|
curves.push(curve);
|
|
}
|
|
}
|
|
|
|
if (!curve || curve.points.length < 2) return;
|
|
|
|
// Calculate t parameter (0-1) for where on the edge the point is being added
|
|
const p1 = curve.points[edgeIndex];
|
|
const p2 = curve.points[(edgeIndex + 1) % curve.points.length];
|
|
let t = 0.5; // default to midpoint
|
|
|
|
if (position) {
|
|
// Calculate t based on position along the edge
|
|
const edgeLength = Math.sqrt((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2);
|
|
if (edgeLength > 0) {
|
|
const posLength = Math.sqrt((position.x - p1[0]) ** 2 + (position.y - p1[1]) ** 2);
|
|
t = Math.max(0, Math.min(1, posLength / edgeLength));
|
|
}
|
|
}
|
|
|
|
// Add point to ALL keyframes that contain this curve
|
|
Object.keys(shape.frames).forEach(fKey => {
|
|
const frameCurves = shape.frames[fKey] || [];
|
|
const frameCurve = frameCurves.find(c => c.id === curveId);
|
|
|
|
if (frameCurve && frameCurve.points.length > edgeIndex) {
|
|
const fp1 = frameCurve.points[edgeIndex];
|
|
const fp2 = frameCurve.points[(edgeIndex + 1) % frameCurve.points.length];
|
|
|
|
// Calculate new point position using same t parameter
|
|
const newX = fp1[0] + t * (fp2[0] - fp1[0]);
|
|
const newY = fp1[1] + t * (fp2[1] - fp1[1]);
|
|
|
|
// Insert new point after edgeIndex
|
|
frameCurve.points.splice(edgeIndex + 1, 0, [newX, newY]);
|
|
|
|
// Also insert "hard" into pointTypes array at same index
|
|
if (!Array.isArray(frameCurve.pointTypes) || frameCurve.pointTypes.length !== frameCurve.points.length - 1) {
|
|
// Reinitialize if missing or wrong length (before adding new point)
|
|
frameCurve.pointTypes = frameCurve.points.slice(0, -1).map(() => "hard");
|
|
}
|
|
frameCurve.pointTypes.splice(edgeIndex + 1, 0, "hard");
|
|
}
|
|
});
|
|
|
|
// Exit add point mode
|
|
state.addPointMode = false;
|
|
if (updateAddPointButtonFn) {
|
|
updateAddPointButtonFn();
|
|
}
|
|
|
|
if (commitFrameFn) {
|
|
commitFrameFn();
|
|
}
|
|
if (redrawFn) {
|
|
redrawFn();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Commit current frame (ensure frame exists in shape)
|
|
* @param {Object} state - Editor state
|
|
* @param {Function} updateNodeInputFn - Function to update node input
|
|
*/
|
|
export function commitFrame(state, updateNodeInputFn) {
|
|
const shape = getActiveShape(state);
|
|
const frameKey = String(state.currentFrame);
|
|
if (!shape.frames[frameKey]) {
|
|
shape.frames[frameKey] = [];
|
|
}
|
|
if (updateNodeInputFn) {
|
|
updateNodeInputFn();
|
|
}
|
|
}
|