refactor1
This commit is contained in:
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+4
-134
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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") {
|
||||
|
||||
Reference in New Issue
Block a user