Files
2026-01-28 19:33:35 +13:00

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