/** * 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(); } }