diff --git a/tests/layer_widget_identity.test.mjs b/tests/layer_widget_identity.test.mjs new file mode 100644 index 0000000..4e49c0f --- /dev/null +++ b/tests/layer_widget_identity.test.mjs @@ -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); + } +}); diff --git a/web/power_spline_editor/canvas_top_row.js b/web/power_spline_editor/canvas_top_row.js index fe7c732..a6a8d68 100644 --- a/web/power_spline_editor/canvas_top_row.js +++ b/web/power_spline_editor/canvas_top_row.js @@ -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]; } -} \ No newline at end of file +} diff --git a/web/power_spline_editor/context_menu.js b/web/power_spline_editor/context_menu.js index b918473..e26f578 100644 --- a/web/power_spline_editor/context_menu.js +++ b/web/power_spline_editor/context_menu.js @@ -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 diff --git a/web/power_spline_editor/layer_ui_main_widget.js b/web/power_spline_editor/layer_ui_main_widget.js index cdbd412..53af7fb 100644 --- a/web/power_spline_editor/layer_ui_main_widget.js +++ b/web/power_spline_editor/layer_ui_main_widget.js @@ -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; } diff --git a/web/power_spline_editor/layer_widget_identity.js b/web/power_spline_editor/layer_widget_identity.js new file mode 100644 index 0000000..52175e6 --- /dev/null +++ b/web/power_spline_editor/layer_widget_identity.js @@ -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; +} diff --git a/web/power_spline_editor/modules/dimension-manager.js b/web/power_spline_editor/modules/dimension-manager.js index fc456a6..3b120b7 100644 --- a/web/power_spline_editor/modules/dimension-manager.js +++ b/web/power_spline_editor/modules/dimension-manager.js @@ -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; } }; -} \ No newline at end of file +} diff --git a/web/power_spline_editor/power_spline_editor.js b/web/power_spline_editor/power_spline_editor.js index 88c9c28..32325d6 100644 --- a/web/power_spline_editor/power_spline_editor.js +++ b/web/power_spline_editor/power_spline_editor.js @@ -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; } } -}); \ No newline at end of file +});