525 lines
24 KiB
JavaScript
525 lines
24 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
|
import { ComfyWidgets } from "../../scripts/widgets.js";
|
|
|
|
// Register custom widget type
|
|
app.registerExtension({
|
|
name: "RyanOnTheInside.DrawableFeature",
|
|
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
|
// console.log("Registering DrawableFeature extension", { nodeType, nodeData });
|
|
|
|
if (nodeData.name === "DrawableFeatureNode") {
|
|
// console.log("Found DrawableFeatureNode, setting up widget");
|
|
|
|
// Set default size
|
|
nodeType.size = [700, 800]; // Increased size for better usability
|
|
|
|
// Store the original methods
|
|
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
|
const onDrawForeground = nodeType.prototype.onDrawForeground;
|
|
const onMouseDown = nodeType.prototype.onMouseDown;
|
|
const onMouseMove = nodeType.prototype.onMouseMove;
|
|
const onMouseUp = nodeType.prototype.onMouseUp;
|
|
const onDblClick = nodeType.prototype.onDblClick;
|
|
const onResize = nodeType.prototype.onResize;
|
|
const onSerialize = nodeType.prototype.onSerialize;
|
|
const onConfigure = nodeType.prototype.onConfigure;
|
|
|
|
// Add serialization support
|
|
nodeType.prototype.onSerialize = function(o) {
|
|
if (onSerialize) {
|
|
onSerialize.apply(this, arguments);
|
|
}
|
|
o.points = this.points;
|
|
};
|
|
|
|
// Add deserialization support
|
|
nodeType.prototype.onConfigure = function(o) {
|
|
if (onConfigure) {
|
|
onConfigure.apply(this, arguments);
|
|
}
|
|
if (o.points) {
|
|
this.points = o.points;
|
|
this.updatePointsValue();
|
|
}
|
|
};
|
|
|
|
// Override onNodeCreated to initialize the node
|
|
nodeType.prototype.onNodeCreated = function() {
|
|
// console.log("Node created, initializing node");
|
|
const r = onNodeCreated?.apply(this, arguments);
|
|
|
|
// Remove default points widget if it exists
|
|
const pointsWidgetIndex = this.widgets.findIndex(w => w.name === "points");
|
|
if (pointsWidgetIndex > -1) {
|
|
this.widgets.splice(pointsWidgetIndex, 1);
|
|
}
|
|
|
|
// Initialize state
|
|
this.points = [];
|
|
this.isDragging = false;
|
|
this.selectedPoint = null;
|
|
this.hoverPoint = null;
|
|
this.isAddingPoint = false;
|
|
|
|
// Add hidden points widget at the end
|
|
this.widgets.push({
|
|
type: "text",
|
|
name: "points",
|
|
value: "[]",
|
|
hidden: true
|
|
});
|
|
|
|
// Restore points from widget value
|
|
try {
|
|
const savedPoints = JSON.parse(this.widgets[this.widgets.length - 1].value);
|
|
if (Array.isArray(savedPoints)) {
|
|
this.points = savedPoints;
|
|
}
|
|
} catch (e) {
|
|
console.error("Failed to restore points:", e);
|
|
}
|
|
|
|
// Add clear button widget
|
|
this.addWidget("button", "Clear Graph", "clear", () => {
|
|
this.points = [];
|
|
this.updatePointsValue();
|
|
this.setDirtyCanvas(true, true);
|
|
});
|
|
|
|
// Add handlers for min/max value changes
|
|
const minValueWidget = this.widgets.find(w => w.name === "min_value");
|
|
const maxValueWidget = this.widgets.find(w => w.name === "max_value");
|
|
|
|
if (minValueWidget) {
|
|
const originalCallback = minValueWidget.callback;
|
|
minValueWidget.callback = (v) => {
|
|
const result = originalCallback?.call(this, v);
|
|
this.clampPoints();
|
|
return result;
|
|
};
|
|
}
|
|
|
|
if (maxValueWidget) {
|
|
const originalCallback = maxValueWidget.callback;
|
|
maxValueWidget.callback = (v) => {
|
|
const result = originalCallback?.call(this, v);
|
|
this.clampPoints();
|
|
return result;
|
|
};
|
|
}
|
|
|
|
return r;
|
|
};
|
|
|
|
// Add method to clamp points to min/max range
|
|
nodeType.prototype.clampPoints = function() {
|
|
if (!this.points || this.points.length === 0) return;
|
|
|
|
// Get current min/max values
|
|
const minValue = this.widgets.find(w => w.name === "min_value")?.value ?? 0;
|
|
const maxValue = this.widgets.find(w => w.name === "max_value")?.value ?? 1;
|
|
|
|
// Clamp any out-of-bounds points
|
|
let needsUpdate = false;
|
|
this.points = this.points.map(([frame, value]) => {
|
|
const clampedValue = Math.min(Math.max(value, minValue), maxValue);
|
|
if (clampedValue !== value) needsUpdate = true;
|
|
return [frame, clampedValue];
|
|
});
|
|
|
|
// Only update if points were actually clamped
|
|
if (needsUpdate) {
|
|
this.updatePointsValue();
|
|
this.setDirtyCanvas(true, true);
|
|
}
|
|
};
|
|
|
|
// Calculate widget area height
|
|
nodeType.prototype.getWidgetAreaHeight = function() {
|
|
let height = 0;
|
|
const titleHeight = 30;
|
|
const widgetSpacing = 4;
|
|
|
|
height += titleHeight;
|
|
|
|
for (const w of this.widgets) {
|
|
if (!w.hidden) {
|
|
height += w.computeSize?.[1] || 20;
|
|
height += widgetSpacing;
|
|
}
|
|
}
|
|
|
|
return height + 10; // Add padding
|
|
};
|
|
|
|
// Override onDrawForeground to draw the graph
|
|
nodeType.prototype.onDrawForeground = function(ctx) {
|
|
if (onDrawForeground) {
|
|
onDrawForeground.apply(this, arguments);
|
|
}
|
|
|
|
if (this.flags.collapsed) return;
|
|
|
|
// Calculate graph dimensions based on node size and widget area
|
|
const margin = 30;
|
|
const widgetAreaHeight = this.getWidgetAreaHeight();
|
|
const graphWidth = this.size[0] - 2 * margin;
|
|
const graphHeight = Math.max(200, this.size[1] - widgetAreaHeight - margin * 2);
|
|
const graphY = widgetAreaHeight + margin;
|
|
|
|
// Draw background
|
|
ctx.fillStyle = "#1a1a1a";
|
|
ctx.fillRect(margin, graphY, graphWidth, graphHeight);
|
|
ctx.strokeStyle = "#666";
|
|
ctx.strokeRect(margin, graphY, graphWidth, graphHeight);
|
|
|
|
// Draw help text
|
|
ctx.fillStyle = "#888";
|
|
ctx.font = "12px Arial";
|
|
ctx.textAlign = "center";
|
|
ctx.fillText("Click empty space to add • Click point to delete • Drag points to move",
|
|
margin + graphWidth / 2, graphY + 20);
|
|
|
|
// Draw grid
|
|
ctx.strokeStyle = "#333";
|
|
ctx.lineWidth = 0.5;
|
|
|
|
// Vertical lines (frames) and labels
|
|
const frameCount = this.widgets.find(w => w.name === "frame_count")?.value || 30;
|
|
const maxFrame = frameCount - 1; // Maximum valid frame
|
|
const frameStep = Math.max(1, Math.floor(maxFrame / 10));
|
|
ctx.fillStyle = "#888";
|
|
ctx.font = "10px Arial";
|
|
ctx.textAlign = "center";
|
|
|
|
for (let f = 0; f <= maxFrame; f += frameStep) {
|
|
const x = margin + (f / maxFrame) * graphWidth;
|
|
ctx.beginPath();
|
|
ctx.moveTo(x, graphY);
|
|
ctx.lineTo(x, graphY + graphHeight);
|
|
ctx.stroke();
|
|
// Frame number labels
|
|
ctx.fillText(f.toString(), x, graphY + graphHeight + 15);
|
|
}
|
|
|
|
// Get min/max values from widgets
|
|
const minValue = this.widgets.find(w => w.name === "min_value")?.value ?? 0;
|
|
const maxValue = this.widgets.find(w => w.name === "max_value")?.value ?? 1;
|
|
const valueRange = maxValue - minValue;
|
|
|
|
// Horizontal lines (values) and labels
|
|
ctx.textAlign = "right";
|
|
for (let v = 0; v <= 1; v += 0.1) {
|
|
const y = graphY + v * graphHeight;
|
|
ctx.beginPath();
|
|
ctx.moveTo(margin, y);
|
|
ctx.lineTo(margin + graphWidth, y);
|
|
ctx.stroke();
|
|
// Value labels
|
|
const value = maxValue - (v * valueRange);
|
|
ctx.fillText(value.toFixed(1), margin - 5, y + 4);
|
|
}
|
|
|
|
// Axis labels
|
|
ctx.save();
|
|
ctx.translate(margin - 25, graphY + graphHeight / 2);
|
|
ctx.rotate(-Math.PI / 2);
|
|
ctx.textAlign = "center";
|
|
ctx.fillText("Value", 0, 0);
|
|
ctx.restore();
|
|
|
|
ctx.textAlign = "center";
|
|
ctx.fillText("Frame", margin + graphWidth / 2, graphY + graphHeight + 30);
|
|
|
|
// Draw points and lines
|
|
if (this.points && this.points.length > 0) {
|
|
// Draw lines between points
|
|
ctx.strokeStyle = "#fff";
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
|
|
const points = this.points.map(([frame, value]) => ({
|
|
x: margin + (frame / maxFrame) * graphWidth,
|
|
y: graphY + (1 - this.normalizeValue(value)) * graphHeight
|
|
}));
|
|
|
|
ctx.moveTo(points[0].x, points[0].y);
|
|
for (let i = 1; i < points.length; i++) {
|
|
ctx.lineTo(points[i].x, points[i].y);
|
|
}
|
|
ctx.stroke();
|
|
|
|
// Draw points with hover and selection effects
|
|
points.forEach((point, i) => {
|
|
ctx.beginPath();
|
|
ctx.arc(point.x, point.y, i === this.selectedPoint ? 7 : 5, 0, Math.PI * 2);
|
|
|
|
if (i === this.selectedPoint) {
|
|
ctx.fillStyle = "#00ff00";
|
|
} else if (i === this.hoverPoint) {
|
|
ctx.fillStyle = "#ffff00";
|
|
} else {
|
|
ctx.fillStyle = "#fff";
|
|
}
|
|
|
|
ctx.fill();
|
|
|
|
if (i === this.selectedPoint || i === this.hoverPoint) {
|
|
ctx.strokeStyle = "#000";
|
|
ctx.lineWidth = 2;
|
|
ctx.stroke();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Draw potential new point position
|
|
if (this.isAddingPoint && this.mousePos) {
|
|
const [frame, value] = this.coordsToGraphValues(this.mousePos[0], this.mousePos[1]);
|
|
if (frame >= 0 && frame < frameCount) {
|
|
const x = margin + (frame / maxFrame) * graphWidth;
|
|
const y = graphY + (1 - this.normalizeValue(value)) * graphHeight;
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, 5, 0, Math.PI * 2);
|
|
ctx.fillStyle = "rgba(255, 255, 255, 0.5)";
|
|
ctx.fill();
|
|
ctx.strokeStyle = "#fff";
|
|
ctx.lineWidth = 1;
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
};
|
|
|
|
// Normalize value to 0-1 range based on min/max widgets
|
|
nodeType.prototype.normalizeValue = function(value) {
|
|
const minValue = this.widgets.find(w => w.name === "min_value")?.value ?? 0;
|
|
const maxValue = this.widgets.find(w => w.name === "max_value")?.value ?? 1;
|
|
return (value - minValue) / (maxValue - minValue);
|
|
};
|
|
|
|
// Denormalize value from 0-1 range to min/max range
|
|
nodeType.prototype.denormalizeValue = function(normalized) {
|
|
const minValue = this.widgets.find(w => w.name === "min_value")?.value ?? 0;
|
|
const maxValue = this.widgets.find(w => w.name === "max_value")?.value ?? 1;
|
|
return normalized * (maxValue - minValue) + minValue;
|
|
};
|
|
|
|
// Convert coordinates to graph values
|
|
nodeType.prototype.coordsToGraphValues = function(x, y) {
|
|
const margin = 30;
|
|
const widgetAreaHeight = this.getWidgetAreaHeight();
|
|
const graphWidth = this.size[0] - 2 * margin;
|
|
const graphHeight = Math.max(200, this.size[1] - widgetAreaHeight - margin * 2);
|
|
const graphY = widgetAreaHeight + margin;
|
|
|
|
const frameCount = this.widgets.find(w => w.name === "frame_count")?.value || 30;
|
|
const maxFrame = frameCount - 1;
|
|
|
|
// Calculate frame (x value) and clamp to valid range
|
|
const frame = Math.round(((x - margin) / graphWidth) * maxFrame);
|
|
const clampedFrame = Math.max(0, Math.min(maxFrame, frame));
|
|
|
|
// Calculate value (y value) - normalize based on graph position
|
|
const normalizedY = Math.max(0, Math.min(1, (y - graphY) / graphHeight));
|
|
const value = this.denormalizeValue(1 - normalizedY);
|
|
|
|
return [clampedFrame, value];
|
|
};
|
|
|
|
// Check if mouse is over graph area
|
|
nodeType.prototype.isMouseOverGraph = function(x, y) {
|
|
const margin = 30;
|
|
const widgetAreaHeight = this.getWidgetAreaHeight();
|
|
const graphWidth = this.size[0] - 2 * margin;
|
|
const graphHeight = Math.max(200, this.size[1] - widgetAreaHeight - margin * 2);
|
|
const graphY = widgetAreaHeight + margin;
|
|
|
|
return x >= margin &&
|
|
x <= this.size[0] - margin &&
|
|
y >= graphY &&
|
|
y <= graphY + graphHeight;
|
|
};
|
|
|
|
// Find point near coordinates
|
|
nodeType.prototype.findNearPoint = function(x, y) {
|
|
const margin = 30;
|
|
const widgetAreaHeight = this.getWidgetAreaHeight();
|
|
const graphWidth = this.size[0] - 2 * margin;
|
|
const graphHeight = Math.max(200, this.size[1] - widgetAreaHeight - margin * 2);
|
|
const graphY = widgetAreaHeight + margin;
|
|
const frameCount = this.widgets.find(w => w.name === "frame_count")?.value || 30;
|
|
const maxFrame = frameCount - 1;
|
|
|
|
for (let i = 0; i < this.points.length; i++) {
|
|
const [frame, value] = this.points[i];
|
|
const px = margin + (frame / maxFrame) * graphWidth;
|
|
const py = graphY + (1 - this.normalizeValue(value)) * graphHeight;
|
|
const dist = Math.sqrt((x - px) ** 2 + (y - py) ** 2);
|
|
if (dist < 10) return i;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// Update the hidden points value
|
|
nodeType.prototype.updatePointsValue = function() {
|
|
const pointsStr = JSON.stringify(this.points);
|
|
// Find or create a hidden widget to store the points
|
|
let pointsWidget = this.widgets.find(w => w.name === "points");
|
|
if (!pointsWidget) {
|
|
// Create new points widget at the end of the list
|
|
pointsWidget = {
|
|
type: "text",
|
|
name: "points",
|
|
value: "[]",
|
|
hidden: true
|
|
};
|
|
this.widgets.push(pointsWidget);
|
|
}
|
|
pointsWidget.value = pointsStr;
|
|
// Trigger widget change to ensure value is saved
|
|
if (this.onWidgetChanged) {
|
|
this.onWidgetChanged(pointsWidget.name, pointsWidget.value, pointsWidget.value, pointsWidget);
|
|
}
|
|
};
|
|
|
|
// Override mouse handlers for improved drawing
|
|
nodeType.prototype.onMouseDown = function(e, pos) {
|
|
const [x, y] = pos;
|
|
if (!this.isMouseOverGraph(x, y)) return false;
|
|
|
|
const pointIndex = this.findNearPoint(x, y);
|
|
|
|
if (pointIndex !== null) {
|
|
this.selectedPoint = pointIndex;
|
|
this.dragStartPos = [...pos];
|
|
} else {
|
|
// Not on a point - add new point
|
|
const [frame, value] = this.coordsToGraphValues(x, y);
|
|
const frameCount = this.widgets.find(w => w.name === "frame_count")?.value || 30;
|
|
const maxFrame = frameCount - 1;
|
|
if (frame >= 0 && frame <= maxFrame) {
|
|
// Remove any existing point at the same frame
|
|
const existingIndex = this.points.findIndex(p => p[0] === frame);
|
|
if (existingIndex !== -1) {
|
|
this.points.splice(existingIndex, 1);
|
|
}
|
|
|
|
this.points.push([frame, value]);
|
|
this.points.sort((a, b) => a[0] - b[0]);
|
|
this.selectedPoint = this.points.findIndex(p => p[0] === frame);
|
|
this.updatePointsValue();
|
|
}
|
|
}
|
|
|
|
this.setDirtyCanvas(true, true);
|
|
return true;
|
|
};
|
|
|
|
nodeType.prototype.onMouseMove = function(e, pos) {
|
|
const [x, y] = pos;
|
|
this.mousePos = pos;
|
|
|
|
if (!this.isMouseOverGraph(x, y)) {
|
|
// Clear all states when mouse leaves graph area
|
|
this.hoverPoint = null;
|
|
this.isAddingPoint = false;
|
|
this.selectedPoint = null;
|
|
this.isDragging = false;
|
|
this.dragStartPos = null;
|
|
this.setDirtyCanvas(true, true);
|
|
return false;
|
|
}
|
|
|
|
if (this.dragStartPos && this.selectedPoint !== null) {
|
|
// If we've moved more than 5 pixels, start dragging
|
|
const dragDist = Math.sqrt(
|
|
Math.pow(pos[0] - this.dragStartPos[0], 2) +
|
|
Math.pow(pos[1] - this.dragStartPos[1], 2)
|
|
);
|
|
|
|
if (dragDist > 5) {
|
|
this.isDragging = true;
|
|
const [frame, value] = this.coordsToGraphValues(x, y);
|
|
const frameCount = this.widgets.find(w => w.name === "frame_count")?.value || 30;
|
|
const maxFrame = frameCount - 1;
|
|
// Keep strict < frameCount for dragging to prevent edge issues
|
|
if (frame >= 0 && frame <= maxFrame) {
|
|
// Check if there's already a point at the target frame (except selected point)
|
|
const existingIndex = this.points.findIndex((p, i) =>
|
|
i !== this.selectedPoint && p[0] === frame);
|
|
|
|
if (existingIndex === -1) {
|
|
this.points[this.selectedPoint] = [frame, value];
|
|
this.points.sort((a, b) => a[0] - b[0]);
|
|
this.selectedPoint = this.points.findIndex(p => p[0] === frame);
|
|
this.updatePointsValue();
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Update hover state
|
|
this.hoverPoint = this.findNearPoint(x, y);
|
|
this.isAddingPoint = this.hoverPoint === null;
|
|
}
|
|
|
|
this.setDirtyCanvas(true, true);
|
|
return true;
|
|
};
|
|
|
|
nodeType.prototype.onMouseUp = function(e, pos) {
|
|
// If we have a selected point but never started dragging, it's a click - delete the point
|
|
if (this.selectedPoint !== null && !this.isDragging) {
|
|
this.points.splice(this.selectedPoint, 1);
|
|
this.updatePointsValue();
|
|
}
|
|
|
|
this.isDragging = false;
|
|
this.selectedPoint = null;
|
|
this.dragStartPos = null;
|
|
this.mouseDownTime = null;
|
|
this.setDirtyCanvas(true, true);
|
|
return false;
|
|
};
|
|
|
|
// Add double click handler
|
|
nodeType.prototype.onDblClick = function(e, pos) {
|
|
const [x, y] = pos;
|
|
if (!this.isMouseOverGraph(x, y)) return false;
|
|
|
|
const pointIndex = this.findNearPoint(x, y);
|
|
if (pointIndex !== null) {
|
|
this.points.splice(pointIndex, 1);
|
|
this.updatePointsValue();
|
|
// Clear all interaction states
|
|
this.selectedPoint = null;
|
|
this.isDragging = false;
|
|
this.dragStartPos = null;
|
|
this.setDirtyCanvas(true, true);
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
// Handle node resizing
|
|
nodeType.prototype.onResize = function(size) {
|
|
if (onResize) {
|
|
onResize.apply(this, arguments);
|
|
}
|
|
// Ensure minimum size
|
|
this.size[0] = Math.max(400, size[0]);
|
|
this.size[1] = Math.max(500, size[1]);
|
|
this.setDirtyCanvas(true, true);
|
|
};
|
|
|
|
// Add click handler
|
|
nodeType.prototype.onClick = function(e, pos) {
|
|
// Clear all states on any click as a safety measure
|
|
this.selectedPoint = null;
|
|
this.isDragging = false;
|
|
this.dragStartPos = null;
|
|
this.setDirtyCanvas(true, true);
|
|
return false;
|
|
};
|
|
}
|
|
}
|
|
});
|