From e3e9d786cf588a9bbdf690a1b2a2bbd68dcad7ae Mon Sep 17 00:00:00 2001 From: JJ Date: Tue, 25 Nov 2025 17:37:44 +0000 Subject: [PATCH] refactor1 --- web/power_spline_editor/canvas_top_row.js | 406 +++++++++++ web/power_spline_editor/context_menu.js | 4 +- web/power_spline_editor/general_utils.js | 58 ++ web/power_spline_editor/layer_add_buttons.js | 177 +++++ web/power_spline_editor/layer_header.js | 79 +++ .../{box_layer.js => layer_type_box.js} | 0 .../{handdraw_layer.js => layer_type_draw.js} | 0 .../{widget_utils.js => layer_type_spline.js} | 138 +--- .../layer_ui_main_widget.js | 606 ++++++++++++++++ .../power_spline_editor.js | 669 +----------------- web/power_spline_editor/spline_utils.js | 443 +----------- 11 files changed, 1361 insertions(+), 1219 deletions(-) create mode 100644 web/power_spline_editor/canvas_top_row.js create mode 100644 web/power_spline_editor/general_utils.js create mode 100644 web/power_spline_editor/layer_add_buttons.js create mode 100644 web/power_spline_editor/layer_header.js rename web/power_spline_editor/{box_layer.js => layer_type_box.js} (100%) rename web/power_spline_editor/{handdraw_layer.js => layer_type_draw.js} (100%) rename web/power_spline_editor/{widget_utils.js => layer_type_spline.js} (86%) create mode 100644 web/power_spline_editor/layer_ui_main_widget.js diff --git a/web/power_spline_editor/canvas_top_row.js b/web/power_spline_editor/canvas_top_row.js new file mode 100644 index 0000000..71d79a5 --- /dev/null +++ b/web/power_spline_editor/canvas_top_row.js @@ -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]; + } +} diff --git a/web/power_spline_editor/context_menu.js b/web/power_spline_editor/context_menu.js index ad2123e..f0cdab0 100644 --- a/web/power_spline_editor/context_menu.js +++ b/web/power_spline_editor/context_menu.js @@ -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 diff --git a/web/power_spline_editor/general_utils.js b/web/power_spline_editor/general_utils.js new file mode 100644 index 0000000..1c40d20 --- /dev/null +++ b/web/power_spline_editor/general_utils.js @@ -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); + } +} diff --git a/web/power_spline_editor/layer_add_buttons.js b/web/power_spline_editor/layer_add_buttons.js new file mode 100644 index 0000000..2c6843f --- /dev/null +++ b/web/power_spline_editor/layer_add_buttons.js @@ -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; +} diff --git a/web/power_spline_editor/layer_header.js b/web/power_spline_editor/layer_header.js new file mode 100644 index 0000000..b00bcaf --- /dev/null +++ b/web/power_spline_editor/layer_header.js @@ -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; + } +} diff --git a/web/power_spline_editor/box_layer.js b/web/power_spline_editor/layer_type_box.js similarity index 100% rename from web/power_spline_editor/box_layer.js rename to web/power_spline_editor/layer_type_box.js diff --git a/web/power_spline_editor/handdraw_layer.js b/web/power_spline_editor/layer_type_draw.js similarity index 100% rename from web/power_spline_editor/handdraw_layer.js rename to web/power_spline_editor/layer_type_draw.js diff --git a/web/power_spline_editor/widget_utils.js b/web/power_spline_editor/layer_type_spline.js similarity index 86% rename from web/power_spline_editor/widget_utils.js rename to web/power_spline_editor/layer_type_spline.js index 183cad4..d2f974c 100644 --- a/web/power_spline_editor/widget_utils.js +++ b/web/power_spline_editor/layer_type_spline.js @@ -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); - } -} diff --git a/web/power_spline_editor/layer_ui_main_widget.js b/web/power_spline_editor/layer_ui_main_widget.js new file mode 100644 index 0000000..c78aa4c --- /dev/null +++ b/web/power_spline_editor/layer_ui_main_widget.js @@ -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; +} diff --git a/web/power_spline_editor/power_spline_editor.js b/web/power_spline_editor/power_spline_editor.js index edeaf36..d76fcd3 100644 --- a/web/power_spline_editor/power_spline_editor.js +++ b/web/power_spline_editor/power_spline_editor.js @@ -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) diff --git a/web/power_spline_editor/spline_utils.js b/web/power_spline_editor/spline_utils.js index 6e43cb6..6f0027b 100644 --- a/web/power_spline_editor/spline_utils.js +++ b/web/power_spline_editor/spline_utils.js @@ -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") {