fix: preserve power spline widget compatibility

Amp-Thread-ID: https://ampcode.com/threads/T-01a0cad9-6963-772e-98a3-ab92e31eba1d
Co-authored-by: Alexander Brown <drjkl@comfy.org>
This commit is contained in:
Amp
2026-09-22 20:51:48 +00:00
co-authored by Alexander Brown
parent 1ee28ef199
commit c8cb4b040a
7 changed files with 88 additions and 123 deletions
+29
View File
@@ -0,0 +1,29 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
getLayerWidgetType,
isLayerWidget,
} from '../web/power_spline_editor/layer_widget_identity.js';
test('recognizes every persisted layer widget type after frontend adoption', () => {
for (const type of ['spline', 'handdraw', 'box_layer']) {
const adoptedWidget = { value: { type } };
assert.equal(isLayerWidget(adoptedWidget), true);
assert.equal(getLayerWidgetType(adoptedWidget), type);
}
});
test('does not classify other custom widgets as layers', () => {
for (const widget of [
{ value: { type: 'reference' } },
{ value: {} },
{ value: null },
{},
null,
]) {
assert.equal(isLayerWidget(widget), false);
assert.equal(getLayerWidgetType(widget), null);
}
});
+18 -18
View File
@@ -542,7 +542,7 @@ export async function handleFramesRefresh(node) {
export class TopRowWidget extends RgthreeBaseWidget {
constructor(name = "TopRowWidget", visibility = {}, handlers = {}) {
constructor(name = "TopRowWidget", controlsVisibility = {}, handlers = {}) {
super(name);
this.type = "custom";
this.options = { serialize: false };
@@ -550,14 +550,14 @@ export class TopRowWidget extends RgthreeBaseWidget {
this.haveMouseMovedValue = false;
this.canvasButtonMouseDown = false;
this.framesButtonMouseDown = false;
this.visibility = {
this.controlsVisibility = {
refreshCanvasButton: true,
refreshFramesButton: true,
bgOpacityControl: true,
animToggleButton: true,
widthControl: true,
heightControl: true,
...visibility,
...controlsVisibility,
};
// Store custom handlers
this.handlers = {
@@ -628,7 +628,7 @@ export class TopRowWidget extends RgthreeBaseWidget {
let posX = startX;
// Draw Refresh Canvas button
if (this.visibility.refreshCanvasButton) {
if (this.controlsVisibility.refreshCanvasButton) {
drawWidgetButton(
ctx,
{ size: [refreshCanvasWidth, height], pos: [posX, posY] },
@@ -640,7 +640,7 @@ export class TopRowWidget extends RgthreeBaseWidget {
posX += refreshCanvasWidth + spacing;
// Draw Refresh Frames button
if (this.visibility.refreshFramesButton) {
if (this.controlsVisibility.refreshFramesButton) {
// Get current max frames for display
const currentMaxFrames = node.editor?._getMaxFrames?.() ?? node.properties?.box_max_frames ?? '?';
const framesLabel = `🕞 ${currentMaxFrames}`;
@@ -657,7 +657,7 @@ export class TopRowWidget extends RgthreeBaseWidget {
// Draw bg_opacity control
const bgOpacityLabelWidth = 30;
if (this.visibility.bgOpacityControl) {
if (this.controlsVisibility.bgOpacityControl) {
// Draw background
ctx.fillStyle = LiteGraph.WIDGET_BGCOLOR;
ctx.fillRect(posX, posY, bgOpacityControlWidth, height);
@@ -678,7 +678,7 @@ export class TopRowWidget extends RgthreeBaseWidget {
height,
value: bgOpacityValue,
direction: 1,
textColor: this.visibility.bgOpacityControl ? undefined : "transparent",
textColor: this.controlsVisibility.bgOpacityControl ? undefined : "transparent",
});
assignBounds("bgOpacityDec", bgOpLeftArrow);
@@ -689,7 +689,7 @@ export class TopRowWidget extends RgthreeBaseWidget {
// Animation toggle icon
const isAnimOn = !!(node?.editor?._inactiveFlowEnabled ?? false);
if (this.visibility.animToggleButton) {
if (this.controlsVisibility.animToggleButton) {
drawWidgetButton(
ctx,
{ size: [iconButtonWidth, height], pos: [posX, posY] },
@@ -746,7 +746,7 @@ export class TopRowWidget extends RgthreeBaseWidget {
const widthLabel = "width:";
const widthControlX = controlsStartX;
if (this.visibility.widthControl) {
if (this.controlsVisibility.widthControl) {
ctx.textBaseline = "middle";
ctx.textAlign = "left";
ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR;
@@ -760,14 +760,14 @@ export class TopRowWidget extends RgthreeBaseWidget {
height,
value: widthValue,
direction: 1,
textColor: this.visibility.widthControl ? undefined : "transparent",
textColor: this.controlsVisibility.widthControl ? undefined : "transparent",
});
assignBounds("widthDec", wLeftArrow);
assignBounds("widthVal", wText);
assignBounds("widthInc", wRightArrow);
assignBounds("widthAny", [wLeftArrow[0], wRightArrow[0] + wRightArrow[1] - wLeftArrow[0]]);
if (this.visibility.widthControl) {
if (this.controlsVisibility.widthControl) {
this.hitAreas.widthDec.onClick = () => this.stepWidth(node, -16);
this.hitAreas.widthInc.onClick = () => this.stepWidth(node, 16);
this.hitAreas.widthVal.onClick = () => this.promptWidth(node);
@@ -778,7 +778,7 @@ export class TopRowWidget extends RgthreeBaseWidget {
const heightControlX = widthControlStartX + numberControlWidth + controlSpacing;
const heightLabel = "height:";
if (this.visibility.heightControl) {
if (this.controlsVisibility.heightControl) {
ctx.textBaseline = "middle";
ctx.fillText(heightLabel, heightControlX, midY);
}
@@ -790,14 +790,14 @@ export class TopRowWidget extends RgthreeBaseWidget {
height,
value: heightValue,
direction: 1,
textColor: this.visibility.heightControl ? undefined : "transparent",
textColor: this.controlsVisibility.heightControl ? undefined : "transparent",
});
assignBounds("heightDec", hLeftArrow);
assignBounds("heightVal", hText);
assignBounds("heightInc", hRightArrow);
assignBounds("heightAny", [hLeftArrow[0], hRightArrow[0] + hRightArrow[1] - hLeftArrow[0]]);
if (this.visibility.heightControl) {
if (this.controlsVisibility.heightControl) {
this.hitAreas.heightDec.onClick = () => this.stepHeight(node, -16);
this.hitAreas.heightInc.onClick = () => this.stepHeight(node, 16);
this.hitAreas.heightVal.onClick = () => this.promptHeight(node);
@@ -805,7 +805,7 @@ export class TopRowWidget extends RgthreeBaseWidget {
}
// Setup event handlers for refresh button
if (this.visibility.refreshCanvasButton) {
if (this.controlsVisibility.refreshCanvasButton) {
this.hitAreas.refreshCanvasButton.onClick = async () => {
if (this.handlers.onRefreshCanvas) {
await this.handlers.onRefreshCanvas(node);
@@ -821,7 +821,7 @@ export class TopRowWidget extends RgthreeBaseWidget {
};
}
if (this.visibility.refreshFramesButton) {
if (this.controlsVisibility.refreshFramesButton) {
this.hitAreas.refreshFramesButton.onClick = async () => {
if (this.handlers.onRefreshFrames) {
await this.handlers.onRefreshFrames(node);
@@ -838,7 +838,7 @@ export class TopRowWidget extends RgthreeBaseWidget {
}
// Setup bg_opacity control handlers
if (this.visibility.bgOpacityControl) {
if (this.controlsVisibility.bgOpacityControl) {
this.hitAreas.bgOpacityDec.onClick = () => this.stepBgOpacity(node, -5);
this.hitAreas.bgOpacityInc.onClick = () => this.stepBgOpacity(node, 5);
this.hitAreas.bgOpacityVal.onClick = () => this.promptBgOpacity(node);
@@ -1004,4 +1004,4 @@ export class TopRowWidget extends RgthreeBaseWidget {
computeSize(width) {
return [width, LiteGraph.NODE_WIDGET_HEIGHT];
}
}
}
+7 -15
View File
@@ -1,8 +1,6 @@
import { app } from '../../../scripts/app.js';
import { PowerSplineWidget } from './spline_utils.js';
import { HandDrawLayerWidget } from './layer_type_draw.js';
import { BoxLayerWidget } from './layer_type_box.js';
import { updateDrivenConfigValue, updateEasingConfigValue } from './persistence.js';
import { getLayerWidgetType, isLayerWidget } from './layer_widget_identity.js';
// Helper to create interactive number input with arrows and drag support
// For driven config (syncs to both driven and _drivenConfig)
@@ -820,9 +818,7 @@ export function getSlotInPosition(canvasX, canvasY) {
if (canvasY >= widgetTop && canvasY <= widgetBottom) {
// Found the widget at this position
const isSplineLike =
(widget instanceof PowerSplineWidget) ||
(widget instanceof HandDrawLayerWidget) ||
(widget instanceof BoxLayerWidget) ||
isLayerWidget(widget) ||
widget.name?.startsWith("spline_") ||
widget.name?.startsWith("box_");
if (isSplineLike) {
@@ -901,14 +897,12 @@ export function showCustomLayerMenu(event, widget, node, position) {
`;
const splineWidgets = node.widgets.filter(w =>
(w instanceof PowerSplineWidget) ||
(w instanceof HandDrawLayerWidget) ||
(w instanceof BoxLayerWidget) ||
isLayerWidget(w) ||
w.name?.startsWith("spline_") ||
w.name?.startsWith("box_")
);
const splineIndex = splineWidgets.indexOf(widget);
const isBoxLayerWidget = (widget instanceof BoxLayerWidget) || widget.name?.startsWith("box_");
const isBoxLayerWidget = getLayerWidgetType(widget) === 'box_layer' || widget.name?.startsWith("box_");
const canMoveUp = splineIndex > 0;
const canMoveDown = splineIndex < splineWidgets.length - 1;
@@ -1198,14 +1192,12 @@ export function getSlotMenuOptions(slot, event) {
// Check if the slot is for a spline widget (layer right-click)
const widget = slot?.widget;
const isLayerWidget = widget &&
((widget instanceof PowerSplineWidget) ||
(widget instanceof HandDrawLayerWidget) ||
(widget instanceof BoxLayerWidget) ||
const isLayer = widget &&
(isLayerWidget(widget) ||
widget.name?.startsWith("spline_") ||
widget.name?.startsWith("box_"));
if (isLayerWidget) {
if (isLayer) {
const canvas = app.canvas;
// Try multiple ways to get the screen position
+14 -41
View File
@@ -22,6 +22,7 @@ import { PowerSplineWidget } from './layer_type_spline.js';
import { HandDrawLayerWidget } from './layer_type_draw.js';
import { BoxLayerWidget } from './layer_type_box.js';
import { hasBackgroundVideo, syncVideoToFrame } from './canvas/canvas_video_background.js';
import { getLayerWidgetType as getLayerWidgetValueType, isLayerWidget } from './layer_widget_identity.js';
/**
* SplineLayerManager - Manages the lifecycle of all layer widgets
@@ -91,11 +92,7 @@ export class SplineLayerManager {
}
getSplineWidgets() {
return this.node.widgets.filter(w =>
(w instanceof PowerSplineWidget) ||
(w instanceof HandDrawLayerWidget) ||
(w instanceof BoxLayerWidget)
);
return this.node.widgets.filter(isLayerWidget);
}
addNewSpline(name) {
@@ -137,7 +134,7 @@ export class SplineLayerManager {
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) {
if (isLayerWidget(wi)) {
insertIndex = i + 1;
} else {
break;
@@ -172,7 +169,7 @@ export class SplineLayerManager {
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) {
if (isLayerWidget(wi)) {
insertIndex = i + 1;
} else {
break;
@@ -221,7 +218,7 @@ export class SplineLayerManager {
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) {
if (isLayerWidget(wi)) {
insertIndex = i + 1;
} else {
break;
@@ -351,8 +348,9 @@ export class SplineLayerManager {
this.node?.editor?.exitHanddrawMode?.(false);
} catch {}
this.node.splineWidgetsCounter++;
const isHand = sourceWidget instanceof HandDrawLayerWidget;
const isBox = sourceWidget instanceof BoxLayerWidget;
const sourceType = getLayerWidgetValueType(sourceWidget);
const isHand = sourceType === 'handdraw';
const isBox = sourceType === 'box_layer';
const WidgetClass = isHand ? HandDrawLayerWidget : (isBox ? BoxLayerWidget : PowerSplineWidget);
const widgetIdPrefix = isBox ? "box_" : "spline_";
const newWidget = new WidgetClass(widgetIdPrefix + this.node.splineWidgetsCounter);
@@ -380,7 +378,7 @@ export class SplineLayerManager {
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) {
if (isLayerWidget(wi)) {
insertIndex = i + 1;
} else {
break;
@@ -432,10 +430,7 @@ export class SplineLayerManager {
}
recreateSplinesFromData(widgets_values) {
this.node.widgets = this.node.widgets.filter(w =>
!(w instanceof PowerSplineWidget) &&
!(w instanceof HandDrawLayerWidget) &&
!(w instanceof BoxLayerWidget));
this.node.widgets = this.node.widgets.filter(w => !isLayerWidget(w));
// Create a map of widget data by name for safe, unambiguous lookup
// This prevents any cross-contamination between widget values
@@ -489,7 +484,7 @@ export class SplineLayerManager {
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) {
if (isLayerWidget(wi)) {
insertIndex = i + 1;
} else {
break;
@@ -581,20 +576,7 @@ export function getLayerWidgetClasses() {
* @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
);
}
export { isLayerWidget } from './layer_widget_identity.js';
/**
* Get the type name of a layer widget
@@ -603,15 +585,6 @@ export function isLayerWidget(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;
const type = getLayerWidgetValueType(widget);
return type === 'box_layer' ? 'box' : type;
}
@@ -0,0 +1,9 @@
const LAYER_WIDGET_TYPES = new Set(['spline', 'handdraw', 'box_layer']);
export function isLayerWidget(widget) {
return LAYER_WIDGET_TYPES.has(widget?.value?.type);
}
export function getLayerWidgetType(widget) {
return isLayerWidget(widget) ? widget.value.type : null;
}
@@ -5,6 +5,7 @@
import { safeSetSessionItem } from './image-cache.js';
import { recenterBackgroundVideo } from '../canvas/canvas_video_background.js';
import { isLayerWidget } from '../layer_widget_identity.js';
/**
* Create dimension manager instance
@@ -294,17 +295,7 @@ export function createDimensionManager(node) {
const scaleX = newWidth / oldWidth;
const scaleY = newHeight / oldHeight;
const splineWidgets = node.widgets?.filter(w => {
const PowerSplineWidget = window.PowerSplineWidget;
const HandDrawLayerWidget = window.HandDrawLayerWidget;
const BoxLayerWidget = window.BoxLayerWidget;
return (
w instanceof PowerSplineWidget ||
w instanceof HandDrawLayerWidget ||
w instanceof BoxLayerWidget
);
});
const splineWidgets = node.widgets?.filter(isLayerWidget);
if (!splineWidgets?.length) {
return;
@@ -467,4 +458,4 @@ export function createDimensionManager(node) {
return null;
}
};
}
}
+8 -37
View File
@@ -10,7 +10,6 @@ import {
create_documentation_stylesheet,
RgthreeBaseWidget,
DimensionsWidget,
PowerSplineWidget,
PowerSplineHeaderWidget,
NodeSizeManager,
drawWidgetButton,
@@ -18,11 +17,8 @@ import {
transformVideoToCanvasSpace
} from './spline_utils.js';
import { TopRowWidget, handleCanvasRefresh, handleFramesRefresh } from './canvas_top_row.js';
import {
HandDrawLayerWidget,
commitHanddrawPath
} from './layer_type_draw.js';
import { BoxLayerWidget } from './layer_type_box.js';
import { commitHanddrawPath } from './layer_type_draw.js';
import { isLayerWidget } from './layer_widget_identity.js';
import { chainCallback, hideWidgetForGood } from './general_utils.js';
import { initializeLayerUI, SplineLayerManager } from './layer_ui_main_widget.js';
import SplineEditor2 from './canvas/canvas_main.js';
@@ -405,33 +401,18 @@ app.registerExtension({
this.hasSplineWidgets = function () {
return (
this.widgets &&
this.widgets.some(
w =>
w instanceof PowerSplineWidget ||
w instanceof HandDrawLayerWidget ||
w instanceof BoxLayerWidget
)
this.widgets.some(isLayerWidget)
);
};
this.allSplinesState = function () {
const layerWidgets = this.widgets.filter(
w =>
w instanceof PowerSplineWidget ||
w instanceof HandDrawLayerWidget ||
w instanceof BoxLayerWidget
);
const layerWidgets = this.widgets.filter(isLayerWidget);
if (!layerWidgets.length) return false;
return layerWidgets.every(w => w.value.on);
};
this.toggleAllSplines = function () {
const layerWidgets = this.widgets.filter(
w =>
w instanceof PowerSplineWidget ||
w instanceof HandDrawLayerWidget ||
w instanceof BoxLayerWidget
);
const layerWidgets = this.widgets.filter(isLayerWidget);
const newState = !this.allSplinesState();
layerWidgets.forEach(w => (w.value.on = newState));
this.setDirtyCanvas(true, true);
@@ -726,12 +707,7 @@ app.registerExtension({
const scaleX = canvasWidth / oldWidth;
const scaleY = canvasHeight / oldHeight;
const splineWidgets = this.widgets?.filter(
w =>
w instanceof PowerSplineWidget ||
w instanceof HandDrawLayerWidget ||
w instanceof BoxLayerWidget
);
const splineWidgets = this.widgets?.filter(isLayerWidget);
splineWidgets?.forEach(widget => {
if (widget.value.points_store) {
@@ -1101,12 +1077,7 @@ app.registerExtension({
nodeType.prototype.onSerialize = function (o) {
const coordinatesWidget = this.widgets.find(w => w.name === 'coordinates');
if (coordinatesWidget) {
const splineWidgets = this.widgets.filter(
w =>
w instanceof PowerSplineWidget ||
w instanceof HandDrawLayerWidget ||
w instanceof BoxLayerWidget
);
const splineWidgets = this.widgets.filter(isLayerWidget);
const values = splineWidgets
.map(w => w.value)
.filter(v => v);
@@ -1120,4 +1091,4 @@ app.registerExtension({
nodeType.prototype.getSlotMenuOptions = getSlotMenuOptions;
}
}
});
});