Vendor the viewer JavaScript into javascript/ and drop the comfy-3d-viewers dependency. The viewers were previously copied into web/ at every startup by prestartup_script.py, which made the served frontend depend on a second package resolving at runtime. Bundles are vendored as .mjs so ComfyUI's **/*.js auto-import glob skips them: they are iframe-only and must never enter the shared main browser realm. Only the widget entry points stay .js. Also replace comfy_env.copy_files with a local helper. Its two extra features (frame-derived source dir, silent no-op on a missing source) were unused here, and the asset seeding now resolves the input directory through folder_paths.get_input_directory() so it follows ComfyUI Desktop and --input-directory instead of writing into the code tree. comfy-test.toml moves to the current schema: [test.workflows] cuda (there is no "gpu" key -- the accelerator is named by backend, and the old spelling parsed to an empty list, so CUDA runners silently ran the full unfiltered set), an explicit [test.platforms] platforms allowlist (per-platform booleans are no longer supported), and the javascript level. Namespace every registerExtension name under sam3. and inline the spinner styles: the stylesheet used to be injected into document.head, which is shared state every other pack inherits and nobody can clean up. It is now inline on the element with the rotation driven by the Web Animations API.
282 lines
7.8 KiB
JavaScript
282 lines
7.8 KiB
JavaScript
/**
|
|
* SAM3 Video Segmentation Dynamic Input Management
|
|
*
|
|
* This extension manages the visibility of prompt-mode-specific inputs
|
|
* based on the selected prompt_mode in SAM3VideoSegmentation node.
|
|
*/
|
|
|
|
import { app } from "../../../scripts/app.js";
|
|
|
|
const DEBUG = false;
|
|
|
|
function log(...args) {
|
|
if (DEBUG) {
|
|
console.log("[SAM3-Video]", ...args);
|
|
}
|
|
}
|
|
|
|
// Helper function to hide a widget
|
|
function hideWidget(node, widget) {
|
|
if (!widget) return;
|
|
if (widget._hidden) {
|
|
log("Widget already hidden:", widget.name);
|
|
return;
|
|
}
|
|
|
|
log("Hiding widget:", widget.name);
|
|
|
|
const index = node.widgets.indexOf(widget);
|
|
if (index === -1) {
|
|
log(" ERROR: Widget not found in node.widgets array!");
|
|
return;
|
|
}
|
|
|
|
// Store original properties for restoration
|
|
if (!widget.origType) {
|
|
widget.origType = widget.type;
|
|
widget.origComputeSize = widget.computeSize;
|
|
widget.origSerializeValue = widget.serializeValue;
|
|
}
|
|
|
|
widget._originalIndex = index;
|
|
widget._hidden = true;
|
|
|
|
// Remove widget from array
|
|
node.widgets.splice(index, 1);
|
|
|
|
log(" Widget removed at index:", index);
|
|
|
|
// Hide linked widgets if any
|
|
if (widget.linkedWidgets) {
|
|
widget.linkedWidgets.forEach(w => hideWidget(node, w));
|
|
}
|
|
}
|
|
|
|
// Helper function to show a widget
|
|
function showWidget(node, widget) {
|
|
if (!widget) return;
|
|
if (!widget._hidden) {
|
|
log("Widget already visible:", widget.name);
|
|
return;
|
|
}
|
|
|
|
log("Showing widget:", widget.name);
|
|
|
|
// Restore original properties
|
|
if (widget.origType) {
|
|
widget.type = widget.origType;
|
|
widget.computeSize = widget.origComputeSize;
|
|
if (widget.origSerializeValue) {
|
|
widget.serializeValue = widget.origSerializeValue;
|
|
}
|
|
}
|
|
|
|
// Re-insert widget at original position
|
|
const targetIndex = widget._originalIndex;
|
|
const insertIndex = Math.min(targetIndex, node.widgets.length);
|
|
node.widgets.splice(insertIndex, 0, widget);
|
|
|
|
log(" Widget restored at index:", insertIndex);
|
|
|
|
widget._hidden = false;
|
|
|
|
// Show linked widgets if any
|
|
if (widget.linkedWidgets) {
|
|
widget.linkedWidgets.forEach(w => showWidget(node, w));
|
|
}
|
|
}
|
|
|
|
// Helper function to hide an input slot (removes from node.inputs)
|
|
function hideInput(node, inputName) {
|
|
if (!node.inputs) return;
|
|
|
|
const index = node.inputs.findIndex(input => input.name === inputName);
|
|
if (index === -1) {
|
|
log(" Input not found:", inputName);
|
|
return;
|
|
}
|
|
|
|
const input = node.inputs[index];
|
|
|
|
// Already hidden
|
|
if (input._hidden) {
|
|
log(" Input already hidden:", inputName);
|
|
return;
|
|
}
|
|
|
|
log("Hiding input:", inputName, "at index:", index);
|
|
|
|
// Store for restoration
|
|
input._originalIndex = index;
|
|
input._hidden = true;
|
|
|
|
// Store in a hidden inputs map on the node
|
|
if (!node._hiddenInputs) {
|
|
node._hiddenInputs = {};
|
|
}
|
|
node._hiddenInputs[inputName] = input;
|
|
|
|
// Disconnect any existing link before removing
|
|
if (input.link !== null) {
|
|
const link = app.graph.links[input.link];
|
|
if (link) {
|
|
const originNode = app.graph.getNodeById(link.origin_id);
|
|
if (originNode) {
|
|
originNode.disconnectOutput(link.origin_slot, node);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove from inputs array
|
|
node.inputs.splice(index, 1);
|
|
|
|
log(" Input removed, remaining inputs:", node.inputs.length);
|
|
}
|
|
|
|
// Helper function to show an input slot (restores to node.inputs)
|
|
function showInput(node, inputName) {
|
|
if (!node._hiddenInputs || !node._hiddenInputs[inputName]) {
|
|
log(" Input not in hidden list:", inputName);
|
|
return;
|
|
}
|
|
|
|
const input = node._hiddenInputs[inputName];
|
|
|
|
if (!input._hidden) {
|
|
log(" Input already visible:", inputName);
|
|
return;
|
|
}
|
|
|
|
log("Showing input:", inputName);
|
|
|
|
// Find the correct position to insert
|
|
// Try to restore at original index, but clamp to valid range
|
|
const targetIndex = input._originalIndex;
|
|
const insertIndex = Math.min(targetIndex, node.inputs.length);
|
|
|
|
// Clear hidden flag
|
|
input._hidden = false;
|
|
|
|
// Re-insert into inputs array
|
|
node.inputs.splice(insertIndex, 0, input);
|
|
|
|
// Remove from hidden map
|
|
delete node._hiddenInputs[inputName];
|
|
|
|
log(" Input restored at index:", insertIndex, "total inputs:", node.inputs.length);
|
|
}
|
|
|
|
// Refresh node layout
|
|
function refreshNode(node) {
|
|
node.setDirtyCanvas(true, true);
|
|
if (app.graph) {
|
|
app.graph.setDirtyCanvas(true, true);
|
|
}
|
|
|
|
requestAnimationFrame(() => {
|
|
const newSize = node.computeSize();
|
|
node.setSize([node.size[0], newSize[1]]);
|
|
node.setDirtyCanvas(true, true);
|
|
|
|
if (app.canvas) {
|
|
app.canvas.setDirty(true, true);
|
|
}
|
|
|
|
requestAnimationFrame(() => {
|
|
if (app.canvas) {
|
|
app.canvas.draw(true, true);
|
|
}
|
|
log("Widget visibility update complete");
|
|
});
|
|
});
|
|
}
|
|
|
|
// Main extension registration
|
|
app.registerExtension({
|
|
name: "sam3.video_dynamic",
|
|
|
|
async nodeCreated(node) {
|
|
if (node.comfyClass === "SAM3VideoSegmentation") {
|
|
setTimeout(() => {
|
|
this.setupVideoSegmentation(node);
|
|
}, 100);
|
|
}
|
|
},
|
|
|
|
setupVideoSegmentation(node) {
|
|
log("Setting up SAM3VideoSegmentation node");
|
|
log("Initial inputs:", node.inputs?.map(i => i.name));
|
|
log("Initial widgets:", node.widgets?.map(w => w.name));
|
|
|
|
// Find the prompt_mode widget
|
|
const promptModeWidget = node.widgets?.find(w => w.name === "prompt_mode");
|
|
if (!promptModeWidget) {
|
|
log("ERROR: prompt_mode widget not found!");
|
|
log("Available widgets:", node.widgets?.map(w => w.name));
|
|
return;
|
|
}
|
|
|
|
log("prompt_mode widget found, current value:", promptModeWidget.value);
|
|
|
|
// Find text_prompt widget (this is a widget, not an input slot)
|
|
const textPromptWidget = node.widgets?.find(w => w.name === "text_prompt");
|
|
|
|
log("Found widgets:", {
|
|
text_prompt: !!textPromptWidget,
|
|
});
|
|
|
|
// Function to update visibility based on mode
|
|
const updateVisibility = (mode) => {
|
|
log("Updating visibility for mode:", mode);
|
|
|
|
// Text mode widget
|
|
if (textPromptWidget) {
|
|
if (mode === "text") {
|
|
showWidget(node, textPromptWidget);
|
|
} else {
|
|
hideWidget(node, textPromptWidget);
|
|
}
|
|
}
|
|
|
|
// Point mode inputs (these are input SLOTS, not widgets)
|
|
if (mode === "point") {
|
|
showInput(node, "positive_points");
|
|
showInput(node, "negative_points");
|
|
} else {
|
|
hideInput(node, "positive_points");
|
|
hideInput(node, "negative_points");
|
|
}
|
|
|
|
// Box mode inputs
|
|
if (mode === "box") {
|
|
showInput(node, "positive_boxes");
|
|
showInput(node, "negative_boxes");
|
|
} else {
|
|
hideInput(node, "positive_boxes");
|
|
hideInput(node, "negative_boxes");
|
|
}
|
|
|
|
refreshNode(node);
|
|
};
|
|
|
|
// Store original callback
|
|
const origCallback = promptModeWidget.callback;
|
|
|
|
// Override callback to update visibility when mode changes
|
|
promptModeWidget.callback = function(value) {
|
|
log("prompt_mode changed to:", value);
|
|
|
|
const result = origCallback?.apply(this, arguments);
|
|
updateVisibility(value);
|
|
|
|
return result;
|
|
};
|
|
|
|
// Initialize visibility
|
|
log("Initializing visibility...");
|
|
updateVisibility(promptModeWidget.value);
|
|
}
|
|
});
|
|
|
|
log("SAM3 Video dynamic inputs extension loaded");
|