diff --git a/project_loader.py b/project_loader.py index 2dc2a40..fe7dcd8 100644 --- a/project_loader.py +++ b/project_loader.py @@ -383,12 +383,63 @@ class BinaryIndexDecoder: ) +class ProjectFrameNames: + """Outputs the filename stem of each frame path field (no directory, no extension). + + Fetches start frame path, middle frame path, and end frame path from the + sequence data and returns Path(value).stem for each, so you get e.g. + 'keyframe8' instead of '/some/dir/keyframe8.png'. + """ + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "source_label": ("STRING", {"default": "", "multiline": False}), + }, + "optional": { + "manager_url": ("STRING", {"default": "http://localhost:8080", "multiline": False}), + "project_name": ("STRING", {"default": "", "multiline": False}), + "file_name": ("STRING", {"default": "", "multiline": False}), + "sequence_number": ("INT", {"default": 1, "min": 1, "max": 9999}), + }, + } + + RETURN_TYPES = ("STRING", "STRING", "STRING") + RETURN_NAMES = ("start_name", "middle_name", "end_name") + FUNCTION = "fetch_frame_names" + CATEGORY = "JSON Manager/project" + OUTPUT_NODE = False + + @classmethod + def IS_CHANGED(cls, **kwargs): + return float("nan") + + def fetch_frame_names(self, source_label, manager_url="http://localhost:8080", + project_name="", file_name="", sequence_number=1): + sequence_number = int(sequence_number) + data = _fetch_data(manager_url, project_name, file_name, sequence_number) + if data.get("error") in ("http_error", "network_error", "parse_error"): + logger.warning("ProjectFrameNames.fetch_frame_names failed: %s", data.get("message")) + return ("", "", "") + + def stem(path_str): + return Path(path_str).stem if path_str else "" + + return ( + stem(data.get("start frame path", "")), + stem(data.get("middle frame path", "")), + stem(data.get("end frame path", "")), + ) + + # --- Mappings --- PROJECT_NODE_CLASS_MAPPINGS = { "ProjectLoaderDynamic": ProjectLoaderDynamic, "ProjectSource": ProjectSource, "ProjectKey": ProjectKey, "ProjectResolution": ProjectResolution, + "ProjectFrameNames": ProjectFrameNames, "BinaryIndexDecoder": BinaryIndexDecoder, } @@ -397,5 +448,6 @@ PROJECT_NODE_DISPLAY_NAME_MAPPINGS = { "ProjectSource": "Project Source", "ProjectKey": "Project Key", "ProjectResolution": "Project Resolution", + "ProjectFrameNames": "Project Frame Names", "BinaryIndexDecoder": "Binary Index Decoder", } diff --git a/web/project_frame_names.js b/web/project_frame_names.js new file mode 100644 index 0000000..02aebd2 --- /dev/null +++ b/web/project_frame_names.js @@ -0,0 +1,131 @@ +import { app } from "../../scripts/app.js"; + +app.registerExtension({ + name: "json.manager.project.frame_names", + + async beforeQueuePrompt() { + if (!app.graph?._nodes) return; + for (const node of app.graph._nodes) { + if (node.type === "ProjectFrameNames" && node._syncFromSource) { + node._syncFromSource(); + } + } + }, + + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name !== "ProjectFrameNames") return; + + function hideWidget(widget) { + if (widget.origType === undefined) widget.origType = widget.type; + widget.type = "hidden"; + widget.hidden = true; + widget.computeSize = () => [0, -4]; + } + + function replaceWithCombo(node, name, values, callback) { + const idx = node.widgets?.findIndex(w => w.name === name); + if (idx === -1 || idx === undefined) return null; + const oldWidget = node.widgets[idx]; + const savedValue = oldWidget.value || ""; + const comboValues = values.length > 0 ? values : [""]; + if (savedValue && !comboValues.includes(savedValue)) comboValues.unshift(savedValue); + const defaultValue = savedValue || comboValues[0]; + node.widgets.splice(idx, 1); + const combo = node.addWidget("combo", name, defaultValue, callback, { values: comboValues }); + if (node.widgets.length > 1) { + node.widgets.splice(node.widgets.length - 1, 1); + node.widgets.splice(idx, 0, combo); + } + return combo; + } + + nodeType.prototype._getSourceLabels = function () { + const seen = new Set(); + const labels = []; + if (!this.graph) return labels; + for (const node of this.graph._nodes) { + if (node.type === "ProjectSource") { + const lw = node.widgets?.find(w => w.name === "label"); + if (lw?.value && !seen.has(lw.value)) { + seen.add(lw.value); + labels.push(lw.value); + } + } + } + return labels; + }; + + nodeType.prototype._findSource = function (label) { + if (!this.graph || !label) return null; + for (const node of this.graph._nodes) { + if (node.type === "ProjectSource") { + const lw = node.widgets?.find(w => w.name === "label"); + if (lw?.value === label) return node; + } + } + return null; + }; + + nodeType.prototype._syncFromSource = function () { + const srcWidget = this.widgets?.find(w => w.name === "source_label"); + const source = this._findSource(srcWidget?.value); + if (!source) return; + for (const name of ["manager_url", "project_name", "file_name", "sequence_number"]) { + const dst = this.widgets?.find(w => w.name === name); + const src = source.widgets?.find(w => w.name === name); + if (dst && src) dst.value = src.value; + } + }; + + const origOnNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + origOnNodeCreated?.apply(this, arguments); + + for (const name of ["manager_url", "project_name", "file_name", "sequence_number"]) { + const w = this.widgets?.find(w => w.name === name); + if (w) hideWidget(w); + } + + const node = this; + replaceWithCombo(this, "source_label", this._getSourceLabels?.() || [], function () { + node._syncFromSource(); + }); + + this.title = "Project Frame Names"; + this.setSize(this.computeSize()); + }; + + const origOnConfigure = nodeType.prototype.onConfigure; + nodeType.prototype.onConfigure = function (info) { + origOnConfigure?.apply(this, arguments); + + for (const name of ["manager_url", "project_name", "file_name", "sequence_number"]) { + const w = this.widgets?.find(w => w.name === name); + if (w) hideWidget(w); + } + + const srcWidget = this.widgets?.find(w => w.name === "source_label"); + if (srcWidget && srcWidget.type !== "combo") { + const node = this; + replaceWithCombo(this, "source_label", this._getSourceLabels?.() || [], function () { + node._syncFromSource(); + }); + } else if (srcWidget) { + srcWidget.options.values = this._getSourceLabels?.() || []; + } + + this.setSize(this.computeSize()); + + const node = this; + queueMicrotask(() => node._syncFromSource()); + }; + + const origOnMouseDown = nodeType.prototype.onMouseDown; + nodeType.prototype.onMouseDown = function (e, localPos, graphCanvas) { + origOnMouseDown?.apply(this, arguments); + const srcWidget = this.widgets?.find(w => w.name === "source_label"); + if (srcWidget) srcWidget.options.values = this._getSourceLabels?.() || []; + this._syncFromSource(); + }; + }, +});