Files
chrmc40 e4b970d2b6 Publish snapshot: image editor suite, Krea 2 templates, sidebar fixes
- Image editor: layers with masks/blend modes, lightweight groups, Camera Raw
  scoped to the selected layer, full-frame i2i, Copy Merged, Photoshop keyboard
  shortcuts, and a large batch of Photoshop-parity bug fixes.
- LoadImage right-click "Open in PromptChain editor" bridge (edit -> lineage
  child re-pointed onto the node).
- Krea 2 Turbo/Realism templates and editor modal integration; remove the
  krea2 Text-to-Image 3D Regional template.
- ControlNet depth + 3D-pose: wire Poser true-depth straight into Apply
  ControlNet on pose paths.
- Sidebar: drag-to-LoadImage now uploads to the input root so the widget value
  is a valid bare filename; "Copy as path" emits native Windows backslashes.
- Contributor/relicensing docs and packaging tweaks.
2026-06-28 11:58:31 -07:00

124 lines
4.7 KiB
JavaScript

// Right-click a LoadImage node -> "Open in PromptChain editor": opens the
// node's currently-loaded input image in the PromptChain editor. A saved edit
// becomes a lineage child (parent = the loaded image) and re-points THIS node
// to the edited result — a NEW input file, not an overwrite of the source.
//
// A SEPARATE extension (own registerExtension) so a JS error here can never
// take down the prompt editor / poser / region box. The menu item rides
// getExtraMenuOptions, which fires in both the legacy and Vue node renderers —
// the only trigger that works uniformly (a bare click on the on-node image is
// not interceptable in Vue mode), mirroring core's "Open in MaskEditor".
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
import { openViewer } from "./lib/viewer-bridge.js";
const TARGET_NODES = new Set(["LoadImage"]);
// The loaded file's identity. node.imgs[i].src is the authoritative
// /view?filename=&subfolder=&type= URL; the 'image' widget value is the
// fallback (it can carry a trailing " [input]" annotation, which we strip).
function readLoadedImage(node) {
const src = node.imgs?.[node.imageIndex ?? 0]?.src;
if (src) {
try {
const u = new URL(src, window.location.origin);
const filename = u.searchParams.get("filename") || "";
if (filename) {
return {
filename,
subfolder: u.searchParams.get("subfolder") || "",
type: u.searchParams.get("type") || "input",
};
}
} catch {}
}
const widget = node.widgets?.find((w) => w.name === "image");
const raw = typeof widget?.value === "string" ? widget.value : "";
const val = raw.replace(/\s*\[(input|output|temp)\]\s*$/, "");
if (!val) return null;
const parts = val.split("/");
const filename = parts.pop();
return { filename, subfolder: parts.join("/"), type: "input" };
}
function findImageWidget(node) {
return node.widgets?.find(
(w) => w.options?.image_upload === true || (w.type === "combo" && w.name === "image")
) || null;
}
// The edit is saved to the OUTPUT tree, but LoadImage reads the INPUT root.
// image-workflow?hash= stages a content-addressed promptchain_source_<hash>
// copy into input/ and hands back input_ref — the same primitive prepareEdit
// uses for the source — which is then a valid LoadImage combo value.
async function attachEditedToNode(node, entry) {
if (!entry?.hash) return;
let inputRef = "";
try {
const res = await api.fetchApi(`/promptchain/image-workflow?hash=${encodeURIComponent(entry.hash)}`);
if (res.ok) inputRef = (await res.json())?.input_ref || "";
} catch (e) {
console.error("[PromptChain] LoadImage attach-back staging failed", e);
}
if (!inputRef) return;
const widget = findImageWidget(node);
if (!widget) return;
// The full re-point: add the option, set the value, fire the combo callback
// (Comfy's installed refresh: node.imgs=undefined -> reload -> setDirtyCanvas).
// Setting .value alone does NOT reload the preview.
if (widget.options?.values && !widget.options.values.includes(inputRef)) {
widget.options.values.push(inputRef);
}
widget.value = inputRef;
widget.callback?.(inputRef);
node.setDirtyCanvas(true, true);
}
async function openLoadImageInEditor(node) {
const img = readLoadedImage(node);
if (!img?.filename) {
app.extensionManager?.toast?.add?.({
severity: "warn",
summary: "PromptChain",
detail: "This node has no loaded image to edit.",
life: 5000,
});
return;
}
// A browse-style entry (no DB hash) — identical shape to the sidebar's, so
// prepareEdit resolves the input file as the lineage parent.
const scope = img.type === "output" ? "output" : "input";
const path = img.subfolder ? `${img.subfolder}/${img.filename}` : img.filename;
const entry = {
hash: path,
filename: img.filename,
subfolder: "",
_directUrl: api.apiURL(`/promptchain/browse/preview?scope=${scope}&path=${encodeURIComponent(path)}`),
_browseScope: scope,
_browsePath: path,
};
openViewer([entry], 0, "", null, {
autoEdit: true,
onAfterEditSave: (saved) => attachEditedToNode(node, saved),
});
}
app.registerExtension({
name: "PromptChain.LoadImageEditor",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (!TARGET_NODES.has(nodeData?.name)) return;
const original = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.getExtraMenuOptions = function (canvas, options) {
const result = original?.apply(this, arguments);
const node = this;
options.push({
content: "Open in PromptChain editor",
callback: () => openLoadImageInEditor(node),
});
return result;
};
},
});