Sweeping update. New features, more maintainable. See readme and comments for more info.
801 lines
38 KiB
JavaScript
801 lines
38 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
|
import { ComfyWidgets } from "../../scripts/widgets.js";
|
|
import { FeatureTemplates } from "./feature_templates.js";
|
|
|
|
|
|
//TODO: add handling for maximum frame count change
|
|
// 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;
|
|
const onConnectionsChange = nodeType.prototype.onConnectionsChange;
|
|
|
|
// 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();
|
|
}
|
|
};
|
|
|
|
// Add connection change handler
|
|
nodeType.prototype.onConnectionsChange = function(slotType, slot, isConnected, link_info, output) {
|
|
// console.log("onConnectionsChange:", { slotType, slot, isConnected, link_info });
|
|
|
|
if (onConnectionsChange) {
|
|
onConnectionsChange.apply(this, arguments);
|
|
}
|
|
|
|
// Handle frame_count input connection changes
|
|
if (slotType === LiteGraph.INPUT) {
|
|
const input = this.inputs[slot];
|
|
// console.log("Input connection change:", { input, name: input?.name });
|
|
|
|
if (input && input.name === "frame_count") {
|
|
if (isConnected && link_info) {
|
|
// When connected, we'll need to listen for value changes
|
|
const inputNode = link_info.origin_id ? this.graph._nodes_by_id[link_info.origin_id] : null;
|
|
// console.log("Connected node:", inputNode);
|
|
|
|
if (inputNode) {
|
|
// Store the connected node for later reference
|
|
this.frameCountInputNode = inputNode;
|
|
// Set up the trigger and mode for value changes
|
|
input.onTrigger = this.onInputUpdated.bind(this);
|
|
// Set mode to trigger on any value change
|
|
input.mode = LiteGraph.UP | LiteGraph.DOWN | LiteGraph.EVENT;
|
|
// Also watch the connected node's widget for changes
|
|
const widget = inputNode.widgets?.[0];
|
|
if (widget) {
|
|
const originalCallback = widget.callback;
|
|
widget.callback = (v) => {
|
|
if (originalCallback) {
|
|
originalCallback.call(widget, v);
|
|
}
|
|
// Trigger our input update when the upstream widget changes
|
|
console.log("Frame count input updated from upstream widget:", v);
|
|
this.onInputUpdated();
|
|
};
|
|
}
|
|
// Trigger initial update
|
|
this.onInputUpdated();
|
|
}
|
|
} else {
|
|
// When disconnected, clear the reference and trigger
|
|
this.frameCountInputNode = null;
|
|
input.onTrigger = null;
|
|
input.mode = LiteGraph.DEFAULT; // Reset mode
|
|
}
|
|
// Redraw since frame count may have changed
|
|
this.setDirtyCanvas(true, true);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Add handler for input value changes
|
|
nodeType.prototype.onInputUpdated = function() {
|
|
// Find frame_count input
|
|
const frameCountInput = this.inputs.find(input => input.name === "frame_count");
|
|
|
|
if (frameCountInput && frameCountInput.link !== null) {
|
|
// Get the connected node
|
|
const link = this.graph.links[frameCountInput.link];
|
|
|
|
if (link && link.origin_id) {
|
|
const inputNode = this.graph._nodes_by_id[link.origin_id];
|
|
|
|
if (inputNode) {
|
|
// Try to get value from node's widget first
|
|
const widget = inputNode.widgets?.[0];
|
|
let frameCount;
|
|
if (widget) {
|
|
frameCount = widget.value;
|
|
} else {
|
|
// Fallback to output value if no widget
|
|
frameCount = inputNode.getOutputData(link.origin_slot);
|
|
}
|
|
|
|
if (frameCount !== undefined && frameCount !== null) {
|
|
// Get frame count widget
|
|
const frameCountWidget = this.widgets.find(w => w.name === "frame_count");
|
|
if (frameCountWidget) {
|
|
const oldFrameCount = frameCountWidget.value;
|
|
// console.log("Updating frame count from", oldFrameCount, "to", frameCount);
|
|
|
|
// Update widget value first
|
|
frameCountWidget.value = frameCount;
|
|
|
|
// Scale points if needed
|
|
if (this.points && this.points.length > 0 && oldFrameCount !== frameCount) {
|
|
this.points = this.points.map(([frame, value]) => {
|
|
const oldMax = oldFrameCount - 1;
|
|
const newMax = frameCount - 1;
|
|
const newFrame = Math.min(newMax, Math.round((frame / oldMax) * newMax));
|
|
return [newFrame, value];
|
|
});
|
|
this.points.sort((a, b) => a[0] - b[0]);
|
|
this.updatePointsValue();
|
|
}
|
|
|
|
// Force graph area update
|
|
this.setSize(this.size); // This will trigger size update and redraw
|
|
this.setDirtyCanvas(true, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// Override onNodeCreated to initialize the node
|
|
nodeType.prototype.onNodeCreated = function() {
|
|
// console.log("Node created, initializing node");
|
|
const r = onNodeCreated?.apply(this, arguments);
|
|
|
|
// Add trigger for input updates
|
|
this.onInputTriggered = this.onInputUpdated.bind(this);
|
|
|
|
// Register for input triggers
|
|
if (!this.inputs) {
|
|
this.inputs = [];
|
|
}
|
|
|
|
// Find or create frame_count input
|
|
let frameCountInput = this.inputs.find(input => input.name === "frame_count");
|
|
if (frameCountInput) {
|
|
// Add trigger callback
|
|
frameCountInput.onTrigger = this.onInputTriggered;
|
|
// Set mode to trigger on value change
|
|
frameCountInput.mode = LiteGraph.UP | LiteGraph.DOWN;
|
|
}
|
|
|
|
// 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 frame count change handler
|
|
const frameCountWidget = this.widgets.find(w => w.name === "frame_count");
|
|
if (frameCountWidget) {
|
|
// Track the last frame count outside the callback
|
|
let lastFrameCount = frameCountWidget.value;
|
|
const originalCallback = frameCountWidget.callback;
|
|
frameCountWidget.callback = function(v, e, skipCallback) {
|
|
// console.log("Frame count widget callback triggered");
|
|
const oldFrameCount = lastFrameCount; // Use tracked value
|
|
// console.log("Current frame count:", oldFrameCount);
|
|
// console.log("New frame count:", v);
|
|
// console.log("this.points exists:", !!this.points);
|
|
// console.log("this.points:", JSON.stringify(this.points));
|
|
// console.log("this.points length:", this.points?.length);
|
|
// console.log("oldFrameCount !== v:", oldFrameCount !== v);
|
|
|
|
// Update tracked value before calling original callback
|
|
lastFrameCount = v;
|
|
|
|
// Call original callback to update the widget value
|
|
if (originalCallback && !skipCallback) {
|
|
originalCallback.call(frameCountWidget, v, e);
|
|
}
|
|
|
|
// Now scale points based on the frame count change
|
|
if (this.points && this.points.length > 0 && oldFrameCount !== v) {
|
|
// console.log("Original points:", JSON.stringify(this.points));
|
|
this.points = this.points.map(([frame, value]) => {
|
|
const oldMax = oldFrameCount - 1; // Convert to 0-based
|
|
const newMax = v - 1; // Convert to 0-based
|
|
const newFrame = Math.min(newMax, Math.round((frame / oldMax) * newMax));
|
|
// console.log(`Scaling point frame ${frame} to ${newFrame} (old max: ${oldMax}, new max: ${newMax})`);
|
|
return [newFrame, value];
|
|
});
|
|
// Sort points by frame number
|
|
this.points.sort((a, b) => a[0] - b[0]);
|
|
// console.log("Scaled points:", JSON.stringify(this.points));
|
|
this.updatePointsValue();
|
|
this.setDirtyCanvas(true, true);
|
|
} else {
|
|
// console.log("Skipped points scaling because:", {
|
|
// hasPoints: !!this.points,
|
|
// pointsLength: this.points?.length,
|
|
// frameCountChanged: oldFrameCount !== v
|
|
// });
|
|
}
|
|
}.bind(this); // Still bind to node instance for this.points access
|
|
}
|
|
|
|
// 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 template dropdown and controls
|
|
const templateWidget = this.addWidget("combo", "Template", "selected_template", (v) => {}, {
|
|
values: [
|
|
"none",
|
|
// Basic waveforms
|
|
"sine",
|
|
"square",
|
|
"triangle",
|
|
"sawtooth",
|
|
// Animation curves
|
|
"easeInOut",
|
|
"easeIn",
|
|
"easeOut",
|
|
// Special patterns
|
|
"bounce",
|
|
"pulse",
|
|
"random",
|
|
"smoothRandom",
|
|
"heartbeat",
|
|
"steps"
|
|
],
|
|
value: "none"
|
|
});
|
|
templateWidget.name = "template";
|
|
|
|
// Add cycles control
|
|
const cyclesWidget = this.addWidget("number", "Template Cycles", "template_cycles", function(v) {
|
|
return Math.max(0.1, Math.min(200, v));
|
|
}, {
|
|
value: 1.0,
|
|
min: 0.1,
|
|
max: 200,
|
|
step: 0.1,
|
|
precision: 2
|
|
});
|
|
cyclesWidget.name = "template_cycles";
|
|
cyclesWidget.value = 1.0; // Explicitly set initial value
|
|
|
|
// Add load template button
|
|
const loadButton = this.addWidget("button", "Load Template", "load", () => {
|
|
const templateWidget = this.widgets.find(w => w.name === "template");
|
|
if (templateWidget && templateWidget.value && templateWidget.value !== "none") {
|
|
this.loadTemplate(templateWidget.value);
|
|
}
|
|
});
|
|
|
|
|
|
// 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
|
|
};
|
|
|
|
// Get current frame count value, checking both widget and input
|
|
nodeType.prototype.getCurrentFrameCount = function() {
|
|
// First check if we have a connected input
|
|
const frameCountInput = this.inputs.find(input => input.name === "frame_count");
|
|
if (frameCountInput && frameCountInput.link !== null) {
|
|
const link = this.graph.links[frameCountInput.link];
|
|
if (link) {
|
|
const inputNode = this.graph._nodes_by_id[link.origin_id];
|
|
if (inputNode) {
|
|
// Try to get value from node's widget first
|
|
const widget = inputNode.widgets?.[0];
|
|
if (widget) {
|
|
// console.log("Getting frame count from widget:", widget.value);
|
|
return widget.value;
|
|
}
|
|
// Fallback to output value if no widget
|
|
const frameCount = inputNode.getOutputData(link.origin_slot);
|
|
// console.log("Getting frame count from output:", frameCount);
|
|
return frameCount;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback to widget value
|
|
const frameCountWidget = this.widgets.find(w => w.name === "frame_count");
|
|
return frameCountWidget?.value || 30; // Default to 30 if no value found
|
|
};
|
|
|
|
// 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 • Double-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.getCurrentFrameCount();
|
|
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.getCurrentFrameCount();
|
|
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) {
|
|
if (this.isDragging && this.selectedPoint !== null) {
|
|
// If already dragging and clicked, drop the point
|
|
this.isDragging = false;
|
|
this.selectedPoint = null;
|
|
this.dragStartPos = null;
|
|
} else {
|
|
// Start dragging
|
|
this.selectedPoint = pointIndex;
|
|
this.dragStartPos = [...pos];
|
|
this.isDragging = true;
|
|
}
|
|
} 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.isDragging && this.selectedPoint !== null) {
|
|
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 && p[1] === value);
|
|
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) {
|
|
// Only clear states if we're not in dragging mode
|
|
if (!this.isDragging) {
|
|
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]);
|
|
// Force graph area recalculation
|
|
this.setDirtyCanvas(true, true);
|
|
};
|
|
|
|
// Add method to update node size
|
|
nodeType.prototype.setSize = function(size) {
|
|
this.size = size;
|
|
if (this.onResize) {
|
|
this.onResize(size);
|
|
}
|
|
};
|
|
|
|
// 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;
|
|
};
|
|
|
|
// Template loading method
|
|
nodeType.prototype.loadTemplate = function(templateName) {
|
|
const frameCount = this.getCurrentFrameCount();
|
|
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 cyclesWidget = this.widgets.find(w => w.name === "template_cycles");
|
|
const cycles = cyclesWidget?.value ?? 1;
|
|
|
|
if (templateName in FeatureTemplates) {
|
|
this.points = FeatureTemplates[templateName](frameCount, minValue, maxValue, cycles);
|
|
this.updatePointsValue();
|
|
this.setDirtyCanvas(true, true);
|
|
}
|
|
};
|
|
}
|
|
}
|
|
});
|