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

292 lines
12 KiB
JavaScript

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