292 lines
12 KiB
JavaScript
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;
|
|
}
|
|
|