/** * Mouse event handlers for canvas */ import { getMousePos } from "../utils/canvas.js"; import { generateUUID } from "../utils/uuid.js"; import { evaluateFrame } from "../core/frameEvaluator.js"; import { pointToLineSegmentDistance } from "../renderer/drawCurve.js"; /** * Create mouse event handlers for canvas * @param {HTMLCanvasElement} canvas - Canvas element * @param {HTMLElement} container - Container element * @param {Object} state - Editor state * @param {Object} callbacks - Callback functions * @param {Function} callbacks.getActiveShape - Get active shape * @param {Function} callbacks.promoteToKeyframe - Promote to keyframe * @param {Function} callbacks.commitFrame - Commit frame * @param {Function} callbacks.redraw - Redraw canvas * @param {Function} callbacks.addPointToCurveAtEdge - Add point to curve at edge * @param {Function} callbacks.updateAddPointButton - Update add point button * @param {Function} callbacks.attachDocumentListeners - Attach document listeners * @returns {Function} Function to handle drag move */ export function createMouseHandlers(canvas, container, state, callbacks) { // Shared function to handle dragging function handleDragMove(e) { if (!state.dragging || state.mode !== "edit") return; const pos = getMousePos(e, canvas); const deltaX = pos.x - state.dragStart.x; const deltaY = pos.y - state.dragStart.y; const shape = callbacks.getActiveShape(); const frameKey = String(state.currentFrame); // Promote only the curves being edited state.selectedPoints.forEach(pointKey => { const [curveId] = pointKey.split(":"); callbacks.promoteToKeyframe(curveId); }); const curves = shape.frames[frameKey] || []; // Move selected points state.selectedPoints.forEach(pointKey => { const [curveId, pointIndex] = pointKey.split(":"); const curve = curves.find(c => c.id === curveId); if (curve && curve.points[parseInt(pointIndex)]) { const point = curve.points[parseInt(pointIndex)]; point[0] = Math.max(0, Math.min(canvas.width, point[0] + deltaX)); point[1] = Math.max(0, Math.min(canvas.height, point[1] + deltaY)); } }); state.dragStart = pos; callbacks.commitFrame(); callbacks.redraw(); } canvas.addEventListener("mousedown", (e) => { // Ensure keyboard shortcuts work after interaction container.focus(); const pos = getMousePos(e, canvas); if (state.mode === "draw") { // Create new curve - don't promote other curves, just ensure frame exists const shape = callbacks.getActiveShape(); const frameKey = String(state.currentFrame); if (!shape.frames[frameKey]) { shape.frames[frameKey] = []; } // Create 4-point box (rectangle) centered on click // Clamp to canvas bounds const clampedX = Math.max(0, Math.min(canvas.width, pos.x)); const clampedY = Math.max(0, Math.min(canvas.height, pos.y)); const size = 50; const curve = { id: generateUUID(), closed: true, points: [ [Math.max(0, clampedX - size), Math.max(0, clampedY - size)], // top-left [Math.min(canvas.width, clampedX + size), Math.max(0, clampedY - size)], // top-right [Math.min(canvas.width, clampedX + size), Math.min(canvas.height, clampedY + size)], // bottom-right [Math.max(0, clampedX - size), Math.min(canvas.height, clampedY + size)] // bottom-left ], pointTypes: ["hard", "hard", "hard", "hard"] // Initialize all points as hard }; shape.frames[frameKey].push(curve); callbacks.commitFrame(); callbacks.redraw(); } else if (state.mode === "edit") { const shape = callbacks.getActiveShape(); const frameKey = String(state.currentFrame); // Check for point hit using EVALUATED curves (includes held curves) const evaluatedCurves = evaluateFrame(shape, state.currentFrame, { hold: true }); let hitPoint = null; let hitCurve = null; for (const curve of evaluatedCurves) { for (let i = 0; i < curve.points.length; i++) { const point = curve.points[i]; const dx = pos.x - point[0]; const dy = pos.y - point[1]; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < 10) { hitPoint = { curve, index: i }; hitCurve = curve; break; } } if (hitPoint) break; } if (hitPoint) { // Start dragging point state.dragging = true; state.dragStart = pos; const pointKey = `${hitPoint.curve.id}:${hitPoint.index}`; // Check if the clicked point is already selected const isPointAlreadySelected = state.selectedPoints.has(pointKey); if (!e.shiftKey && !isPointAlreadySelected) { // Only clear selection if clicking a new point (not already selected) state.selectedPoints.clear(); state.selectedCurves.clear(); } // Add the point to selection (if not already there) state.selectedPoints.add(pointKey); state.selectedCurves.add(hitPoint.curve.id); // Attach document listeners to allow dragging outside canvas callbacks.attachDocumentListeners(); callbacks.redraw(); // Update toggle smooth button state if (callbacks.updateToggleSmoothButton) { callbacks.updateToggleSmoothButton(); } } else { // Only check for edge hit if in add point mode if (state.addPointMode) { let hitEdge = null; let minDist = Infinity; const edgeHitThreshold = 15; // pixels - slightly larger for easier clicking // Only check selected curves const selectedCurveId = Array.from(state.selectedCurves)[0]; for (const curve of evaluatedCurves) { if (curve.id !== selectedCurveId) continue; for (let i = 0; i < curve.points.length; i++) { const p1 = curve.points[i]; const p2 = curve.points[(i + 1) % curve.points.length]; // Calculate distance from mouse to line segment const dist = pointToLineSegmentDistance(pos, p1, p2); if (dist < edgeHitThreshold && dist < minDist) { minDist = dist; // Calculate closest point on line segment const t = Math.max(0, Math.min(1, ((pos.x - p1[0]) * (p2[0] - p1[0]) + (pos.y - p1[1]) * (p2[1] - p1[1])) / ((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2) )); const closestPoint = { x: p1[0] + t * (p2[0] - p1[0]), y: p1[1] + t * (p2[1] - p1[1]) }; hitEdge = { curve: curve, edgeIndex: i, position: closestPoint }; } } } if (hitEdge) { // Add point at clicked location on edge if (callbacks.addPointToCurveAtEdge) { callbacks.addPointToCurveAtEdge(hitEdge.curve.id, hitEdge.edgeIndex, hitEdge.position); } else { console.error("[ComfyUI Roto] addPointToCurveAtEdge callback not available"); } } else { // Clicked but missed the edge - cancel add point mode state.addPointMode = false; if (callbacks.updateAddPointButton) { callbacks.updateAddPointButton(); } } } else { // Normal edit mode - start selection box if clicking empty space // Clear selection box if it exists (shouldn't happen, but safety check) if (state.selectionBox) { state.selectionBox = null; } // Start selection box state.selectionBox = { start: pos, end: pos, shiftKey: e.shiftKey }; // If Shift not held, clear existing selection if (!e.shiftKey) { state.selectedCurves.clear(); state.selectedPoints.clear(); } callbacks.redraw(); // Update toggle smooth button state if (callbacks.updateToggleSmoothButton) { callbacks.updateToggleSmoothButton(); } } } } }); canvas.addEventListener("mousemove", (e) => { // Check for selection box first (before dragging) if (state.selectionBox !== null && state.mode === "edit") { const pos = getMousePos(e, canvas); state.selectionBox.end = pos; callbacks.redraw(); } else { // Handle point dragging handleDragMove(e); } }); canvas.addEventListener("mouseup", (e) => { // Handle selection box completion if (state.selectionBox !== null && state.mode === "edit") { const shape = callbacks.getActiveShape(); const evaluatedCurves = evaluateFrame(shape, state.currentFrame, { hold: true }); // Calculate normalized box bounds const minX = Math.min(state.selectionBox.start.x, state.selectionBox.end.x); const maxX = Math.max(state.selectionBox.start.x, state.selectionBox.end.x); const minY = Math.min(state.selectionBox.start.y, state.selectionBox.end.y); const maxY = Math.max(state.selectionBox.start.y, state.selectionBox.end.y); // Find all points within selection box for (const curve of evaluatedCurves) { for (let i = 0; i < curve.points.length; i++) { const point = curve.points[i]; const x = point[0]; const y = point[1]; // Check if point center is within box bounds if (x >= minX && x <= maxX && y >= minY && y <= maxY) { const pointKey = `${curve.id}:${i}`; state.selectedPoints.add(pointKey); state.selectedCurves.add(curve.id); } } } // Clear selection box state.selectionBox = null; callbacks.redraw(); // Update toggle smooth button state if (callbacks.updateToggleSmoothButton) { callbacks.updateToggleSmoothButton(); } } else if (state.dragging) { state.dragging = false; state.dragStart = null; callbacks.removeDocumentListeners(); } }); return handleDragMove; }