refactor1

This commit is contained in:
JJ
2025-11-25 18:27:11 +00:00
parent bb2a03f177
commit e3e9d786cf
11 changed files with 1361 additions and 1219 deletions
+406
View File
@@ -0,0 +1,406 @@
import { app } from '../../../scripts/app.js';
import {
drawWidgetButton,
drawNumberWidgetPart,
RgthreeBaseWidget
} from './drawing_utils.js';
// === TOP ROW WIDGET (Combined refresh button, bg_image dropdown, and width/height controls) ===
export class TopRowWidget extends RgthreeBaseWidget {
constructor(name = "TopRowWidget") {
super(name);
this.type = "custom";
this.options = { serialize: false };
this.value = {};
this.haveMouseMovedValue = false;
this.canvasButtonMouseDown = false;
this.framesButtonMouseDown = false;
this.hitAreas = {
refreshCanvasButton: { bounds: [0, 0], onClick: null },
refreshFramesButton: { bounds: [0, 0], onClick: null },
bgImgLeftArrow: { bounds: [0, 0], onClick: null },
bgImgVal: { bounds: [0, 0], onClick: null },
bgImgRightArrow: { bounds: [0, 0], onClick: null },
bgImgAny: { bounds: [0, 0], onClick: null },
widthDec: { bounds: [0, 0], onClick: null },
widthVal: { bounds: [0, 0], onClick: null },
widthInc: { bounds: [0, 0], onClick: null },
widthAny: { bounds: [0, 0], onMove: null },
heightDec: { bounds: [0, 0], onClick: null },
heightVal: { bounds: [0, 0], onClick: null },
heightInc: { bounds: [0, 0], onClick: null },
heightAny: { bounds: [0, 0], onMove: null },
};
}
draw(ctx, node, w, posY, height) {
const margin = 15;
const spacing = 10; // Reduced spacing to fit within 100%
const midY = posY + height * 0.5;
ctx.save();
// Get widget values
const widthWidget = node.widgets?.find(w => w.name === "mask_width");
const widthValue = widthWidget ? widthWidget.value : 512;
const heightWidget = node.widgets?.find(w => w.name === "mask_height");
const heightValue = heightWidget ? heightWidget.value : 512;
const bgImgWidget = node.widgets?.find(w => w.name === "bg_img");
const bgImgValue = bgImgWidget ? bgImgWidget.value : "None";
// Calculate available width for components (excluding margins and spacing)
const availableWidth = node.size[0] - margin * 2 - spacing * 4; // Account for spacing between elements
// Calculate component widths based on percentages (now totaling 100% of available width)
const refreshCanvasWidth = availableWidth * 0.12;
const refreshFramesWidth = availableWidth * 0.12;
const bgImgDropdownWidth = availableWidth * 0.14;
const iconButtonWidth = Math.max(20, Math.min(28, availableWidth * 0.05));
const dimensionsAreaWidth = availableWidth - (
refreshCanvasWidth + spacing +
refreshFramesWidth + spacing +
bgImgDropdownWidth + spacing +
iconButtonWidth
);
// Calculate total width and starting position to center everything
const totalWidth = refreshCanvasWidth + spacing + refreshFramesWidth + spacing + bgImgDropdownWidth + spacing + iconButtonWidth + spacing + dimensionsAreaWidth;
const startX = margin; // Start from left margin instead of centering
let posX = startX;
// Draw Refresh Canvas button
drawWidgetButton(
ctx,
{ size: [refreshCanvasWidth, height], pos: [posX, posY] },
"🔃 Canvas",
this.canvasButtonMouseDown
);
this.hitAreas.refreshCanvasButton.bounds = [posX, refreshCanvasWidth];
posX += refreshCanvasWidth + spacing;
// Draw Refresh Frames button
drawWidgetButton(
ctx,
{ size: [refreshFramesWidth, height], pos: [posX, posY] },
"🕞 Frames",
this.framesButtonMouseDown
);
this.hitAreas.refreshFramesButton.bounds = [posX, refreshFramesWidth];
posX += refreshFramesWidth + spacing;
// Draw bg_img control with left/right arrows
const arrowWidth = 7; // Smaller arrows
// Draw background box
ctx.fillStyle = LiteGraph.WIDGET_BGCOLOR;
ctx.fillRect(posX, posY, bgImgDropdownWidth, height);
ctx.strokeStyle = LiteGraph.WIDGET_OUTLINE_COLOR;
ctx.strokeRect(posX, posY, bgImgDropdownWidth, height);
// Draw left arrow (positioned at the very left edge)
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR;
ctx.fillText("<", posX + arrowWidth / 2, midY);
this.hitAreas.bgImgLeftArrow.bounds = [posX, arrowWidth];
// Draw text value (centered in the control)
ctx.fillText(bgImgValue, posX + bgImgDropdownWidth / 2, midY);
this.hitAreas.bgImgVal.bounds = [posX + arrowWidth, bgImgDropdownWidth - (arrowWidth * 2)];
// Draw right arrow (positioned at the very right edge)
ctx.fillText(">", posX + bgImgDropdownWidth - arrowWidth / 2, midY);
this.hitAreas.bgImgRightArrow.bounds = [posX + bgImgDropdownWidth - arrowWidth, arrowWidth];
// Combined bounds for the entire control
this.hitAreas.bgImgAny.bounds = [posX, bgImgDropdownWidth];
posX += bgImgDropdownWidth + spacing;
// Animation toggle icon for inactive flow animation (default OFF)
const isAnimOn = !!(node?.editor?._inactiveFlowEnabled ?? false);
drawWidgetButton(
ctx,
{ size: [iconButtonWidth, height], pos: [posX, posY] },
"~", // wavy line icon
isAnimOn
);
if (isAnimOn) {
const pad = 0.5;
ctx.save();
ctx.strokeStyle = '#2cc6ff';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.roundRect(posX + pad, posY + pad, iconButtonWidth - pad * 2, height - pad * 2, [4]);
ctx.stroke();
ctx.restore();
}
this.hitAreas.animToggleButton = { bounds: [posX, iconButtonWidth], onClick: (e, p, n) => {
if (n?.editor) {
n.editor._inactiveFlowEnabled = !n.editor._inactiveFlowEnabled;
try { n.editor.layerRenderer?.updateInactiveDash?.(); } catch {}
n.setDirtyCanvas(true, true);
}
return true;
}};
posX += iconButtonWidth + spacing;
// Draw rounded area for width/height controls
const roundedAreaX = posX;
const roundedAreaY = posY;
const roundedAreaWidth = dimensionsAreaWidth;
const roundedAreaHeight = height;
// Draw rounded rectangle background
ctx.fillStyle = LiteGraph.WIDGET_BGCOLOR;
ctx.strokeStyle = LiteGraph.WIDGET_OUTLINE_COLOR;
ctx.beginPath();
ctx.roundRect(roundedAreaX, roundedAreaY, roundedAreaWidth, roundedAreaHeight, [roundedAreaHeight * 0.5]);
ctx.fill();
ctx.stroke();
// Calculate positions for width and height controls within the rounded area
// Center the controls in the rounded area
const controlSpacing = 20;
const numberControlWidth = drawNumberWidgetPart.WIDTH_TOTAL; // Width of each number control
const labelWidth = 40; // Approximate width for labels
const totalControlWidth = labelWidth + numberControlWidth + controlSpacing + labelWidth + numberControlWidth;
// Center the controls in the rounded area
const controlsStartX = roundedAreaX + (roundedAreaWidth - totalControlWidth) / 2;
// Width control with label
const widthLabel = "width:";
const widthControlX = controlsStartX;
// Draw width label
ctx.textAlign = "left";
ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR;
ctx.fillText(widthLabel, widthControlX, midY);
// Draw width control (positioned after label)
const widthControlStartX = widthControlX + labelWidth;
const [wLeftArrow, wText, wRightArrow] = drawNumberWidgetPart(ctx, {
posX: widthControlStartX,
posY,
height,
value: widthValue,
direction: 1,
});
this.hitAreas.widthDec.bounds = wLeftArrow;
this.hitAreas.widthVal.bounds = wText;
this.hitAreas.widthInc.bounds = wRightArrow;
this.hitAreas.widthAny.bounds = [wLeftArrow[0], wRightArrow[0] + wRightArrow[1] - wLeftArrow[0]];
this.hitAreas.widthDec.onClick = () => this.stepWidth(node, -16);
this.hitAreas.widthInc.onClick = () => this.stepWidth(node, 16);
this.hitAreas.widthVal.onClick = () => this.promptWidth(node);
this.hitAreas.widthAny.onMove = (event) => this.dragWidth(node, event);
// Height control with label
const heightControlX = widthControlStartX + numberControlWidth + controlSpacing;
const heightLabel = "height:";
// Draw height label
ctx.fillText(heightLabel, heightControlX, midY);
// Draw height control (positioned after label)
const heightControlStartX = heightControlX + labelWidth;
const [hLeftArrow, hText, hRightArrow] = drawNumberWidgetPart(ctx, {
posX: heightControlStartX,
posY,
height,
value: heightValue,
direction: 1,
});
this.hitAreas.heightDec.bounds = hLeftArrow;
this.hitAreas.heightVal.bounds = hText;
this.hitAreas.heightInc.bounds = hRightArrow;
this.hitAreas.heightAny.bounds = [hLeftArrow[0], hRightArrow[0] + hRightArrow[1] - hLeftArrow[0]];
this.hitAreas.heightDec.onClick = () => this.stepHeight(node, -16);
this.hitAreas.heightInc.onClick = () => this.stepHeight(node, 16);
this.hitAreas.heightVal.onClick = () => this.promptHeight(node);
this.hitAreas.heightAny.onMove = (event) => this.dragHeight(node, event);
// Set up event handlers for refresh button
this.hitAreas.refreshCanvasButton.onClick = async () => {
// Update all box layer ref images from connected ref_images
if (node.updateAllBoxLayerRefs) {
try {
await node.updateAllBoxLayerRefs();
} catch (e) {
console.warn('Failed to update box layer refs:', e);
}
}
// Wait for reference image update and scale recalculation to complete
if (node.updateReferenceImageFromConnectedNode) {
try {
// This now properly awaits the image loading and scale recalculation
await node.updateReferenceImageFromConnectedNode();
// Force a final render with the correct scale
// The scale should already be correct from handleImageLoad -> recenterBackgroundImage
if (node.editor && node.editor.layerRenderer) {
node.editor.layerRenderer.render();
}
} catch (e) {
console.warn('Failed to update reference image:', e);
}
}
// Check for frames input and handle box layer keyframe scaling
if (node.handleFramesRefresh) {
node.handleFramesRefresh();
}
};
this.hitAreas.refreshCanvasButton.onDown = () => {
this.canvasButtonMouseDown = true;
node.setDirtyCanvas(true, false);
};
this.hitAreas.refreshCanvasButton.onUp = () => {
this.canvasButtonMouseDown = false;
node.setDirtyCanvas(true, false);
};
this.hitAreas.refreshFramesButton.onClick = () => {
if (node.handleFramesRefresh) {
node.handleFramesRefresh();
}
};
this.hitAreas.refreshFramesButton.onDown = () => {
this.framesButtonMouseDown = true;
node.setDirtyCanvas(true, false);
};
this.hitAreas.refreshFramesButton.onUp = () => {
this.framesButtonMouseDown = false;
node.setDirtyCanvas(true, false);
};
// Set up event handlers for bg_img control
this.hitAreas.bgImgLeftArrow.onClick = () => this.stepBgImg(node, -1);
this.hitAreas.bgImgVal.onClick = () => this.stepBgImg(node, 1);
this.hitAreas.bgImgRightArrow.onClick = () => this.stepBgImg(node, 1);
ctx.restore();
}
// Methods for width controls
stepWidth(node, step) {
const widthWidget = node.widgets?.find(w => w.name === "mask_width");
if (widthWidget) {
const newValue = widthWidget.value + step;
widthWidget.value = node.sizeManager ? node.sizeManager.constrainCanvasWidth(newValue) : Math.max(64, newValue);
if (widthWidget.callback) {
widthWidget.callback(widthWidget.value);
}
node.setDirtyCanvas(true, true);
}
}
promptWidth(node) {
if (this.haveMouseMovedValue) return;
const widthWidget = node.widgets?.find(w => w.name === "mask_width");
if (widthWidget) {
const canvas = app.canvas;
canvas.prompt("Width", widthWidget.value, (v) => {
const newValue = Number(v);
widthWidget.value = node.sizeManager ? node.sizeManager.constrainCanvasWidth(newValue) : Math.max(64, newValue);
if (widthWidget.callback) {
widthWidget.callback(widthWidget.value);
}
});
}
}
dragWidth(node, event) {
if (event.deltaX) {
this.haveMouseMovedValue = true;
const widthWidget = node.widgets?.find(w => w.name === "mask_width");
if (widthWidget) {
const newValue = widthWidget.value + event.deltaX * 2;
widthWidget.value = node.sizeManager ? node.sizeManager.constrainCanvasWidth(newValue) : Math.max(64, newValue);
if (widthWidget.callback) {
widthWidget.callback(widthWidget.value);
}
node.setDirtyCanvas(true, true);
}
}
}
// Methods for height controls
stepHeight(node, step) {
const heightWidget = node.widgets?.find(w => w.name === "mask_height");
if (heightWidget) {
const newValue = heightWidget.value + step;
heightWidget.value = node.sizeManager ? node.sizeManager.constrainCanvasHeight(newValue) : Math.max(64, newValue);
if (heightWidget.callback) {
heightWidget.callback(heightWidget.value);
}
node.setDirtyCanvas(true, true);
}
}
promptHeight(node) {
if (this.haveMouseMovedValue) return;
const heightWidget = node.widgets?.find(w => w.name === "mask_height");
if (heightWidget) {
const canvas = app.canvas;
canvas.prompt("Height", heightWidget.value, (v) => {
const newValue = Number(v);
heightWidget.value = node.sizeManager ? node.sizeManager.constrainCanvasHeight(newValue) : Math.max(64, newValue);
if (heightWidget.callback) {
heightWidget.callback(heightWidget.value);
}
});
}
}
dragHeight(node, event) {
if (event.deltaX) {
this.haveMouseMovedValue = true;
const heightWidget = node.widgets?.find(w => w.name === "mask_height");
if (heightWidget) {
const newValue = heightWidget.value + event.deltaX * 2;
heightWidget.value = node.sizeManager ? node.sizeManager.constrainCanvasHeight(newValue) : Math.max(64, newValue);
if (heightWidget.callback) {
heightWidget.callback(heightWidget.value);
}
node.setDirtyCanvas(true, true);
}
}
}
// Methods for bg_img controls
stepBgImg(node, step) {
const bgImgWidget = node.widgets?.find(w => w.name === "bg_img");
if (bgImgWidget) {
const options = ["None", "A", "B", "C"];
const currentIndex = options.indexOf(bgImgWidget.value);
const newIndex = (currentIndex + step + options.length) % options.length;
bgImgWidget.value = options[newIndex];
if (bgImgWidget.callback) {
bgImgWidget.callback(bgImgWidget.value);
}
node.setDirtyCanvas(true, true);
}
}
// Mouse event handlers
mouse(event, pos, node) {
// Use the parent's mouse handling which properly manages hit areas
return super.mouse(event, pos, node);
}
onMouseUp(event, pos, node) {
super.onMouseUp(event, pos, node);
this.haveMouseMovedValue = false;
this.canvasButtonMouseDown = false;
this.framesButtonMouseDown = false;
}
computeSize(width) {
return [width, LiteGraph.NODE_WIDGET_HEIGHT];
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
import { app } from '../../../scripts/app.js';
import { PowerSplineWidget } from './spline_utils.js';
import { HandDrawLayerWidget } from './handdraw_layer.js';
import { BoxLayerWidget } from './box_layer.js';
import { HandDrawLayerWidget } from './layer_type_draw.js';
import { BoxLayerWidget } from './layer_type_box.js';
import { updateDrivenConfigValue, updateEasingConfigValue } from './persistence.js';
// Helper to create interactive number input with arrows and drag support
+58
View File
@@ -0,0 +1,58 @@
// General utility functions for widget and callback management
/**
* Chain a callback to an existing object property
* Allows adding new behavior while preserving original functionality
*/
export function chainCallback(object, property, callback) {
if (object == undefined) {
//This should not happen.
console.error("Tried to add callback to non-existant object")
return;
}
if (property in object) {
const callback_orig = object[property]
object[property] = function () {
const r = callback_orig.apply(this, arguments);
callback.apply(this, arguments);
return r
};
} else {
object[property] = callback;
}
}
/**
* Permanently hide a widget from display and interaction
* Useful for internal-only widgets that shouldn't be visible to users
*/
export function hideWidgetForGood(node, widget, suffix = '') {
widget.origType = widget.type;
widget.type = 'hidden' + suffix;
widget.hidden = true;
// Monkeypatch draw to do nothing
widget.draw = () => {};
// Monkeypatch computeSize to return [0, -4]
// This is a hack to make the widget not take up any space
// We need to return -4 instead of 0 because of how LiteGraph calculates node height
// In recent versions of LiteGraph, it adds 4 to the widget height
widget.computeSize = () => [0, -4];
// Prevent the widget from being serialized
if (!widget.options) {
widget.options = {};
}
// widget.options.serialize = false;
// Hide the widget from the node's list of widgets
// This is another hack to prevent the widget from being drawn
// We can't just remove it from the list, because other parts of the code
// might still need to access it
const index = node.widgets.indexOf(widget);
if (index > -1) {
node.widgets.splice(index, 1);
node.widgets.push(widget);
}
}
@@ -0,0 +1,177 @@
// Button bar widget for adding layers (Add Spline, Add Keyframes, Draw, Duplicate)
import { RgthreeBaseWidget, drawWidgetButton } from './drawing_utils.js';
/**
* Creates a button bar widget with Add Spline, Add Keyframes, Draw, and Duplicate buttons
* @returns {RgthreeBaseWidget} The configured button bar widget
*/
export function createButtonBarWidget() {
const buttonBarWidget = new RgthreeBaseWidget("button_bar");
buttonBarWidget.type = "custom";
buttonBarWidget.serialize = false;
// Track mouse state for button hover effects
buttonBarWidget.addSplineMouseDown = false;
buttonBarWidget.addBoxMouseDown = false;
buttonBarWidget.drawMouseDown = false;
buttonBarWidget.duplicateMouseDown = false;
buttonBarWidget.computeSize = function(width) {
return [width, LiteGraph.NODE_WIDGET_HEIGHT];
};
buttonBarWidget.draw = function(ctx, node, width, posY, height) {
const margin = 15;
const gap = 5;
const totalWidth = width - margin * 2 - gap * 3;
const addSplineWidth = totalWidth * 0.30;
const addBoxWidth = totalWidth * 0.30;
const drawWidth = totalWidth * 0.20;
const duplicateWidth = totalWidth * 0.20;
const addSplineX = margin;
const addBoxX = addSplineX + addSplineWidth + gap;
const drawX = addBoxX + addBoxWidth + gap;
const duplicateX = drawX + drawWidth + gap;
// Draw Add Spline button (30% width)
drawWidgetButton(
ctx,
{ size: [addSplineWidth, height], pos: [addSplineX, posY] },
"➕ Add Spline",
this.addSplineMouseDown
);
// Draw Add Box button (30% width)
drawWidgetButton(
ctx,
{ size: [addBoxWidth, height], pos: [addBoxX, posY] },
"🎥 Add Keyframes",
this.addBoxMouseDown
);
// Draw button (20% width)
drawWidgetButton(
ctx,
{ size: [drawWidth, height], pos: [drawX, posY] },
"✏️ Draw",
this.drawMouseDown || (node?.editor?._handdrawMode === 'create')
);
// Draw Duplicate button (20% width)
drawWidgetButton(
ctx,
{ size: [duplicateWidth, height], pos: [duplicateX, posY] },
"🧬 Duplicate",
this.duplicateMouseDown
);
};
buttonBarWidget.mouse = function(event, pos, node) {
if (event.type === "pointerdown" || event.type === "mousedown") {
const margin = 15;
const gap = 5;
const width = node.size[0];
const totalWidth = width - margin * 2 - gap * 3;
const addSplineWidth = totalWidth * 0.30;
const addBoxWidth = totalWidth * 0.30;
const drawWidth = totalWidth * 0.20;
const duplicateWidth = totalWidth * 0.20;
const addSplineLeft = margin;
const addSplineRight = addSplineLeft + addSplineWidth;
const addBoxLeft = addSplineRight + gap;
const addBoxRight = addBoxLeft + addBoxWidth;
const drawLeft = addBoxRight + gap;
const drawRight = drawLeft + drawWidth;
const duplicateLeft = drawRight + gap;
const duplicateRight = duplicateLeft + duplicateWidth;
if (pos[0] >= addSplineLeft && pos[0] <= addSplineRight) {
this.addSplineMouseDown = true;
node.setDirtyCanvas(true, false);
return true;
}
else if (pos[0] >= addBoxLeft && pos[0] <= addBoxRight) {
this.addBoxMouseDown = true;
node.setDirtyCanvas(true, false);
return true;
}
else if (pos[0] >= drawLeft && pos[0] <= drawRight) {
this.drawMouseDown = true;
node.setDirtyCanvas(true, false);
return true;
}
else if (pos[0] >= duplicateLeft &&
pos[0] <= duplicateRight) {
this.duplicateMouseDown = true;
node.setDirtyCanvas(true, false);
return true;
}
}
else if (event.type === "pointerup" || event.type === "mouseup") {
const margin = 15;
const gap = 5;
const width = node.size[0];
const totalWidth = width - margin * 2 - gap * 3;
const addSplineWidth = totalWidth * 0.30;
const addBoxWidth = totalWidth * 0.30;
const drawWidth = totalWidth * 0.20;
const duplicateWidth = totalWidth * 0.20;
const addSplineLeft = margin;
const addSplineRight = addSplineLeft + addSplineWidth;
const addBoxLeft = addSplineRight + gap;
const addBoxRight = addBoxLeft + addBoxWidth;
const drawLeft = addBoxRight + gap;
const drawRight = drawLeft + drawWidth;
const duplicateLeft = drawRight + gap;
const duplicateRight = duplicateLeft + duplicateWidth;
if (this.addSplineMouseDown &&
pos[0] >= addSplineLeft && pos[0] <= addSplineRight) {
if (node?.editor && node.editor._handdrawMode === 'create') {
node.editor.exitHanddrawMode?.(false);
}
node.layerManager.addNewSpline();
}
else if (this.addBoxMouseDown &&
pos[0] >= addBoxLeft && pos[0] <= addBoxRight) {
if (node?.editor && node.editor._handdrawMode === 'create') {
node.editor.exitHanddrawMode?.(false);
}
if (node.layerManager?.addNewBox) {
node.layerManager.addNewBox();
}
}
else if (this.drawMouseDown &&
pos[0] >= drawLeft && pos[0] <= drawRight) {
if (node?.editor) {
if (node.editor._handdrawMode === 'create') {
node.editor.exitHanddrawMode?.(false);
} else {
node.editor.enterHanddrawMode?.('create');
}
node.setDirtyCanvas(true, true);
}
}
else if (this.duplicateMouseDown &&
pos[0] >= duplicateLeft && pos[0] <= duplicateRight) {
if (node?.editor && node.editor._handdrawMode === 'create') {
node.editor.exitHanddrawMode?.(false);
}
const activeWidget = node.layerManager.getActiveWidget();
if (activeWidget) {
node.layerManager.duplicateSpline(activeWidget);
}
}
this.addSplineMouseDown = false;
this.addBoxMouseDown = false;
this.drawMouseDown = false;
this.duplicateMouseDown = false;
node.setDirtyCanvas(true, false);
return true;
}
return false;
};
return buttonBarWidget;
}
+79
View File
@@ -0,0 +1,79 @@
// Header widget that displays column labels for spline layers
import { app } from '../../../scripts/app.js';
import { isLowQuality, drawTogglePart, drawNumberWidgetPart, RgthreeBaseWidget } from './drawing_utils.js';
export class PowerSplineHeaderWidget extends RgthreeBaseWidget {
constructor(name = "PowerSplineHeaderWidget") {
super(name);
this.value = { type: "PowerSplineHeaderWidget" };
this.type = "custom";
this.options = { serialize: false };
this.hitAreas = {
toggle: { bounds: [0, 0], onDown: this.onToggleDown },
};
}
draw(ctx, node, w, posY, height) {
const margin = 10;
const innerMargin = margin * 0.33;
const lowQuality = isLowQuality();
const allSplineState = node.allSplinesState ? node.allSplinesState() : false;
posY += 2;
const midY = posY + height * 0.5;
let posX = 10;
ctx.save();
this.hitAreas.toggle.bounds = drawTogglePart(ctx, { posX, posY, height, value: allSplineState });
if (!lowQuality) {
posX += this.hitAreas.toggle.bounds[1] + innerMargin;
ctx.globalAlpha = app.canvas.editor_alpha * 0.55;
ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR;
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.fillText("Toggle All", posX, midY);
let rposX = node.size[0] - margin - innerMargin - innerMargin;
ctx.textAlign = "center";
// Match the widget layout exactly:
// 1. Repeat control (far right) - drawNumberWidgetPart.WIDTH_TOTAL = 56
const repeatWidth = drawNumberWidgetPart.WIDTH_TOTAL;
ctx.fillText("Repeat", rposX - repeatWidth / 2, midY);
rposX -= repeatWidth + 10;
// 2. Interpolation selector - just text, no arrows (width = 70)
const interpTextWidth = 70;
ctx.fillText("Type", rposX - interpTextWidth / 2, midY);
rposX -= interpTextWidth + 10;
const numberWidth = drawNumberWidgetPart.WIDTH_TOTAL;
ctx.fillText("Z-Pause", rposX - numberWidth / 2, midY);
rposX -= numberWidth + 10;
ctx.fillText("A-Pause", rposX - numberWidth / 2, midY);
rposX -= numberWidth + 10;
ctx.fillText("Offset", rposX - numberWidth / 2, midY);
rposX -= numberWidth + 10;
// Easing column - same width as interpolation
const easingTextWidth = 70;
ctx.fillText("Easing", rposX - easingTextWidth / 2, midY);
rposX -= easingTextWidth + 10;
// Scale column
ctx.fillText("Scale", rposX - numberWidth / 2, midY);
rposX -= numberWidth + 10;
// Driven column (moved after Scale)
ctx.fillText("Driven", rposX - numberWidth / 2, midY);
rposX -= numberWidth + 10;
}
ctx.restore();
}
onToggleDown(event, pos, node) {
if (node.toggleAllSplines) {
node.toggleAllSplines();
}
this.cancelMouseDown();
return true;
}
}
@@ -1,84 +1,8 @@
import { app } from '../../../scripts/app.js';
import { isLowQuality, drawTogglePart, drawNumberWidgetPart, drawRoundedRectangle, fitString, RgthreeBaseWidget } from './drawing_utils.js';
// Normal spline layer widget
import { RgthreeBaseWidget, drawRoundedRectangle, drawTogglePart, drawNumberWidgetPart, fitString } from './drawing_utils.js';
import { initializeDrivenConfig, initializeEasingConfig, initializeScaleConfig, toggleDrivenState, prepareDrivenMenu } from './persistence.js';
import { showCustomEasingMenu, showCustomDrivenToggleMenu, showInterpolationMenu } from './context_menu.js';
// === SPLINE MULTI WIDGET ===
export class PowerSplineHeaderWidget extends RgthreeBaseWidget {
constructor(name = "PowerSplineHeaderWidget") {
super(name);
this.value = { type: "PowerSplineHeaderWidget" };
this.type = "custom";
this.options = { serialize: false };
this.hitAreas = {
toggle: { bounds: [0, 0], onDown: this.onToggleDown },
};
}
draw(ctx, node, w, posY, height) {
const margin = 10;
const innerMargin = margin * 0.33;
const lowQuality = isLowQuality();
const allSplineState = node.allSplinesState ? node.allSplinesState() : false;
posY += 2;
const midY = posY + height * 0.5;
let posX = 10;
ctx.save();
this.hitAreas.toggle.bounds = drawTogglePart(ctx, { posX, posY, height, value: allSplineState });
if (!lowQuality) {
posX += this.hitAreas.toggle.bounds[1] + innerMargin;
ctx.globalAlpha = app.canvas.editor_alpha * 0.55;
ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR;
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.fillText("Toggle All", posX, midY);
let rposX = node.size[0] - margin - innerMargin - innerMargin;
ctx.textAlign = "center";
// Match the widget layout exactly:
// 1. Repeat control (far right) - drawNumberWidgetPart.WIDTH_TOTAL = 56
const repeatWidth = drawNumberWidgetPart.WIDTH_TOTAL;
ctx.fillText("Repeat", rposX - repeatWidth / 2, midY);
rposX -= repeatWidth + 10;
// 2. Interpolation selector - just text, no arrows (width = 70)
const interpTextWidth = 70;
ctx.fillText("Type", rposX - interpTextWidth / 2, midY);
rposX -= interpTextWidth + 10;
const numberWidth = drawNumberWidgetPart.WIDTH_TOTAL;
ctx.fillText("Z-Pause", rposX - numberWidth / 2, midY);
rposX -= numberWidth + 10;
ctx.fillText("A-Pause", rposX - numberWidth / 2, midY);
rposX -= numberWidth + 10;
ctx.fillText("Offset", rposX - numberWidth / 2, midY);
rposX -= numberWidth + 10;
// Easing column - same width as interpolation
const easingTextWidth = 70;
ctx.fillText("Easing", rposX - easingTextWidth / 2, midY);
rposX -= easingTextWidth + 10;
// Scale column
ctx.fillText("Scale", rposX - numberWidth / 2, midY);
rposX -= numberWidth + 10;
// Driven column (moved after Scale)
ctx.fillText("Driven", rposX - numberWidth / 2, midY);
rposX -= numberWidth + 10;
}
ctx.restore();
}
onToggleDown(event, pos, node) {
if (node.toggleAllSplines) {
node.toggleAllSplines();
}
this.cancelMouseDown();
return true;
}
}
import { app } from '../../../scripts/app.js';
export class PowerSplineWidget extends RgthreeBaseWidget {
constructor(name) {
@@ -149,7 +73,6 @@ export class PowerSplineWidget extends RgthreeBaseWidget {
ctx.save();
const margin = 10;
const innerMargin = margin * 0.33;
const lowQuality = isLowQuality();
const midY = posY + height * 0.5;
let posX = margin;
@@ -168,11 +91,6 @@ export class PowerSplineWidget extends RgthreeBaseWidget {
this.hitAreas.toggle.bounds = drawTogglePart(ctx, { posX, posY, height, value: this.value.on });
posX += this.hitAreas.toggle.bounds[1] + innerMargin;
if (lowQuality) {
ctx.restore();
return;
}
if (!this.value.on) {
ctx.globalAlpha = app.canvas.editor_alpha * 0.4;
}
@@ -779,6 +697,7 @@ export class PowerSplineWidget extends RgthreeBaseWidget {
document.addEventListener('pointermove', onMove, true);
document.addEventListener('pointerup', onUp, true);
}
serializeValue(node, index) {
// Return a deep copy to prevent reference sharing between widgets during serialization
// This ensures each widget has independent interpolation and other values
@@ -790,52 +709,3 @@ export class PowerSplineWidget extends RgthreeBaseWidget {
return [width, LiteGraph.NODE_WIDGET_HEIGHT];
}
}
export function chainCallback(object, property, callback) {
if (object == undefined) {
//This should not happen.
console.error("Tried to add callback to non-existant object")
return;
}
if (property in object) {
const callback_orig = object[property]
object[property] = function () {
const r = callback_orig.apply(this, arguments);
callback.apply(this, arguments);
return r
};
} else {
object[property] = callback;
}
}
export function hideWidgetForGood(node, widget, suffix = '') {
widget.origType = widget.type;
widget.type = 'hidden' + suffix;
widget.hidden = true;
// Monkeypatch draw to do nothing
widget.draw = () => {};
// Monkeypatch computeSize to return [0, -4]
// This is a hack to make the widget not take up any space
// We need to return -4 instead of 0 because of how LiteGraph calculates node height
// In recent versions of LiteGraph, it adds 4 to the widget height
widget.computeSize = () => [0, -4];
// Prevent the widget from being serialized
if (!widget.options) {
widget.options = {};
}
// widget.options.serialize = false;
// Hide the widget from the node's list of widgets
// This is another hack to prevent the widget from being drawn
// We can't just remove it from the list, because other parts of the code
// might still need to access it
const index = node.widgets.indexOf(widget);
if (index > -1) {
node.widgets.splice(index, 1);
node.widgets.push(widget);
}
}
@@ -0,0 +1,606 @@
/**
* Layer UI Main Widget - Unified container for all layer-related UI components
*
* This module provides a single entry point for the layer management UI,
* orchestrating the button bar, header widgets, and layer manager in a clean, modular way.
*
* Architecture:
* - SplineLayerManager: Manages layer widget lifecycle (add, remove, duplicate, activate)
* - Manages the lifecycle of button bar and header widgets
* - Provides a clean API for adding layer UI to the node
* - Encapsulates all layer UI component initialization
*
* Usage:
* import { initializeLayerUI, SplineLayerManager } from './layer_ui_main_widget.js';
* node.layerManager = new SplineLayerManager(node);
* initializeLayerUI(node);
*/
import { PowerSplineHeaderWidget } from './layer_header.js';
import { createButtonBarWidget } from './layer_add_buttons.js';
import { PowerSplineWidget } from './layer_type_spline.js';
import { HandDrawLayerWidget } from './layer_type_draw.js';
import { BoxLayerWidget } from './layer_type_box.js';
/**
* SplineLayerManager - Manages the lifecycle of all layer widgets
* Handles adding, removing, duplicating, and switching between layers
*/
export class SplineLayerManager {
constructor(node) {
this.node = node;
this.activeWidget = null;
this.node.splineWidgetsCounter = 0;
this._noActiveSelection = false;
}
setActiveWidget(widget) {
if (this.activeWidget === widget) {
return;
}
// Track explicit "no selection" state
this._noActiveSelection = (widget == null);
// If the editor is in handdraw create/edit mode, disarm it when switching layers
// Exception: when switch is initiated by handdraw commit flow, one-shot suppress exit
try {
const ed = this.node?.editor;
const suppressOnce = !!ed?._suppressHanddrawExitOnce;
if (ed) ed._suppressHanddrawExitOnce = false; // always reset flag
if (ed && ed._handdrawMode && ed._handdrawMode !== 'off' && !suppressOnce) {
ed.exitHanddrawMode?.(false);
}
} catch {}
this.activeWidget = widget;
if (this.node.editor) {
this.node.editor.onActiveLayerChanged(); // Notify canvas
}
this.node.setDirtyCanvas(true, true);
}
getActiveWidget() {
if (this.activeWidget && !this.node.widgets.includes(this.activeWidget)) {
this.activeWidget = null;
}
if (!this.activeWidget) {
if (this._noActiveSelection) return null;
const splines = this.getSplineWidgets();
if (splines.length > 0) {
// Find the first visible/active spline widget, not just any spline
const visibleSpline = splines.find(w => w.value.on);
if (visibleSpline) {
this.activeWidget = visibleSpline;
} else {
// If no visible splines, we should not set an active widget
// This prevents trying to work with hidden layers
this.activeWidget = null;
}
}
}
return this.activeWidget;
}
getSplineWidgets() {
return this.node.widgets.filter(w =>
(w instanceof PowerSplineWidget) ||
(w instanceof HandDrawLayerWidget) ||
(w instanceof BoxLayerWidget)
);
}
addNewSpline(name) {
try {
this.node?.editor?.exitHanddrawMode?.(false);
} catch {}
this.node.splineWidgetsCounter++;
const widget = new PowerSplineWidget("spline_" + this.node.splineWidgetsCounter);
const baseName = name || `Spline ${this.node.splineWidgetsCounter}`;
const existingNames = this.getSplineWidgets().map(w => w.value.name);
widget.value.name = this.node.generateUniqueName(baseName, existingNames);
widget.value.type = 'spline';
widget.value.offset = 0;
widget.value.a_pause = 0;
widget.value.z_pause = 0;
widget.value.easing = 'in_out'; // Start with in_out as default (using simple naming)
widget.value.easingConfig = { path: "full", strength: 1.0 }; // Start with full path and strength 1.0
widget.value.driven = false; // Start with driven off
widget.value._drivenConfig = { driver: "", rotate: 0, d_scale: 1.0 }; // Preserved config
// Initialize with two points in normalized coordinates (center and slightly right)
// Two points prevent rendering issues and provide proper direction
const widthWidget = this.node.widgets.find(w => w.name === "mask_width");
const heightWidget = this.node.widgets.find(w => w.name === "mask_height");
const width = widthWidget ? widthWidget.value : 512;
const height = heightWidget ? heightWidget.value : 512;
const newPoints = [
{ x: Math.random() * 0.3 + 0.1, y: Math.random() * 0.8 + 0.1, highlighted: false },
{ x: Math.random() * 0.3 + 0.6, y: Math.random() * 0.8 + 0.1, highlighted: false }
];
widget.value.points_store = JSON.stringify(newPoints);
// Insert spline widgets AFTER the header (not before button_bar)
// This ensures order: canvas → button_bar → header → splines
const headerIndex = this.node.widgets.findIndex(w => w.name === "spline_header");
if (headerIndex !== -1) {
// Find the last layer widget (spline or handdraw) after the header
let insertIndex = headerIndex + 1;
for (let i = headerIndex + 1; i < this.node.widgets.length; i++) {
const wi = this.node.widgets[i];
if (wi instanceof PowerSplineWidget || wi instanceof HandDrawLayerWidget || wi instanceof BoxLayerWidget) {
insertIndex = i + 1;
} else {
break;
}
}
this.node.widgets.splice(insertIndex, 0, widget);
widget.parent = this.node;
} else {
this.node.addCustomWidget(widget);
}
this.node.updateNodeHeight();
this.setActiveWidget(widget);
// Force canvas to update after node height change
if (this.node.editor && this.node.editor.vis) {
this.node.editor.vis.render();
}
this.node.setDirtyCanvas(true, true);
return widget;
}
addNewHanddraw(name, activate = true) {
this.node.splineWidgetsCounter++;
const widget = new HandDrawLayerWidget("spline_" + this.node.splineWidgetsCounter);
const baseName = name || `Handdraw ${this.node.splineWidgetsCounter}`;
const existingNames = this.getSplineWidgets().map(w => w.value.name);
widget.value.name = this.node.generateUniqueName(baseName, existingNames);
const headerIndex = this.node.widgets.findIndex(w => w.name === "spline_header");
if (headerIndex !== -1) {
let insertIndex = headerIndex + 1;
for (let i = headerIndex + 1; i < this.node.widgets.length; i++) {
const wi = this.node.widgets[i];
if (wi instanceof PowerSplineWidget || wi instanceof HandDrawLayerWidget || wi instanceof BoxLayerWidget) {
insertIndex = i + 1;
} else {
break;
}
}
this.node.widgets.splice(insertIndex, 0, widget);
widget.parent = this.node;
} else {
this.node.addCustomWidget(widget);
}
this.node.updateNodeHeight();
if (activate) {
this.setActiveWidget(widget);
if (this.node.editor && this.node.editor.vis) {
this.node.editor.vis.render();
}
this.node.setDirtyCanvas(true, true);
}
return widget;
}
addNewBox(name, activate = true) {
this.node.splineWidgetsCounter++;
const widget = new BoxLayerWidget("box_" + this.node.splineWidgetsCounter);
const baseName = name || `Box ${this.node.splineWidgetsCounter}`;
const existingNames = this.getSplineWidgets().map(w => w.value.name);
widget.value.name = this.node.generateUniqueName(baseName, existingNames);
widget.value.points_store = JSON.stringify([{
x: 0.5,
y: 0.5,
highlighted: false,
boxScale: 1,
pointScale: 1,
scale: 1,
rotation: 0,
boxRotation: 0,
}]);
widget.value.box_keys = [];
widget.value.box_timeline_point = 1;
widget.value.interpolation = 'box';
const headerIndex = this.node.widgets.findIndex(w => w.name === "spline_header");
if (headerIndex !== -1) {
let insertIndex = headerIndex + 1;
for (let i = headerIndex + 1; i < this.node.widgets.length; i++) {
const wi = this.node.widgets[i];
if (wi instanceof PowerSplineWidget || wi instanceof HandDrawLayerWidget || wi instanceof BoxLayerWidget) {
insertIndex = i + 1;
} else {
break;
}
}
this.node.widgets.splice(insertIndex, 0, widget);
widget.parent = this.node;
} else {
this.node.addCustomWidget(widget);
}
this.node.updateNodeHeight();
if (activate) {
this.setActiveWidget(widget);
if (this.node.editor && this.node.editor.vis) {
this.node.editor.vis.render();
}
this.node.setDirtyCanvas(true, true);
}
return widget;
}
removeSpline(widget) {
const index = this.node.widgets.indexOf(widget);
if (index > -1) {
// Get all spline widgets before removal
const allSplines = this.getSplineWidgets();
console.log("[SplineLayerManager] removeSpline: removing", widget?.value?.name, "total splines:", allSplines.length);
// Check if the widget being removed is the active widget
const removingActiveWidget = (this.activeWidget === widget);
if (removingActiveWidget) {
console.log("[SplineLayerManager] removeSpline: target is active widget");
// Select a new active widget BEFORE removing the current one
let newActiveWidget = null;
// Try to find a replacement widget
if (allSplines.length > 1) {
// Prefer the next visible spline, or previous if this is the last one
const currentIndex = allSplines.indexOf(widget);
// Try next visible spline
for (let i = currentIndex + 1; i < allSplines.length; i++) {
if (allSplines[i].value.on) {
newActiveWidget = allSplines[i];
break;
}
}
// If no visible spline found after current, try before
if (!newActiveWidget) {
for (let i = currentIndex - 1; i >= 0; i--) {
if (allSplines[i].value.on) {
newActiveWidget = allSplines[i];
break;
}
}
}
// If still no visible spline, just take the next one (or previous)
if (!newActiveWidget) {
if (currentIndex + 1 < allSplines.length) {
newActiveWidget = allSplines[currentIndex + 1];
} else if (currentIndex - 1 >= 0) {
newActiveWidget = allSplines[currentIndex - 1];
}
}
}
// Set the new active widget (triggers editor sync)
this.setActiveWidget(newActiveWidget);
console.log("[SplineLayerManager] removeSpline: new active widget", newActiveWidget?.value?.name);
// If no active widget remains, clear the editor state
if (this.node.editor && !this.activeWidget) {
// No splines left - clear the editor's points
console.log("[SplineLayerManager] removeSpline: no active widget, clearing editor points");
this.node.editor.points = [];
if (this.node.editor.layerRenderer) {
this.node.editor.layerRenderer.render();
}
}
}
// Now remove the widget
this.node.widgets.splice(index, 1);
this.node.updateNodeHeight();
const editor = this.node.editor;
if (editor) {
console.log("[SplineLayerManager] removeSpline: refresh editor state. Active widget:", this.activeWidget?.value?.name);
if (this.activeWidget) {
// Refresh editor state based on newly active widget
if (typeof editor.onActiveLayerChanged === "function") {
console.log("[SplineLayerManager] removeSpline: calling editor.onActiveLayerChanged()");
editor.onActiveLayerChanged();
}
editor.points = editor.getActivePoints();
if (editor.layerRenderer) {
console.log("[SplineLayerManager] removeSpline: re-rendering layers");
editor.layerRenderer.render();
}
console.log("[SplineLayerManager] removeSpline: updating path");
editor.updatePath();
} else {
// No active spline remains
editor.points = [];
if (editor.layerRenderer) {
console.log("[SplineLayerManager] removeSpline: no layers left, rendering empty");
editor.layerRenderer.render();
}
}
if (editor.vis) {
console.log("[SplineLayerManager] removeSpline: rendering editor vis");
editor.vis.render();
}
}
this.node.setDirtyCanvas(true, true);
}
}
duplicateSpline(sourceWidget) {
if (!sourceWidget) return;
try {
this.node?.editor?.exitHanddrawMode?.(false);
} catch {}
this.node.splineWidgetsCounter++;
const isHand = sourceWidget instanceof HandDrawLayerWidget;
const isBox = sourceWidget instanceof BoxLayerWidget;
const WidgetClass = isHand ? HandDrawLayerWidget : (isBox ? BoxLayerWidget : PowerSplineWidget);
const widgetIdPrefix = isBox ? "box_" : "spline_";
const newWidget = new WidgetClass(widgetIdPrefix + this.node.splineWidgetsCounter);
// Deep copy of value.
const sourceValue = sourceWidget.value;
const newValue = JSON.parse(JSON.stringify(sourceValue));
// Generate a unique name
const existingNames = this.getSplineWidgets().map(w => w.value.name);
newValue.name = this.node.generateUniqueName(sourceValue.name, existingNames);
newWidget.value = newValue;
// Insert it after the source widget
const sourceIndex = this.node.widgets.indexOf(sourceWidget);
if (sourceIndex !== -1) {
this.node.widgets.splice(sourceIndex + 1, 0, newWidget);
newWidget.parent = this.node;
} else {
// Fallback, should not happen if sourceWidget is valid
// Insert after header (same logic as addNewSpline)
const headerIndex = this.node.widgets.findIndex(w => w.name === "spline_header");
if (headerIndex !== -1) {
let insertIndex = headerIndex + 1;
for (let i = headerIndex + 1; i < this.node.widgets.length; i++) {
const wi = this.node.widgets[i];
if (wi instanceof PowerSplineWidget || wi instanceof HandDrawLayerWidget || wi instanceof BoxLayerWidget) {
insertIndex = i + 1;
} else {
break;
}
}
this.node.widgets.splice(insertIndex, 0, newWidget);
newWidget.parent = this.node;
} else {
this.node.addCustomWidget(newWidget);
}
}
this.node.updateNodeHeight();
this.setActiveWidget(newWidget);
// Force canvas to update after node height change
if (this.node.editor && this.node.editor.vis) {
this.node.editor.vis.render();
}
this.node.setDirtyCanvas(true, true);
return newWidget;
}
removeAllSplines() {
// Remove all spline widgets
const splineWidgets = this.getSplineWidgets();
splineWidgets.forEach(widget => {
const index = this.node.widgets.indexOf(widget);
if (index > -1) {
this.node.widgets.splice(index, 1);
}
});
// Clear active widget reference
this.activeWidget = null;
// Update node height
this.node.updateNodeHeight();
// Trigger re-render of all layers
if (this.node.editor && this.node.editor.layerRenderer) {
this.node.editor.layerRenderer.render();
}
// Force canvas to update after node height change
if (this.node.editor && this.node.editor.vis) {
this.node.editor.vis.render();
}
this.node.setDirtyCanvas(true, true);
}
recreateSplinesFromData(widgets_values) {
this.node.widgets = this.node.widgets.filter(w =>
!(w instanceof PowerSplineWidget) &&
!(w instanceof HandDrawLayerWidget) &&
!(w instanceof BoxLayerWidget));
// Create a map of widget data by name for safe, unambiguous lookup
// This prevents any cross-contamination between widget values
const widgetDataByName = new Map();
for (const widgetValue of widgets_values || []) {
if (widgetValue?.name && widgetValue.interpolation !== undefined) {
widgetDataByName.set(widgetValue.name, widgetValue);
}
}
// Recreate widgets using name-based lookup with explicit field assignment
for (const [name, widgetValue] of widgetDataByName.entries()) {
this.node.splineWidgetsCounter++;
const isHand = widgetValue.type === 'handdraw';
const isBox = widgetValue.type === 'box_layer';
const widgetIdPrefix = isBox ? "box_" : "spline_";
const widget = isHand
? new HandDrawLayerWidget("spline_" + this.node.splineWidgetsCounter)
: (isBox ? new BoxLayerWidget(widgetIdPrefix + this.node.splineWidgetsCounter)
: new PowerSplineWidget("spline_" + this.node.splineWidgetsCounter));
// Explicitly assign each field to ensure complete isolation
// No spread operators that might share references
widget.value = {
on: widgetValue.on !== undefined ? widgetValue.on : true,
name: widgetValue.name,
type: isBox ? 'box_layer' : (isHand ? 'handdraw' : 'spline'),
interpolation: widgetValue.interpolation,
repeat: widgetValue.repeat || 1,
offset: widgetValue.offset || 0,
a_pause: widgetValue.a_pause || 0,
z_pause: widgetValue.z_pause || 0,
easing: widgetValue.easing || 'linear',
easingConfig: widgetValue.easingConfig || { path: "full", strength: 1.0 },
driven: widgetValue.driven || false,
_drivenConfig: widgetValue._drivenConfig || (typeof widgetValue.driven === 'object' ? {...widgetValue.driven} : { driver: "", rotate: 0, d_scale: 1.0 }),
scale: widgetValue.scale !== undefined ? widgetValue.scale : 1.00,
points_store: widgetValue.points_store || "[]",
coordinates: widgetValue.coordinates || "[]"
};
if (isBox) {
widget.value.box_keys = widgetValue.box_keys || [];
widget.value.box_timeline_point = widgetValue.box_timeline_point || 1;
widget.value.box_interpolation = widgetValue.box_interpolation || 'linear'; // Restore box interpolation
}
// Insert spline widgets AFTER the header (same as addNewSpline logic)
const headerIndex = this.node.widgets.findIndex(w => w.name === "spline_header");
if (headerIndex !== -1) {
// Find the last layer widget after the header
let insertIndex = headerIndex + 1;
for (let i = headerIndex + 1; i < this.node.widgets.length; i++) {
const wi = this.node.widgets[i];
if (wi instanceof PowerSplineWidget || wi instanceof HandDrawLayerWidget || wi instanceof BoxLayerWidget) {
insertIndex = i + 1;
} else {
break;
}
}
this.node.widgets.splice(insertIndex, 0, widget);
widget.parent = this.node;
} else {
this.node.widgets.push(widget);
widget.parent = this.node;
}
}
// Reset active widget since all spline widgets are being recreated
// This is critical to prevent stale references after node restoration
const oldActiveWidget = this.activeWidget;
this.activeWidget = null;
// If there were widgets before recreation, we should try to set a new active widget
if (this.node.widgets.length > 0) {
const splineWidgets = this.getSplineWidgets();
if (splineWidgets.length > 0) {
// Find the first visible spline as the new active widget
const visibleSpline = splineWidgets.find(w => w.value.on);
if (visibleSpline) {
this.activeWidget = visibleSpline;
} else {
// If no visible splines, select the first one (even if hidden)
this.activeWidget = splineWidgets[0];
}
}
}
}
}
/**
* Initialize all layer UI components and add them to the node
* This is the main entry point for setting up the layer management interface
*
* @param {Object} node - The LiteGraph node to add layer UI to
* @returns {Object} An object containing references to created widgets
*/
export function initializeLayerUI(node) {
if (!node || !node.addCustomWidget) {
console.error('initializeLayerUI: Invalid node provided');
return null;
}
// Create button bar widget (Add Spline, Add Keyframes, Draw, Duplicate)
const buttonBarWidget = createButtonBarWidget();
// Create header widget (column labels)
const headerWidget = new PowerSplineHeaderWidget("spline_header");
// Add widgets to node in the correct order
// Order matters: buttons first, then header, then individual layers will follow
node.addCustomWidget(buttonBarWidget);
node.addCustomWidget(headerWidget);
// Return references for potential future use
return {
buttonBar: buttonBarWidget,
header: headerWidget,
};
}
/**
* Get all layer type classes for type checking
* This is useful for identifying layer widgets vs other widget types
*
* @returns {Object} Object containing all layer widget classes
*/
export function getLayerWidgetClasses() {
// Lazy import to avoid circular dependencies
const { PowerSplineWidget } = require('./layer_type_spline.js');
const { HandDrawLayerWidget } = require('./layer_type_draw.js');
const { BoxLayerWidget } = require('./layer_type_box.js');
return {
PowerSplineWidget,
HandDrawLayerWidget,
BoxLayerWidget,
};
}
/**
* Check if a widget is a layer widget (any type)
*
* @param {Object} widget - The widget to check
* @returns {boolean} True if widget is a layer widget
*/
export function isLayerWidget(widget) {
if (!widget) return false;
// Import layer classes (using dynamic import to avoid circular deps)
const { PowerSplineWidget } = require('./layer_type_spline.js');
const { HandDrawLayerWidget } = require('./layer_type_draw.js');
const { BoxLayerWidget } = require('./layer_type_box.js');
return (
widget instanceof PowerSplineWidget ||
widget instanceof HandDrawLayerWidget ||
widget instanceof BoxLayerWidget
);
}
/**
* Get the type name of a layer widget
*
* @param {Object} widget - The layer widget
* @returns {string|null} The type name ('spline', 'handdraw', 'box') or null
*/
export function getLayerWidgetType(widget) {
if (!widget) return null;
const { PowerSplineWidget } = require('./layer_type_spline.js');
const { HandDrawLayerWidget } = require('./layer_type_draw.js');
const { BoxLayerWidget } = require('./layer_type_box.js');
if (widget instanceof HandDrawLayerWidget) return 'handdraw';
if (widget instanceof BoxLayerWidget) return 'box';
if (widget instanceof PowerSplineWidget) return 'spline';
return null;
}
+6 -663
View File
@@ -1,8 +1,9 @@
import { app } from '../../../scripts/app.js';
import { makeUUID, loadScript, create_documentation_stylesheet, RgthreeBaseWidget, DimensionsWidget, TopRowWidget, PowerSplineWidget, PowerSplineHeaderWidget, NodeSizeManager, drawWidgetButton } from './spline_utils.js';
import { HandDrawLayerWidget, commitHanddrawPath } from './handdraw_layer.js';
import { BoxLayerWidget } from './box_layer.js';
import { chainCallback, hideWidgetForGood } from './widget_utils.js';
import { HandDrawLayerWidget, commitHanddrawPath } from './layer_type_draw.js';
import { BoxLayerWidget } from './layer_type_box.js';
import { chainCallback, hideWidgetForGood } from './general_utils.js';
import { initializeLayerUI, SplineLayerManager } from './layer_ui_main_widget.js';
import SplineEditor2 from './canvas/canvas_main.js';
import { getSlotInPosition, getSlotMenuOptions, showCustomDrivenToggleMenu } from './context_menu.js';
import { drawDriverLines } from './driver_line_renderer.js';
@@ -245,494 +246,6 @@ async function loadCachedRefImageAsBase64() {
// == SPLINE LAYER MANAGER
class SplineLayerManager {
constructor(node) {
this.node = node;
this.activeWidget = null;
this.node.splineWidgetsCounter = 0;
this._noActiveSelection = false;
}
setActiveWidget(widget) {
if (this.activeWidget === widget) {
return;
}
// Track explicit "no selection" state
this._noActiveSelection = (widget == null);
// If the editor is in handdraw create/edit mode, disarm it when switching layers
// Exception: when switch is initiated by handdraw commit flow, one-shot suppress exit
try {
const ed = this.node?.editor;
const suppressOnce = !!ed?._suppressHanddrawExitOnce;
if (ed) ed._suppressHanddrawExitOnce = false; // always reset flag
if (ed && ed._handdrawMode && ed._handdrawMode !== 'off' && !suppressOnce) {
ed.exitHanddrawMode?.(false);
}
} catch {}
this.activeWidget = widget;
if (this.node.editor) {
this.node.editor.onActiveLayerChanged(); // Notify canvas
}
this.node.setDirtyCanvas(true, true);
}
getActiveWidget() {
if (this.activeWidget && !this.node.widgets.includes(this.activeWidget)) {
this.activeWidget = null;
}
if (!this.activeWidget) {
if (this._noActiveSelection) return null;
const splines = this.getSplineWidgets();
if (splines.length > 0) {
// Find the first visible/active spline widget, not just any spline
const visibleSpline = splines.find(w => w.value.on);
if (visibleSpline) {
this.activeWidget = visibleSpline;
} else {
// If no visible splines, we should not set an active widget
// This prevents trying to work with hidden layers
this.activeWidget = null;
}
}
}
return this.activeWidget;
}
getSplineWidgets() {
return this.node.widgets.filter(w =>
(w instanceof PowerSplineWidget) ||
(w instanceof HandDrawLayerWidget) ||
(w instanceof BoxLayerWidget)
);
}
addNewSpline(name) {
try {
this.node?.editor?.exitHanddrawMode?.(false);
} catch {}
this.node.splineWidgetsCounter++;
const widget = new PowerSplineWidget("spline_" + this.node.splineWidgetsCounter);
const baseName = name || `Spline ${this.node.splineWidgetsCounter}`;
const existingNames = this.getSplineWidgets().map(w => w.value.name);
widget.value.name = this.node.generateUniqueName(baseName, existingNames);
widget.value.type = 'spline';
widget.value.offset = 0;
widget.value.a_pause = 0;
widget.value.z_pause = 0;
widget.value.easing = 'in_out'; // Start with in_out as default (using simple naming)
widget.value.easingConfig = { path: "full", strength: 1.0 }; // Start with full path and strength 1.0
widget.value.driven = false; // Start with driven off
widget.value._drivenConfig = { driver: "", rotate: 0, d_scale: 1.0 }; // Preserved config
// Initialize with two points in normalized coordinates (center and slightly right)
// Two points prevent rendering issues and provide proper direction
const widthWidget = this.node.widgets.find(w => w.name === "mask_width");
const heightWidget = this.node.widgets.find(w => w.name === "mask_height");
const width = widthWidget ? widthWidget.value : 512;
const height = heightWidget ? heightWidget.value : 512;
const newPoints = [
{ x: Math.random() * 0.3 + 0.1, y: Math.random() * 0.8 + 0.1, highlighted: false },
{ x: Math.random() * 0.3 + 0.6, y: Math.random() * 0.8 + 0.1, highlighted: false }
];
widget.value.points_store = JSON.stringify(newPoints);
// Insert spline widgets AFTER the header (not before button_bar)
// This ensures order: canvas ? button_bar ? header ? splines
const headerIndex = this.node.widgets.findIndex(w => w.name === "spline_header");
if (headerIndex !== -1) {
// Find the last layer widget (spline or handdraw) after the header
let insertIndex = headerIndex + 1;
for (let i = headerIndex + 1; i < this.node.widgets.length; i++) {
const wi = this.node.widgets[i];
if (wi instanceof PowerSplineWidget || wi instanceof HandDrawLayerWidget || wi instanceof BoxLayerWidget) {
insertIndex = i + 1;
} else {
break;
}
}
this.node.widgets.splice(insertIndex, 0, widget);
widget.parent = this.node;
} else {
this.node.addCustomWidget(widget);
}
this.node.updateNodeHeight();
this.setActiveWidget(widget);
// Force canvas to update after node height change
if (this.node.editor && this.node.editor.vis) {
this.node.editor.vis.render();
}
this.node.setDirtyCanvas(true, true);
return widget;
}
addNewHanddraw(name, activate = true) {
this.node.splineWidgetsCounter++;
const widget = new HandDrawLayerWidget("spline_" + this.node.splineWidgetsCounter);
const baseName = name || `Handdraw ${this.node.splineWidgetsCounter}`;
const existingNames = this.getSplineWidgets().map(w => w.value.name);
widget.value.name = this.node.generateUniqueName(baseName, existingNames);
const headerIndex = this.node.widgets.findIndex(w => w.name === "spline_header");
if (headerIndex !== -1) {
let insertIndex = headerIndex + 1;
for (let i = headerIndex + 1; i < this.node.widgets.length; i++) {
const wi = this.node.widgets[i];
if (wi instanceof PowerSplineWidget || wi instanceof HandDrawLayerWidget || wi instanceof BoxLayerWidget) {
insertIndex = i + 1;
} else {
break;
}
}
this.node.widgets.splice(insertIndex, 0, widget);
widget.parent = this.node;
} else {
this.node.addCustomWidget(widget);
}
this.node.updateNodeHeight();
if (activate) {
this.setActiveWidget(widget);
if (this.node.editor && this.node.editor.vis) {
this.node.editor.vis.render();
}
this.node.setDirtyCanvas(true, true);
}
return widget;
}
addNewBox(name, activate = true) {
this.node.splineWidgetsCounter++;
const widget = new BoxLayerWidget("box_" + this.node.splineWidgetsCounter);
const baseName = name || `Box ${this.node.splineWidgetsCounter}`;
const existingNames = this.getSplineWidgets().map(w => w.value.name);
widget.value.name = this.node.generateUniqueName(baseName, existingNames);
widget.value.points_store = JSON.stringify([{
x: 0.5,
y: 0.5,
highlighted: false,
boxScale: 1,
pointScale: 1,
scale: 1,
rotation: 0,
boxRotation: 0,
}]);
widget.value.box_keys = [];
widget.value.box_timeline_point = 1;
widget.value.interpolation = 'box';
const headerIndex = this.node.widgets.findIndex(w => w.name === "spline_header");
if (headerIndex !== -1) {
let insertIndex = headerIndex + 1;
for (let i = headerIndex + 1; i < this.node.widgets.length; i++) {
const wi = this.node.widgets[i];
if (wi instanceof PowerSplineWidget || wi instanceof HandDrawLayerWidget || wi instanceof BoxLayerWidget) {
insertIndex = i + 1;
} else {
break;
}
}
this.node.widgets.splice(insertIndex, 0, widget);
widget.parent = this.node;
} else {
this.node.addCustomWidget(widget);
}
this.node.updateNodeHeight();
if (activate) {
this.setActiveWidget(widget);
if (this.node.editor && this.node.editor.vis) {
this.node.editor.vis.render();
}
this.node.setDirtyCanvas(true, true);
}
return widget;
}
removeSpline(widget) {
const index = this.node.widgets.indexOf(widget);
if (index > -1) {
// Get all spline widgets before removal
const allSplines = this.getSplineWidgets();
console.log("[SplineLayerManager] removeSpline: removing", widget?.value?.name, "total splines:", allSplines.length);
// Check if the widget being removed is the active widget
const removingActiveWidget = (this.activeWidget === widget);
if (removingActiveWidget) {
console.log("[SplineLayerManager] removeSpline: target is active widget");
// Select a new active widget BEFORE removing the current one
let newActiveWidget = null;
// Try to find a replacement widget
if (allSplines.length > 1) {
// Prefer the next visible spline, or previous if this is the last one
const currentIndex = allSplines.indexOf(widget);
// Try next visible spline
for (let i = currentIndex + 1; i < allSplines.length; i++) {
if (allSplines[i].value.on) {
newActiveWidget = allSplines[i];
break;
}
}
// If no visible spline found after current, try before
if (!newActiveWidget) {
for (let i = currentIndex - 1; i >= 0; i--) {
if (allSplines[i].value.on) {
newActiveWidget = allSplines[i];
break;
}
}
}
// If still no visible spline, just take the next one (or previous)
if (!newActiveWidget) {
if (currentIndex + 1 < allSplines.length) {
newActiveWidget = allSplines[currentIndex + 1];
} else if (currentIndex - 1 >= 0) {
newActiveWidget = allSplines[currentIndex - 1];
}
}
}
// Set the new active widget (triggers editor sync)
this.setActiveWidget(newActiveWidget);
console.log("[SplineLayerManager] removeSpline: new active widget", newActiveWidget?.value?.name);
// If no active widget remains, clear the editor state
if (this.node.editor && !this.activeWidget) {
// No splines left - clear the editor's points
console.log("[SplineLayerManager] removeSpline: no active widget, clearing editor points");
this.node.editor.points = [];
if (this.node.editor.layerRenderer) {
this.node.editor.layerRenderer.render();
}
}
}
// Now remove the widget
this.node.widgets.splice(index, 1);
this.node.updateNodeHeight();
const editor = this.node.editor;
if (editor) {
console.log("[SplineLayerManager] removeSpline: refresh editor state. Active widget:", this.activeWidget?.value?.name);
if (this.activeWidget) {
// Refresh editor state based on newly active widget
if (typeof editor.onActiveLayerChanged === "function") {
console.log("[SplineLayerManager] removeSpline: calling editor.onActiveLayerChanged()");
editor.onActiveLayerChanged();
}
editor.points = editor.getActivePoints();
if (editor.layerRenderer) {
console.log("[SplineLayerManager] removeSpline: re-rendering layers");
editor.layerRenderer.render();
}
console.log("[SplineLayerManager] removeSpline: updating path");
editor.updatePath();
} else {
// No active spline remains
editor.points = [];
if (editor.layerRenderer) {
console.log("[SplineLayerManager] removeSpline: no layers left, rendering empty");
editor.layerRenderer.render();
}
}
if (editor.vis) {
console.log("[SplineLayerManager] removeSpline: rendering editor vis");
editor.vis.render();
}
}
this.node.setDirtyCanvas(true, true);
}
}
duplicateSpline(sourceWidget) {
if (!sourceWidget) return;
try {
this.node?.editor?.exitHanddrawMode?.(false);
} catch {}
this.node.splineWidgetsCounter++;
const isHand = sourceWidget instanceof HandDrawLayerWidget;
const isBox = sourceWidget instanceof BoxLayerWidget;
const WidgetClass = isHand ? HandDrawLayerWidget : (isBox ? BoxLayerWidget : PowerSplineWidget);
const widgetIdPrefix = isBox ? "box_" : "spline_";
const newWidget = new WidgetClass(widgetIdPrefix + this.node.splineWidgetsCounter);
// Deep copy of value.
const sourceValue = sourceWidget.value;
const newValue = JSON.parse(JSON.stringify(sourceValue));
// Generate a unique name
const existingNames = this.getSplineWidgets().map(w => w.value.name);
newValue.name = this.node.generateUniqueName(sourceValue.name, existingNames);
newWidget.value = newValue;
// Insert it after the source widget
const sourceIndex = this.node.widgets.indexOf(sourceWidget);
if (sourceIndex !== -1) {
this.node.widgets.splice(sourceIndex + 1, 0, newWidget);
newWidget.parent = this.node;
} else {
// Fallback, should not happen if sourceWidget is valid
// Insert after header (same logic as addNewSpline)
const headerIndex = this.node.widgets.findIndex(w => w.name === "spline_header");
if (headerIndex !== -1) {
let insertIndex = headerIndex + 1;
for (let i = headerIndex + 1; i < this.node.widgets.length; i++) {
const wi = this.node.widgets[i];
if (wi instanceof PowerSplineWidget || wi instanceof HandDrawLayerWidget || wi instanceof BoxLayerWidget) {
insertIndex = i + 1;
} else {
break;
}
}
this.node.widgets.splice(insertIndex, 0, newWidget);
newWidget.parent = this.node;
} else {
this.node.addCustomWidget(newWidget);
}
}
this.node.updateNodeHeight();
this.setActiveWidget(newWidget);
// Force canvas to update after node height change
if (this.node.editor && this.node.editor.vis) {
this.node.editor.vis.render();
}
this.node.setDirtyCanvas(true, true);
return newWidget;
}
removeAllSplines() {
// Remove all spline widgets
const splineWidgets = this.getSplineWidgets();
splineWidgets.forEach(widget => {
const index = this.node.widgets.indexOf(widget);
if (index > -1) {
this.node.widgets.splice(index, 1);
}
});
// Clear active widget reference
this.activeWidget = null;
// Update node height
this.node.updateNodeHeight();
// Trigger re-render of all layers
if (this.node.editor && this.node.editor.layerRenderer) {
this.node.editor.layerRenderer.render();
}
// Force canvas to update after node height change
if (this.node.editor && this.node.editor.vis) {
this.node.editor.vis.render();
}
this.node.setDirtyCanvas(true, true);
}
recreateSplinesFromData(widgets_values) {
this.node.widgets = this.node.widgets.filter(w =>
!(w instanceof PowerSplineWidget) &&
!(w instanceof HandDrawLayerWidget) &&
!(w instanceof BoxLayerWidget));
// Create a map of widget data by name for safe, unambiguous lookup
// This prevents any cross-contamination between widget values
const widgetDataByName = new Map();
for (const widgetValue of widgets_values || []) {
if (widgetValue?.name && widgetValue.interpolation !== undefined) {
widgetDataByName.set(widgetValue.name, widgetValue);
}
}
// Recreate widgets using name-based lookup with explicit field assignment
for (const [name, widgetValue] of widgetDataByName.entries()) {
this.node.splineWidgetsCounter++;
const isHand = widgetValue.type === 'handdraw';
const isBox = widgetValue.type === 'box_layer';
const widgetIdPrefix = isBox ? "box_" : "spline_";
const widget = isHand
? new HandDrawLayerWidget("spline_" + this.node.splineWidgetsCounter)
: (isBox ? new BoxLayerWidget(widgetIdPrefix + this.node.splineWidgetsCounter)
: new PowerSplineWidget("spline_" + this.node.splineWidgetsCounter));
// Explicitly assign each field to ensure complete isolation
// No spread operators that might share references
widget.value = {
on: widgetValue.on !== undefined ? widgetValue.on : true,
name: widgetValue.name,
type: isBox ? 'box_layer' : (isHand ? 'handdraw' : 'spline'),
interpolation: widgetValue.interpolation,
repeat: widgetValue.repeat || 1,
offset: widgetValue.offset || 0,
a_pause: widgetValue.a_pause || 0,
z_pause: widgetValue.z_pause || 0,
easing: widgetValue.easing || 'linear',
easingConfig: widgetValue.easingConfig || { path: "full", strength: 1.0 },
driven: widgetValue.driven || false,
_drivenConfig: widgetValue._drivenConfig || (typeof widgetValue.driven === 'object' ? {...widgetValue.driven} : { driver: "", rotate: 0, d_scale: 1.0 }),
scale: widgetValue.scale !== undefined ? widgetValue.scale : 1.00,
points_store: widgetValue.points_store || "[]",
coordinates: widgetValue.coordinates || "[]"
};
if (isBox) {
widget.value.box_keys = widgetValue.box_keys || [];
widget.value.box_timeline_point = widgetValue.box_timeline_point || 1;
widget.value.box_interpolation = widgetValue.box_interpolation || 'linear'; // Restore box interpolation
}
// Insert spline widgets AFTER the header (same as addNewSpline logic)
const headerIndex = this.node.widgets.findIndex(w => w.name === "spline_header");
if (headerIndex !== -1) {
// Find the last layer widget after the header
let insertIndex = headerIndex + 1;
for (let i = headerIndex + 1; i < this.node.widgets.length; i++) {
const wi = this.node.widgets[i];
if (wi instanceof PowerSplineWidget || wi instanceof HandDrawLayerWidget || wi instanceof BoxLayerWidget) {
insertIndex = i + 1;
} else {
break;
}
}
this.node.widgets.splice(insertIndex, 0, widget);
widget.parent = this.node;
} else {
this.node.widgets.push(widget);
widget.parent = this.node;
}
}
// Reset active widget since all spline widgets are being recreated
// This is critical to prevent stale references after node restoration
const oldActiveWidget = this.activeWidget;
this.activeWidget = null;
// If there were widgets before recreation, we should try to set a new active widget
if (this.node.widgets.length > 0) {
const splineWidgets = this.getSplineWidgets();
if (splineWidgets.length > 0) {
// Find the first visible spline as the new active widget
const visibleSpline = splineWidgets.find(w => w.value.on);
if (visibleSpline) {
this.activeWidget = visibleSpline;
} else {
// If no visible splines, select the first one (even if hidden)
this.activeWidget = splineWidgets[0];
}
}
}
}
}
// Safe sessionStorage setter to avoid quota errors
function safeSetSessionItem(key, value) {
try { sessionStorage.setItem(key, value); } catch (e) { /* ignore quota/unsupported */ }
@@ -1987,178 +1500,8 @@ app.registerExtension({
});
}
// Add button bar widget AFTER canvas - positioned between canvas and layers
const buttonBarWidget = new RgthreeBaseWidget("button_bar");
buttonBarWidget.type = "custom";
buttonBarWidget.serialize = false;
// Track mouse state for button hover effects
buttonBarWidget.addSplineMouseDown = false;
buttonBarWidget.addBoxMouseDown = false;
buttonBarWidget.drawMouseDown = false;
buttonBarWidget.duplicateMouseDown = false;
buttonBarWidget.computeSize = function(width) {
return [width, LiteGraph.NODE_WIDGET_HEIGHT];
};
buttonBarWidget.draw = function(ctx, node, width, posY, height) {
const margin = 15;
const gap = 5;
const totalWidth = width - margin * 2 - gap * 3;
const addSplineWidth = totalWidth * 0.30;
const addBoxWidth = totalWidth * 0.30;
const drawWidth = totalWidth * 0.20;
const duplicateWidth = totalWidth * 0.20;
const addSplineX = margin;
const addBoxX = addSplineX + addSplineWidth + gap;
const drawX = addBoxX + addBoxWidth + gap;
const duplicateX = drawX + drawWidth + gap;
// Draw Add Spline button (30% width)
drawWidgetButton(
ctx,
{ size: [addSplineWidth, height], pos: [addSplineX, posY] },
"➕ Add Spline",
this.addSplineMouseDown
);
// Draw Add Box button (30% width)
drawWidgetButton(
ctx,
{ size: [addBoxWidth, height], pos: [addBoxX, posY] },
"🎥 Add Keyframes",
this.addBoxMouseDown
);
// Draw button (20% width)
drawWidgetButton(
ctx,
{ size: [drawWidth, height], pos: [drawX, posY] },
"✏️ Draw",
this.drawMouseDown || (node?.editor?._handdrawMode === 'create')
);
// Draw Duplicate button (20% width)
drawWidgetButton(
ctx,
{ size: [duplicateWidth, height], pos: [duplicateX, posY] },
"🧬 Duplicate",
this.duplicateMouseDown
);
};
buttonBarWidget.mouse = function(event, pos, node) {
if (event.type === "pointerdown" || event.type === "mousedown") {
const margin = 15;
const gap = 5;
const width = node.size[0];
const totalWidth = width - margin * 2 - gap * 3;
const addSplineWidth = totalWidth * 0.30;
const addBoxWidth = totalWidth * 0.30;
const drawWidth = totalWidth * 0.20;
const duplicateWidth = totalWidth * 0.20;
const addSplineLeft = margin;
const addSplineRight = addSplineLeft + addSplineWidth;
const addBoxLeft = addSplineRight + gap;
const addBoxRight = addBoxLeft + addBoxWidth;
const drawLeft = addBoxRight + gap;
const drawRight = drawLeft + drawWidth;
const duplicateLeft = drawRight + gap;
const duplicateRight = duplicateLeft + duplicateWidth;
if (pos[0] >= addSplineLeft && pos[0] <= addSplineRight) {
this.addSplineMouseDown = true;
node.setDirtyCanvas(true, false);
return true;
}
else if (pos[0] >= addBoxLeft && pos[0] <= addBoxRight) {
this.addBoxMouseDown = true;
node.setDirtyCanvas(true, false);
return true;
}
else if (pos[0] >= drawLeft && pos[0] <= drawRight) {
this.drawMouseDown = true;
node.setDirtyCanvas(true, false);
return true;
}
else if (pos[0] >= duplicateLeft &&
pos[0] <= duplicateRight) {
this.duplicateMouseDown = true;
node.setDirtyCanvas(true, false);
return true;
}
}
else if (event.type === "pointerup" || event.type === "mouseup") {
const margin = 15;
const gap = 5;
const width = node.size[0];
const totalWidth = width - margin * 2 - gap * 3;
const addSplineWidth = totalWidth * 0.30;
const addBoxWidth = totalWidth * 0.30;
const drawWidth = totalWidth * 0.20;
const duplicateWidth = totalWidth * 0.20;
const addSplineLeft = margin;
const addSplineRight = addSplineLeft + addSplineWidth;
const addBoxLeft = addSplineRight + gap;
const addBoxRight = addBoxLeft + addBoxWidth;
const drawLeft = addBoxRight + gap;
const drawRight = drawLeft + drawWidth;
const duplicateLeft = drawRight + gap;
const duplicateRight = duplicateLeft + duplicateWidth;
if (this.addSplineMouseDown &&
pos[0] >= addSplineLeft && pos[0] <= addSplineRight) {
if (node?.editor && node.editor._handdrawMode === 'create') {
node.editor.exitHanddrawMode?.(false);
}
node.layerManager.addNewSpline();
}
else if (this.addBoxMouseDown &&
pos[0] >= addBoxLeft && pos[0] <= addBoxRight) {
if (node?.editor && node.editor._handdrawMode === 'create') {
node.editor.exitHanddrawMode?.(false);
}
if (node.layerManager?.addNewBox) {
node.layerManager.addNewBox();
}
}
else if (this.drawMouseDown &&
pos[0] >= drawLeft && pos[0] <= drawRight) {
if (node?.editor) {
if (node.editor._handdrawMode === 'create') {
node.editor.exitHanddrawMode?.(false);
} else {
node.editor.enterHanddrawMode?.('create');
}
node.setDirtyCanvas(true, true);
}
}
else if (this.duplicateMouseDown &&
pos[0] >= duplicateLeft && pos[0] <= duplicateRight) {
if (node?.editor && node.editor._handdrawMode === 'create') {
node.editor.exitHanddrawMode?.(false);
}
const activeWidget = node.layerManager.getActiveWidget();
if (activeWidget) {
node.layerManager.duplicateSpline(activeWidget);
}
}
this.addSplineMouseDown = false;
this.addBoxMouseDown = false;
this.drawMouseDown = false;
this.duplicateMouseDown = false;
node.setDirtyCanvas(true, false);
return true;
}
return false;
};
this.addCustomWidget(buttonBarWidget);
// Add header widget AFTER button bar
this.addCustomWidget(new PowerSplineHeaderWidget("spline_header"));
// Initialize layer UI (button bar + header) - positioned between canvas and layers
initializeLayerUI(this);
// Removed double-click rename behavior (no longer used)
+23 -420
View File
@@ -2,31 +2,34 @@ import { app } from '../../../scripts/app.js';
import { showCustomDrivenToggleMenu, showCustomEasingMenu } from './context_menu.js';
import { initializeDrivenConfig, initializeEasingConfig, toggleDrivenState, prepareDrivenMenu } from './persistence.js';
import { NodeSizeManager } from './node_size_manager.js';
import { PowerSplineHeaderWidget, PowerSplineWidget } from './widget_utils.js';
import {
binarySearch,
fitString,
measureText,
isLowQuality,
drawRoundedRectangle,
drawWidgetButton,
drawTogglePart,
import { PowerSplineHeaderWidget } from './layer_header.js';
import { PowerSplineWidget } from './layer_type_spline.js';
import { TopRowWidget } from './canvas_top_row.js';
import {
binarySearch,
fitString,
measureText,
isLowQuality,
drawRoundedRectangle,
drawWidgetButton,
drawTogglePart,
drawNumberWidgetPart,
RgthreeBaseWidget
} from './drawing_utils.js';
// Re-export for convenience
export {
NodeSizeManager,
PowerSplineHeaderWidget,
PowerSplineWidget,
binarySearch,
fitString,
measureText,
isLowQuality,
drawRoundedRectangle,
drawWidgetButton,
drawTogglePart,
export {
NodeSizeManager,
PowerSplineHeaderWidget,
PowerSplineWidget,
TopRowWidget,
binarySearch,
fitString,
measureText,
isLowQuality,
drawRoundedRectangle,
drawWidgetButton,
drawTogglePart,
drawNumberWidgetPart,
RgthreeBaseWidget
};
@@ -110,406 +113,6 @@ export const create_documentation_stylesheet = () => {
}
}
// === TOP ROW WIDGET (Combined refresh button, bg_image dropdown, and width/height controls) ===
export class TopRowWidget extends RgthreeBaseWidget {
constructor(name = "TopRowWidget") {
super(name);
this.type = "custom";
this.options = { serialize: false };
this.value = {};
this.haveMouseMovedValue = false;
this.canvasButtonMouseDown = false;
this.framesButtonMouseDown = false;
this.hitAreas = {
refreshCanvasButton: { bounds: [0, 0], onClick: null },
refreshFramesButton: { bounds: [0, 0], onClick: null },
bgImgLeftArrow: { bounds: [0, 0], onClick: null },
bgImgVal: { bounds: [0, 0], onClick: null },
bgImgRightArrow: { bounds: [0, 0], onClick: null },
bgImgAny: { bounds: [0, 0], onClick: null },
widthDec: { bounds: [0, 0], onClick: null },
widthVal: { bounds: [0, 0], onClick: null },
widthInc: { bounds: [0, 0], onClick: null },
widthAny: { bounds: [0, 0], onMove: null },
heightDec: { bounds: [0, 0], onClick: null },
heightVal: { bounds: [0, 0], onClick: null },
heightInc: { bounds: [0, 0], onClick: null },
heightAny: { bounds: [0, 0], onMove: null },
};
}
draw(ctx, node, w, posY, height) {
const margin = 15;
const spacing = 10; // Reduced spacing to fit within 100%
const midY = posY + height * 0.5;
ctx.save();
// Get widget values
const widthWidget = node.widgets?.find(w => w.name === "mask_width");
const widthValue = widthWidget ? widthWidget.value : 512;
const heightWidget = node.widgets?.find(w => w.name === "mask_height");
const heightValue = heightWidget ? heightWidget.value : 512;
const bgImgWidget = node.widgets?.find(w => w.name === "bg_img");
const bgImgValue = bgImgWidget ? bgImgWidget.value : "None";
// Calculate available width for components (excluding margins and spacing)
const availableWidth = node.size[0] - margin * 2 - spacing * 4; // Account for spacing between elements
// Calculate component widths based on percentages (now totaling 100% of available width)
const refreshCanvasWidth = availableWidth * 0.12;
const refreshFramesWidth = availableWidth * 0.12;
const bgImgDropdownWidth = availableWidth * 0.14;
const iconButtonWidth = Math.max(20, Math.min(28, availableWidth * 0.05));
const dimensionsAreaWidth = availableWidth - (
refreshCanvasWidth + spacing +
refreshFramesWidth + spacing +
bgImgDropdownWidth + spacing +
iconButtonWidth
);
// Calculate total width and starting position to center everything
const totalWidth = refreshCanvasWidth + spacing + refreshFramesWidth + spacing + bgImgDropdownWidth + spacing + iconButtonWidth + spacing + dimensionsAreaWidth;
const startX = margin; // Start from left margin instead of centering
let posX = startX;
// Draw Refresh Canvas button
drawWidgetButton(
ctx,
{ size: [refreshCanvasWidth, height], pos: [posX, posY] },
"🔃 Canvas",
this.canvasButtonMouseDown
);
this.hitAreas.refreshCanvasButton.bounds = [posX, refreshCanvasWidth];
posX += refreshCanvasWidth + spacing;
// Draw Refresh Frames button
drawWidgetButton(
ctx,
{ size: [refreshFramesWidth, height], pos: [posX, posY] },
"🕞 Frames",
this.framesButtonMouseDown
);
this.hitAreas.refreshFramesButton.bounds = [posX, refreshFramesWidth];
posX += refreshFramesWidth + spacing;
// Draw bg_img control with left/right arrows
const arrowWidth = 7; // Smaller arrows
// Draw background box
ctx.fillStyle = LiteGraph.WIDGET_BGCOLOR;
ctx.fillRect(posX, posY, bgImgDropdownWidth, height);
ctx.strokeStyle = LiteGraph.WIDGET_OUTLINE_COLOR;
ctx.strokeRect(posX, posY, bgImgDropdownWidth, height);
// Draw left arrow (positioned at the very left edge)
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR;
ctx.fillText("<", posX + arrowWidth / 2, midY);
this.hitAreas.bgImgLeftArrow.bounds = [posX, arrowWidth];
// Draw text value (centered in the control)
ctx.fillText(bgImgValue, posX + bgImgDropdownWidth / 2, midY);
this.hitAreas.bgImgVal.bounds = [posX + arrowWidth, bgImgDropdownWidth - (arrowWidth * 2)];
// Draw right arrow (positioned at the very right edge)
ctx.fillText(">", posX + bgImgDropdownWidth - arrowWidth / 2, midY);
this.hitAreas.bgImgRightArrow.bounds = [posX + bgImgDropdownWidth - arrowWidth, arrowWidth];
// Combined bounds for the entire control
this.hitAreas.bgImgAny.bounds = [posX, bgImgDropdownWidth];
posX += bgImgDropdownWidth + spacing;
// Animation toggle icon for inactive flow animation (default OFF)
const isAnimOn = !!(node?.editor?._inactiveFlowEnabled ?? false);
drawWidgetButton(
ctx,
{ size: [iconButtonWidth, height], pos: [posX, posY] },
"~", // wavy line icon
isAnimOn
);
if (isAnimOn) {
const pad = 0.5;
ctx.save();
ctx.strokeStyle = '#2cc6ff';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.roundRect(posX + pad, posY + pad, iconButtonWidth - pad * 2, height - pad * 2, [4]);
ctx.stroke();
ctx.restore();
}
this.hitAreas.animToggleButton = { bounds: [posX, iconButtonWidth], onClick: (e, p, n) => {
if (n?.editor) {
n.editor._inactiveFlowEnabled = !n.editor._inactiveFlowEnabled;
try { n.editor.layerRenderer?.updateInactiveDash?.(); } catch {}
n.setDirtyCanvas(true, true);
}
return true;
}};
posX += iconButtonWidth + spacing;
// Draw rounded area for width/height controls
const roundedAreaX = posX;
const roundedAreaY = posY;
const roundedAreaWidth = dimensionsAreaWidth;
const roundedAreaHeight = height;
// Draw rounded rectangle background
ctx.fillStyle = LiteGraph.WIDGET_BGCOLOR;
ctx.strokeStyle = LiteGraph.WIDGET_OUTLINE_COLOR;
ctx.beginPath();
ctx.roundRect(roundedAreaX, roundedAreaY, roundedAreaWidth, roundedAreaHeight, [roundedAreaHeight * 0.5]);
ctx.fill();
ctx.stroke();
// Calculate positions for width and height controls within the rounded area
// Center the controls in the rounded area
const controlSpacing = 20;
const numberControlWidth = drawNumberWidgetPart.WIDTH_TOTAL; // Width of each number control
const labelWidth = 40; // Approximate width for labels
const totalControlWidth = labelWidth + numberControlWidth + controlSpacing + labelWidth + numberControlWidth;
// Center the controls in the rounded area
const controlsStartX = roundedAreaX + (roundedAreaWidth - totalControlWidth) / 2;
// Width control with label
const widthLabel = "width:";
const widthControlX = controlsStartX;
// Draw width label
ctx.textAlign = "left";
ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR;
ctx.fillText(widthLabel, widthControlX, midY);
// Draw width control (positioned after label)
const widthControlStartX = widthControlX + labelWidth;
const [wLeftArrow, wText, wRightArrow] = drawNumberWidgetPart(ctx, {
posX: widthControlStartX,
posY,
height,
value: widthValue,
direction: 1,
});
this.hitAreas.widthDec.bounds = wLeftArrow;
this.hitAreas.widthVal.bounds = wText;
this.hitAreas.widthInc.bounds = wRightArrow;
this.hitAreas.widthAny.bounds = [wLeftArrow[0], wRightArrow[0] + wRightArrow[1] - wLeftArrow[0]];
this.hitAreas.widthDec.onClick = () => this.stepWidth(node, -16);
this.hitAreas.widthInc.onClick = () => this.stepWidth(node, 16);
this.hitAreas.widthVal.onClick = () => this.promptWidth(node);
this.hitAreas.widthAny.onMove = (event) => this.dragWidth(node, event);
// Height control with label
const heightControlX = widthControlStartX + numberControlWidth + controlSpacing;
const heightLabel = "height:";
// Draw height label
ctx.fillText(heightLabel, heightControlX, midY);
// Draw height control (positioned after label)
const heightControlStartX = heightControlX + labelWidth;
const [hLeftArrow, hText, hRightArrow] = drawNumberWidgetPart(ctx, {
posX: heightControlStartX,
posY,
height,
value: heightValue,
direction: 1,
});
this.hitAreas.heightDec.bounds = hLeftArrow;
this.hitAreas.heightVal.bounds = hText;
this.hitAreas.heightInc.bounds = hRightArrow;
this.hitAreas.heightAny.bounds = [hLeftArrow[0], hRightArrow[0] + hRightArrow[1] - hLeftArrow[0]];
this.hitAreas.heightDec.onClick = () => this.stepHeight(node, -16);
this.hitAreas.heightInc.onClick = () => this.stepHeight(node, 16);
this.hitAreas.heightVal.onClick = () => this.promptHeight(node);
this.hitAreas.heightAny.onMove = (event) => this.dragHeight(node, event);
// Set up event handlers for refresh button
this.hitAreas.refreshCanvasButton.onClick = async () => {
// Update all box layer ref images from connected ref_images
if (node.updateAllBoxLayerRefs) {
try {
await node.updateAllBoxLayerRefs();
} catch (e) {
console.warn('Failed to update box layer refs:', e);
}
}
// Wait for reference image update and scale recalculation to complete
if (node.updateReferenceImageFromConnectedNode) {
try {
// This now properly awaits the image loading and scale recalculation
await node.updateReferenceImageFromConnectedNode();
// Force a final render with the correct scale
// The scale should already be correct from handleImageLoad -> recenterBackgroundImage
if (node.editor && node.editor.layerRenderer) {
node.editor.layerRenderer.render();
}
} catch (e) {
console.warn('Failed to update reference image:', e);
}
}
// Check for frames input and handle box layer keyframe scaling
if (node.handleFramesRefresh) {
node.handleFramesRefresh();
}
};
this.hitAreas.refreshCanvasButton.onDown = () => {
this.canvasButtonMouseDown = true;
node.setDirtyCanvas(true, false);
};
this.hitAreas.refreshCanvasButton.onUp = () => {
this.canvasButtonMouseDown = false;
node.setDirtyCanvas(true, false);
};
this.hitAreas.refreshFramesButton.onClick = () => {
if (node.handleFramesRefresh) {
node.handleFramesRefresh();
}
};
this.hitAreas.refreshFramesButton.onDown = () => {
this.framesButtonMouseDown = true;
node.setDirtyCanvas(true, false);
};
this.hitAreas.refreshFramesButton.onUp = () => {
this.framesButtonMouseDown = false;
node.setDirtyCanvas(true, false);
};
// Set up event handlers for bg_img control
this.hitAreas.bgImgLeftArrow.onClick = () => this.stepBgImg(node, -1);
this.hitAreas.bgImgVal.onClick = () => this.stepBgImg(node, 1);
this.hitAreas.bgImgRightArrow.onClick = () => this.stepBgImg(node, 1);
ctx.restore();
}
// Methods for width controls
stepWidth(node, step) {
const widthWidget = node.widgets?.find(w => w.name === "mask_width");
if (widthWidget) {
const newValue = widthWidget.value + step;
widthWidget.value = node.sizeManager ? node.sizeManager.constrainCanvasWidth(newValue) : Math.max(64, newValue);
if (widthWidget.callback) {
widthWidget.callback(widthWidget.value);
}
node.setDirtyCanvas(true, true);
}
}
promptWidth(node) {
if (this.haveMouseMovedValue) return;
const widthWidget = node.widgets?.find(w => w.name === "mask_width");
if (widthWidget) {
const canvas = app.canvas;
canvas.prompt("Width", widthWidget.value, (v) => {
const newValue = Number(v);
widthWidget.value = node.sizeManager ? node.sizeManager.constrainCanvasWidth(newValue) : Math.max(64, newValue);
if (widthWidget.callback) {
widthWidget.callback(widthWidget.value);
}
});
}
}
dragWidth(node, event) {
if (event.deltaX) {
this.haveMouseMovedValue = true;
const widthWidget = node.widgets?.find(w => w.name === "mask_width");
if (widthWidget) {
const newValue = widthWidget.value + event.deltaX * 2;
widthWidget.value = node.sizeManager ? node.sizeManager.constrainCanvasWidth(newValue) : Math.max(64, newValue);
if (widthWidget.callback) {
widthWidget.callback(widthWidget.value);
}
node.setDirtyCanvas(true, true);
}
}
}
// Methods for height controls
stepHeight(node, step) {
const heightWidget = node.widgets?.find(w => w.name === "mask_height");
if (heightWidget) {
const newValue = heightWidget.value + step;
heightWidget.value = node.sizeManager ? node.sizeManager.constrainCanvasHeight(newValue) : Math.max(64, newValue);
if (heightWidget.callback) {
heightWidget.callback(heightWidget.value);
}
node.setDirtyCanvas(true, true);
}
}
promptHeight(node) {
if (this.haveMouseMovedValue) return;
const heightWidget = node.widgets?.find(w => w.name === "mask_height");
if (heightWidget) {
const canvas = app.canvas;
canvas.prompt("Height", heightWidget.value, (v) => {
const newValue = Number(v);
heightWidget.value = node.sizeManager ? node.sizeManager.constrainCanvasHeight(newValue) : Math.max(64, newValue);
if (heightWidget.callback) {
heightWidget.callback(heightWidget.value);
}
});
}
}
dragHeight(node, event) {
if (event.deltaX) {
this.haveMouseMovedValue = true;
const heightWidget = node.widgets?.find(w => w.name === "mask_height");
if (heightWidget) {
const newValue = heightWidget.value + event.deltaX * 2;
heightWidget.value = node.sizeManager ? node.sizeManager.constrainCanvasHeight(newValue) : Math.max(64, newValue);
if (heightWidget.callback) {
heightWidget.callback(heightWidget.value);
}
node.setDirtyCanvas(true, true);
}
}
}
// Methods for bg_img controls
stepBgImg(node, step) {
const bgImgWidget = node.widgets?.find(w => w.name === "bg_img");
if (bgImgWidget) {
const options = ["None", "A", "B", "C"];
const currentIndex = options.indexOf(bgImgWidget.value);
const newIndex = (currentIndex + step + options.length) % options.length;
bgImgWidget.value = options[newIndex];
if (bgImgWidget.callback) {
bgImgWidget.callback(bgImgWidget.value);
}
node.setDirtyCanvas(true, true);
}
}
// Mouse event handlers
mouse(event, pos, node) {
// Use the parent's mouse handling which properly manages hit areas
return super.mouse(event, pos, node);
}
onMouseUp(event, pos, node) {
super.onMouseUp(event, pos, node);
this.haveMouseMovedValue = false;
this.canvasButtonMouseDown = false;
this.framesButtonMouseDown = false;
}
computeSize(width) {
return [width, LiteGraph.NODE_WIDGET_HEIGHT];
}
}
// === DIMENSIONS WIDGET (Simplified version for backward compatibility) ===
export class DimensionsWidget extends RgthreeBaseWidget {
constructor(name = "DimensionsWidget") {