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.
This commit is contained in:
chrmc40
2026-06-28 11:58:31 -07:00
parent deb1ed1eb3
commit e4b970d2b6
39 changed files with 2865 additions and 1341 deletions
+4
View File
@@ -7,6 +7,10 @@
# dev test harnesses
tests/
# GitHub repo metadata (PR template etc.) — only meaningful on GitHub, not in an
# installed pack. CONTRIBUTING.md is kept so the contributor terms ship with it.
.github/
# comfy-node-publish build artifact
node.zip
+15
View File
@@ -0,0 +1,15 @@
> **By opening this pull request, I agree to the terms in
> [CONTRIBUTING.md](../CONTRIBUTING.md)** — including the grant that lets the
> maintainer relicense my contribution (for example, under the MIT License).
<!-- The line above is the contributor agreement; please leave it in place. -->
## What does this PR do?
## Related issues
## Checklist
- [ ] I have read and agree to [CONTRIBUTING.md](../CONTRIBUTING.md).
+41
View File
@@ -0,0 +1,41 @@
# Contributing to PromptChain
Thanks for your interest in contributing! PromptChain is licensed under the
[GNU AGPL-3.0](LICENSE), and it stays that way. The terms below let the project
keep that license today while preserving the maintainer's option to relicense
later (for example, to a permissive license such as MIT). Please read them
before opening a pull request.
## Contributor terms
By submitting a contribution to this project — a pull request, patch, or any
other change — you agree that:
1. **License.** Your contribution is licensed to the project and its users
under the project's current license, the GNU AGPL-3.0.
2. **Relicensing grant.** In consideration of the project reviewing, merging,
and maintaining your contribution, you also grant the project maintainer
(mobcat40) and their successors and assigns a perpetual, worldwide,
irrevocable, non-exclusive, royalty-free, sublicensable right to use,
reproduce, modify, prepare derivative works of, publicly display and perform,
distribute, and relicense your contribution under any license — including
permissive licenses such as the MIT License — as part of this project or any
future version or derivative of it. This grant does not revoke the AGPL-3.0
license in point 1; it adds the maintainer's ability to also distribute your
contribution under other terms.
3. **Right to contribute.** The contribution is your own original work (or you
otherwise have the right to submit it), and you have the legal right to make
the contribution and to grant the rights above. If your employer has any
rights to your contribution, you confirm you have permission to submit it
under these terms (or your employer has waived those rights).
4. **No warranty.** Unless required by law or agreed in writing, you provide
your contribution on an "AS IS" basis, without warranties or conditions of
any kind, express or implied.
If you do not agree to these terms, please do not submit a contribution.
These terms apply to contributions submitted after these terms were added to
the project. Opening a pull request is taken as your agreement to them.
+4
View File
@@ -112,6 +112,10 @@ Add the **Prompt Chain** node from the node menu, and onboarding takes it from t
| Prompt Chain Background Mask (Depth) | Depth-based background mask for bokeh-preserving upscales |
| Prompt Chain OpenPose / Depth Anything / Canny / Tile / Luminance / Scribble / Soft Edge / Line Art | Bundled, license-clean ControlNet image preprocessors |
## Contributing
Contributions are welcome. By opening a pull request you agree to the terms in [CONTRIBUTING.md](CONTRIBUTING.md), which keep PromptChain on AGPL-3.0 today while preserving the maintainer's option to relicense contributions (for example, to MIT) in the future.
## License
[AGPL-3.0](LICENSE). © 2026 mobcat40. If you run a modified PromptChain as part of a hosted service, you must offer your users the modified source.
File diff suppressed because one or more lines are too long
+43
View File
@@ -0,0 +1,43 @@
# Relicensing checklist (maintainer)
PromptChain ships under [AGPL-3.0](../LICENSE). This note records what must be
true to relicense the project — or a future version — under a permissive license
such as MIT. It is a maintainer reference, not part of the contributor terms.
Two independent things must **both** hold. The contributor grant only handles
the first; the second is on you regardless of any contributor terms.
## 1. Every contributor's code is relicensable
- Outside contributions are covered by the relicensing grant in
[CONTRIBUTING.md](../CONTRIBUTING.md) **only if** they were submitted after
that file existed and the contributor accepted the terms (by opening a PR).
- **Invariant:** no outside contribution may predate the CONTRIBUTING.md commit.
History is currently 100% single-author (mobcat40), so this holds. If it ever
stops holding, get an explicit written grant from that author before
relicensing — the grant cannot reach backwards.
- The grant is a *license*, not an assignment: contributors keep copyright, and
the AGPL availability of already-published versions cannot be withdrawn. That
does not block offering *future* versions under MIT.
- Route all outside contributions through PRs so the assent trigger always
fires. A DCO ("Signed-off-by") is **not** a substitute — it certifies origin,
not a relicensing right.
- Strongest hardening, cheapest before outside PRs arrive: enable **CLA
Assistant** as a required status check, so each contributor's grant is backed
by a recorded, identity-bound signature rather than only the PR-template line.
## 2. Every bundled / vendored third-party component is MIT-compatible
The contributor grant covers only what contributors submit — it does nothing for
third-party code the project incorporates. A whole-project MIT relicense
additionally requires that **every** copyrightable component be MIT-compatible.
- Copyleft third-party code (GPL / AGPL / LGPL source) **cannot** be relicensed
to MIT. Keep it as an *external dependency* loaded at runtime, not vendored
into this tree. Vendoring a copyleft node pack's source would block a clean
MIT relicense — watch this when doing full-install / vendoring work.
- Permissive third-party code (MIT / BSD / Apache-2.0) may be included, but its
own license notices and attribution must be retained. The result is then
"MIT project + retained third-party notices," not a single bare MIT file.
- Before relicensing, inventory `bundled_packs/`, `vendor/`, and
`requirements.txt` and confirm each component's license.
+5 -2
View File
@@ -108,12 +108,15 @@ async function handleDrop(e) {
if (!fetchResponse.ok) throw new Error(`Failed to fetch: ${fetchResponse.status}`);
const blob = await fetchResponse.blob();
// upload to ComfyUI's input folder
// Upload into the input ROOT, not a subfolder. Stock LoadImage's INPUT_TYPES
// only enumerates top-level input files, so a "subfolder/name" combo value is
// never in its option list — the node loads flagged/errored even though the
// backend's VALIDATE_INPUTS (exists_annotated_filepath) still finds the file
// and the graph runs. A bare filename keeps the combo valid.
const filename = assetData.path.split("/").pop();
const file = new File([blob], filename, { type: blob.type });
const formData = new FormData();
formData.append("image", file);
formData.append("subfolder", "promptchain_assets");
formData.append("type", "input");
formData.append("overwrite", "true");
const uploadResponse = await api.fetchApi("/upload/image", { method: "POST", body: formData });
+98 -46
View File
@@ -1816,9 +1816,10 @@ function injectControlNet(pcNode, controlType) {
if (!graph) return false;
const LG = window.LiteGraph || graph.constructor?.LiteGraph;
if (!LG) return false;
// "depth-pose" is the depth branch with a Pose Studio figure as the image
// source instead of a LoadImage — the rendered figure goes THROUGH
// DepthAnything just like an uploaded photo would.
// "depth-pose" is the depth branch sourced from a 3D Poser figure instead of a
// LoadImage. The Poser renders its OWN true geometric depth, so it wires straight
// into Apply ControlNet — no DepthAnything estimator (that exists only for the
// uploaded-image branches, which have no scene geometry to read).
const usePose = controlType === "depth-pose";
const cfg = CONTROLNET_TYPES[usePose ? "depth" : controlType];
if (!cfg) return false;
@@ -1838,12 +1839,16 @@ function injectControlNet(pcNode, controlType) {
if (!posSrc?.node || !negSrc?.node) return false;
// Image source: a shared LoadImage (uploaded reference) reused across stacked
// branches — or, for "depth + 3D pose", a Pose Studio node whose rendered figure
// feeds the depth preprocessor. Pose nodes aren't shared (one figure per branch).
// branches — or, for "depth + 3D pose", a Pose Studio node whose true-depth
// render IS the control map. Pose nodes aren't shared (one figure per branch).
let loadImage = null, newLoadImage = false, poseNode = null;
if (usePose) {
poseNode = LG.createNode("PromptChain_PoseStudio");
if (!poseNode) return false;
// Switch the Poser to true geometric depth before its mount seeds the first
// capture (read live from pcrOutputMode) — the default disparity curve matches
// what the depth ControlNets were trained on.
poseNode.properties = { ...(poseNode.properties || {}), pcrOutputMode: "depth", pcrTrueDepth: true };
} else {
loadImage = graph._nodes?.find(n => n.comfyClass === "LoadImage" && n.title === "ControlNet Reference");
newLoadImage = !loadImage;
@@ -1851,12 +1856,13 @@ function injectControlNet(pcNode, controlType) {
}
const sourceNode = poseNode || loadImage;
const preNode = LG.createNode(resolvePre(cfg.pre));
// Pose-direct depth needs no preprocessor — the Poser's IMAGE already IS the map.
const preNode = usePose ? null : LG.createNode(resolvePre(cfg.pre));
const loaderNode = LG.createNode("ControlNetLoader");
const applyNode = LG.createNode("ControlNetApplyAdvanced");
const setUnion = cfg.unionType ? LG.createNode("SetUnionControlNetType") : null;
const preview = LG.createNode("PreviewImage");
if (!preNode || !loaderNode || !applyNode) return false;
if ((!usePose && !preNode) || !loaderNode || !applyNode) return false;
if (cfg.unionType && !setUnion) return false;
// Layout mirrors the hand-arranged reference (janku-depth-3): the cluster
@@ -1869,7 +1875,7 @@ function injectControlNet(pcNode, controlType) {
// Loader sits just below the preprocessor, whose height varies a lot by type
// (pose tallest, scribble shortest) — derive from the live node height so one
// rule fits all controls. SetUnion is 150 below the loader.
const preH = preNode.size?.[1] || 130;
const preH = preNode?.size?.[1] || 130;
const loaderY = preH + 65;
const setUnionY = loaderY + 150;
if (poseNode) {
@@ -1882,12 +1888,12 @@ function injectControlNet(pcNode, controlType) {
graph.add(loadImage);
loadImage.size = [250, 340];
}
preNode.pos = [baseX - 643, baseY + dy];
if (preNode) preNode.pos = [baseX - 643, baseY + dy];
loaderNode.pos = [baseX - 633, baseY + loaderY + dy];
if (setUnion) setUnion.pos = [baseX - 633, baseY + setUnionY + dy];
applyNode.pos = [baseX - 313, baseY + dy];
if (preview) preview.pos = [baseX - 933, baseY + dy];
graph.add(preNode);
if (preNode) graph.add(preNode);
graph.add(loaderNode);
if (setUnion) graph.add(setUnion);
graph.add(applyNode);
@@ -1895,7 +1901,7 @@ function injectControlNet(pcNode, controlType) {
// Sizes set after add so the litegraph size setter runs on a registered node.
// Preprocessor: fix width, keep its auto height (varies by widget count —
// canny has more rows than depth).
preNode.size = [291, preNode.size?.[1] ?? 130];
if (preNode) preNode.size = [291, preNode.size?.[1] ?? 130];
loaderNode.size = [270, 83];
if (setUnion) setUnion.size = [270, 83];
applyNode.size = [270, 240];
@@ -1917,15 +1923,22 @@ function injectControlNet(pcNode, controlType) {
else if (w.name === "start_percent") { w.value = 0.0; w.callback?.(0.0); }
else if (w.name === "end_percent") { w.value = cfg.end; w.callback?.(cfg.end); }
}
for (const w of preNode.widgets || []) {
for (const w of preNode?.widgets || []) {
if (w.name === "resolution") { w.value = 1024; w.callback?.(1024); }
// DWPose ships a toggle specifically for xinsir ControlNet stick scaling.
else if (w.name === "scale_stick_for_xinsr_cn") { w.value = "enable"; w.callback?.("enable"); }
}
sourceNode.connect(0, preNode, preNode.inputs?.findIndex(i => i.name === "image") ?? 0);
preNode.connect(0, applyNode, applyNode.inputs?.findIndex(i => i.name === "image") ?? 3);
if (preview) preNode.connect(0, preview, 0); // show the preprocessed map
const applyImageIdx = applyNode.inputs?.findIndex(i => i.name === "image") ?? 3;
if (preNode) {
sourceNode.connect(0, preNode, preNode.inputs?.findIndex(i => i.name === "image") ?? 0);
preNode.connect(0, applyNode, applyImageIdx);
if (preview) preNode.connect(0, preview, 0); // show the preprocessed map
} else {
// Pose-direct: the Poser's true-depth IMAGE is the control map itself.
sourceNode.connect(0, applyNode, applyImageIdx);
if (preview) sourceNode.connect(0, preview, 0); // the depth-map flash
}
if (setUnion) {
loaderNode.connect(0, setUnion, 0);
setUnion.connect(0, applyNode, applyNode.inputs?.findIndex(i => i.name === "control_net") ?? 2);
@@ -2344,7 +2357,11 @@ function injectFluxControlNet(pcNode, controlType) {
if (!graph) return false;
const LG = window.LiteGraph || graph.constructor?.LiteGraph;
if (!LG) return false;
const cfg = FLUX_CONTROLNET_TYPES[controlType];
// "depth-pose" = the depth branch sourced from a 3D Poser figure (true geometric
// depth) instead of a LoadImage — wired straight into Apply ControlNet.
const usePose = controlType === "depth-pose";
const baseControl = usePose ? "depth" : controlType;
const cfg = FLUX_CONTROLNET_TYPES[baseControl];
if (!cfg) return false;
const downstream = findDownstreamNodes(pcNode);
@@ -2373,47 +2390,62 @@ function injectFluxControlNet(pcNode, controlType) {
}
}
// Reuse one shared reference image across stacked branches.
let loadImage = graph._nodes?.find(n => n.comfyClass === "LoadImage" && n.title === "ControlNet Reference");
const newLoadImage = !loadImage;
if (!loadImage) loadImage = LG.createNode("LoadImage");
// Image source: a 3D Poser figure (depth + 3D pose) whose true-depth render IS
// the control map — or a shared LoadImage reference reused across stacked branches.
let loadImage = null, newLoadImage = false, poseNode = null;
if (usePose) {
poseNode = LG.createNode("PromptChain_PoseStudio");
if (!poseNode) return false;
// True geometric depth, set before its mount seeds the first capture.
poseNode.properties = { ...(poseNode.properties || {}), pcrOutputMode: "depth", pcrTrueDepth: true };
} else {
loadImage = graph._nodes?.find(n => n.comfyClass === "LoadImage" && n.title === "ControlNet Reference");
newLoadImage = !loadImage;
if (!loadImage) loadImage = LG.createNode("LoadImage");
}
const sourceNode = poseNode || loadImage;
const preNode = LG.createNode(resolvePre(cfg.pre));
// Pose-direct depth needs no preprocessor — the Poser's IMAGE already IS the map.
const preNode = usePose ? null : LG.createNode(resolvePre(cfg.pre));
const loaderNode = LG.createNode("ControlNetLoader");
const applyNode = LG.createNode("ControlNetApplyAdvanced");
const preview = LG.createNode("PreviewImage");
if (!preNode || !loaderNode || !applyNode) return false;
if ((!usePose && !preNode) || !loaderNode || !applyNode) return false;
// Same left-of-node layout as the SDXL cluster, minus the SetUnion node.
const existingApplies = downstream.filter(n => n.comfyClass === "ControlNetApplyAdvanced").length;
const baseX = pcNode.pos?.[0] ?? 0;
const baseY = pcNode.pos?.[1] ?? 0;
const dy = existingApplies * 660;
const preH = preNode.size?.[1] || 130;
const preH = preNode?.size?.[1] || 130;
const loaderY = preH + 65;
if (newLoadImage) {
if (poseNode) {
poseNode.pos = [baseX - 1383, baseY + dy];
graph.add(poseNode);
poseNode.size = [360, 470];
} else if (newLoadImage) {
loadImage.title = "ControlNet Reference";
loadImage.pos = [baseX - 1223, baseY];
graph.add(loadImage);
loadImage.size = [250, 340];
}
preNode.pos = [baseX - 643, baseY + dy];
if (preNode) preNode.pos = [baseX - 643, baseY + dy];
loaderNode.pos = [baseX - 633, baseY + loaderY + dy];
applyNode.pos = [baseX - 313, baseY + dy];
if (preview) preview.pos = [baseX - 933, baseY + dy];
graph.add(preNode);
if (preNode) graph.add(preNode);
graph.add(loaderNode);
graph.add(applyNode);
if (preview) graph.add(preview);
preNode.size = [291, preNode.size?.[1] ?? 130];
if (preNode) preNode.size = [291, preNode.size?.[1] ?? 130];
loaderNode.size = [270, 83];
applyNode.size = [270, 240];
if (preview) preview.size = [250, 360];
// Flux branches carry a "flux-" prefixed control type so their settings
// sections + recommended markers stay distinct from the SDXL defaults.
const settingsType = `flux-${controlType}`;
applyNode.title = `ControlNet — ${CN_LABELS[settingsType] || controlType}`;
const settingsType = `flux-${baseControl}`;
applyNode.title = `ControlNet — ${usePose ? "Depth (pose)" : (CN_LABELS[settingsType] || controlType)}`;
applyNode.properties = { ...(applyNode.properties || {}), pcrControlType: settingsType };
setControlNetModel(loaderNode, "fluxunion");
@@ -2422,13 +2454,20 @@ function injectFluxControlNet(pcNode, controlType) {
else if (w.name === "start_percent") { w.value = 0.0; w.callback?.(0.0); }
else if (w.name === "end_percent") { w.value = cfg.end; w.callback?.(cfg.end); }
}
for (const w of preNode.widgets || []) {
for (const w of preNode?.widgets || []) {
if (w.name === "resolution") { w.value = 1024; w.callback?.(1024); }
}
loadImage.connect(0, preNode, preNode.inputs?.findIndex(i => i.name === "image") ?? 0);
preNode.connect(0, applyNode, applyNode.inputs?.findIndex(i => i.name === "image") ?? 3);
if (preview) preNode.connect(0, preview, 0);
const applyImageIdx = applyNode.inputs?.findIndex(i => i.name === "image") ?? 3;
if (preNode) {
sourceNode.connect(0, preNode, preNode.inputs?.findIndex(i => i.name === "image") ?? 0);
preNode.connect(0, applyNode, applyImageIdx);
if (preview) preNode.connect(0, preview, 0);
} else {
// Pose-direct: the Poser's true-depth IMAGE is the control map itself.
sourceNode.connect(0, applyNode, applyImageIdx);
if (preview) sourceNode.connect(0, preview, 0);
}
loaderNode.connect(0, applyNode, applyNode.inputs?.findIndex(i => i.name === "control_net") ?? 2);
if (vaeSrc?.node) {
const vaeIdx = applyNode.inputs?.findIndex(i => i.name === "vae") ?? -1;
@@ -2452,9 +2491,10 @@ function injectZImageControlNet(pcNode, controlType, isBase = false) {
if (!graph) return false;
const LG = window.LiteGraph || graph.constructor?.LiteGraph;
if (!LG) return false;
// "depth-pose" = the depth branch driven by a 3D Poser figure instead of a
// LoadImage — the rendered figure goes THROUGH the depth preprocessor, and its
// MASKS/POSE outputs are what a follow-up Regional inject binds to.
// "depth-pose" = the depth branch sourced from a 3D Poser figure instead of a
// LoadImage. The Poser emits its own true geometric depth, so it wires straight
// into ZImageFunControlnet (no DepthAnything); its MASKS/POSE outputs are what a
// follow-up Regional inject binds to.
const usePose = controlType === "depth-pose";
const baseControl = usePose ? "depth" : controlType;
const cfg = ZIMAGE_CONTROLNET_TYPES[baseControl];
@@ -2487,6 +2527,9 @@ function injectZImageControlNet(pcNode, controlType, isBase = false) {
if (usePose) {
poseNode = LG.createNode("PromptChain_PoseStudio");
if (!poseNode) return false;
// Poser emits its own true geometric depth → straight into ZImageFunControlnet,
// no DepthAnything re-estimation. Set before its mount seeds the first capture.
poseNode.properties = { ...(poseNode.properties || {}), pcrOutputMode: "depth", pcrTrueDepth: true };
} else {
loadImage = graph._nodes?.find(n => n.comfyClass === "LoadImage" && n.title === "ControlNet Reference");
newLoadImage = !loadImage;
@@ -2494,17 +2537,18 @@ function injectZImageControlNet(pcNode, controlType, isBase = false) {
}
const sourceNode = poseNode || loadImage;
const preNode = LG.createNode(resolvePre(cfg.pre));
// Pose-direct depth needs no preprocessor — the Poser's IMAGE already IS the map.
const preNode = usePose ? null : LG.createNode(resolvePre(cfg.pre));
const patchLoader = LG.createNode("ModelPatchLoader");
const zcnNode = LG.createNode("ZImageFunControlnet");
const preview = LG.createNode("PreviewImage");
if (!preNode || !patchLoader || !zcnNode) return false;
if ((!usePose && !preNode) || !patchLoader || !zcnNode) return false;
const existing = downstream.filter(n => n.comfyClass === "ZImageFunControlnet").length;
const baseX = pcNode.pos?.[0] ?? 0;
const baseY = pcNode.pos?.[1] ?? 0;
const dy = existing * 660;
const preH = preNode.size?.[1] || 130;
const preH = preNode?.size?.[1] || 130;
const loaderY = preH + 65;
if (poseNode) {
poseNode.pos = [baseX - 1383, baseY + dy];
@@ -2516,15 +2560,15 @@ function injectZImageControlNet(pcNode, controlType, isBase = false) {
graph.add(loadImage);
loadImage.size = [250, 340];
}
preNode.pos = [baseX - 643, baseY + dy];
if (preNode) preNode.pos = [baseX - 643, baseY + dy];
patchLoader.pos = [baseX - 633, baseY + loaderY + dy];
zcnNode.pos = [baseX - 313, baseY + dy];
if (preview) preview.pos = [baseX - 933, baseY + dy];
graph.add(preNode);
if (preNode) graph.add(preNode);
graph.add(patchLoader);
graph.add(zcnNode);
if (preview) graph.add(preview);
preNode.size = [291, preNode.size?.[1] ?? 130];
if (preNode) preNode.size = [291, preNode.size?.[1] ?? 130];
patchLoader.size = [270, 58];
zcnNode.size = [270, 130];
if (preview) preview.size = [250, 360];
@@ -2549,13 +2593,20 @@ function injectZImageControlNet(pcNode, controlType, isBase = false) {
for (const w of zcnNode.widgets || []) {
if (w.name === "strength") { w.value = cfg.strength; w.callback?.(cfg.strength); }
}
for (const w of preNode.widgets || []) {
for (const w of preNode?.widgets || []) {
if (w.name === "resolution") { w.value = 1024; w.callback?.(1024); }
}
sourceNode.connect(0, preNode, preNode.inputs?.findIndex(i => i.name === "image") ?? 0);
preNode.connect(0, zcnNode, zcnNode.inputs?.findIndex(i => i.name === "image") ?? -1);
if (preview) preNode.connect(0, preview, 0);
const zcnImageIdx = zcnNode.inputs?.findIndex(i => i.name === "image") ?? -1;
if (preNode) {
sourceNode.connect(0, preNode, preNode.inputs?.findIndex(i => i.name === "image") ?? 0);
preNode.connect(0, zcnNode, zcnImageIdx);
if (preview) preNode.connect(0, preview, 0);
} else {
// Pose-direct: the Poser's true-depth IMAGE feeds the Z-Image control patch.
sourceNode.connect(0, zcnNode, zcnImageIdx);
if (preview) sourceNode.connect(0, preview, 0);
}
patchLoader.connect(0, zcnNode, zcnNode.inputs?.findIndex(i => i.name === "model_patch") ?? 1);
if (vaeSrc?.node) vaeSrc.node.connect(vaeSrc.slot, zcnNode, zcnNode.inputs?.findIndex(i => i.name === "vae") ?? 2);
// Model line: source → ZImageFunControlnet → sampler.
@@ -3049,6 +3100,7 @@ export async function showModelModal(pcNode, modelInfo, anchorEl, { tab = null }
label: "ControlNet",
children: [
{ label: "Depth", value: "FluxControlNet:depth" },
{ label: "Depth + 3D pose", value: "FluxControlNet:depth-pose" },
{ label: "Canny", value: "FluxControlNet:canny" },
{ label: "Pose", value: "FluxControlNet:pose" },
{ label: "Soft Edge", value: "FluxControlNet:softedge" },
@@ -1,5 +1,5 @@
import { d as delegate, p as push, a as prop, u as user_effect, D as comment, v as first_child, i as if_block, k as append, l as pop, g as get, s as state, f as sibling, o as bind_this, e as set, t as template_effect, y as set_text, j as delegated, A as from_html, n as child } from "./disclose-version-uq4tn5Y6.js";
import { a as action } from "./actions-WPfqiWYB.js";
import { d as delegate, p as push, a as prop, u as user_effect, D as comment, v as first_child, i as if_block, k as append, l as pop, g as get, s as state, f as sibling, o as bind_this, e as set, t as template_effect, y as set_text, j as delegated, A as from_html, n as child } from "./disclose-version-et9wt-4m.js";
import { a as action } from "./actions-zjmT0mOr.js";
function portal(node) {
document.body.appendChild(node);
return { destroy() {
@@ -87,4 +87,4 @@ export {
ConfirmModal as C,
portal as p
};
//# sourceMappingURL=ConfirmModal-BGcVYndw.js.map
//# sourceMappingURL=ConfirmModal-CtR7InHn.js.map
@@ -1,9 +1,9 @@
import { d as delegate, p as push, a as prop, u as user_effect, i as if_block, g as get, o as bind_this, t as template_effect, k as append, l as pop, m as user_derived, n as child, v as first_child, f as sibling, j as delegated, r as event, A as from_html, s as state, y as set_text, w as each, h as set_class, z as index, e as set, c as proxy, C as tick, x as set_attribute, D as comment } from "./disclose-version-uq4tn5Y6.js";
import { s as set_style } from "./style-Boi27oOu.js";
import { d as delegate, p as push, a as prop, u as user_effect, i as if_block, g as get, o as bind_this, t as template_effect, k as append, l as pop, m as user_derived, n as child, v as first_child, f as sibling, j as delegated, r as event, A as from_html, s as state, y as set_text, w as each, h as set_class, z as index, e as set, c as proxy, C as tick, x as set_attribute, D as comment } from "./disclose-version-et9wt-4m.js";
import { s as set_style } from "./style-B3hsaAru.js";
import { app } from "/scripts/app.js";
import { j as justifiedLayout } from "./justified-layout-cyVM7i96.js";
import { a as action } from "./actions-WPfqiWYB.js";
import { p as portal, C as ConfirmModal } from "./ConfirmModal-BGcVYndw.js";
import { a as action } from "./actions-zjmT0mOr.js";
import { p as portal, C as ConfirmModal } from "./ConfirmModal-CtR7InHn.js";
const GALLERY_ROW_HEIGHT_MIN = 40;
const GALLERY_ROW_HEIGHT_MAX = 400;
const ZOOM_FACTOR = 1.15;
@@ -801,4 +801,4 @@ export {
clampGalleryRowHeight as c,
zoomGalleryRowHeight as z
};
//# sourceMappingURL=GeneratedGallery-D5XbkMk7.js.map
//# sourceMappingURL=GeneratedGallery-DgtlJz3l.js.map
@@ -1,10 +1,10 @@
import { d as delegate, p as push, a as prop, u as user_effect, g as get, e as set, v as first_child, i as if_block, f as sibling, k as append, l as pop, s as state, m as user_derived, o as bind_this, j as delegated, n as child, D as comment, w as each, z as index, A as from_html, t as template_effect, h as set_class, r as event, y as set_text, x as set_attribute } from "./disclose-version-uq4tn5Y6.js";
import { o as onDestroy } from "./index-client-iMRCrpBY.js";
import { h as html, P as PopupAnchor } from "./PopupAnchor-C6OWGGTI.js";
import { a as action } from "./actions-WPfqiWYB.js";
import { s as set_style } from "./style-Boi27oOu.js";
import { d as delegate, p as push, a as prop, u as user_effect, g as get, e as set, v as first_child, i as if_block, f as sibling, k as append, l as pop, s as state, m as user_derived, o as bind_this, j as delegated, n as child, D as comment, w as each, z as index, A as from_html, t as template_effect, h as set_class, r as event, y as set_text, x as set_attribute } from "./disclose-version-et9wt-4m.js";
import { o as onDestroy } from "./index-client-6amB1qrM.js";
import { h as html, P as PopupAnchor } from "./PopupAnchor-D5Lvfjom.js";
import { a as action } from "./actions-zjmT0mOr.js";
import { s as set_style } from "./style-B3hsaAru.js";
import { app } from "/scripts/app.js";
import { b as bind_value } from "./input-DFQhebEz.js";
import { b as bind_value } from "./input-B9kD0bWJ.js";
var root_1$2 = from_html(`<div class="pcr-mode-menu-search-container"><input type="text" class="pcr-mode-menu-search" placeholder="Search options..."/></div> <div class="pcr-mode-menu-separator"></div>`, 1);
var root_2$2 = from_html(`<div class="pcr-mode-menu-empty">No matching options</div>`);
var root_5$1 = from_html(`<span class="pcr-mode-menu-check"></span>`);
@@ -606,4 +606,4 @@ export {
Menubar as M,
SearchableList as S
};
//# sourceMappingURL=Menubar-DC12j8gE.js.map
//# sourceMappingURL=Menubar-CMm15KuK.js.map
@@ -1,5 +1,5 @@
import { t as template_effect, S as active_effect, T as assign_nodes, U as get_first_child, V as remove_effect_dom, W as create_element, X as NAMESPACE_SVG, Y as NAMESPACE_MATHML, b as block, Z as EFFECT_TRANSPARENT, B as BranchManager, c as proxy, d as delegate, p as push, a as prop, u as user_effect, o as bind_this, j as delegated, k as append, l as pop, A as from_html, n as child } from "./disclose-version-uq4tn5Y6.js";
import { a as onMount, o as onDestroy } from "./index-client-iMRCrpBY.js";
import { t as template_effect, S as active_effect, T as assign_nodes, U as get_first_child, V as remove_effect_dom, W as create_element, X as NAMESPACE_SVG, Y as NAMESPACE_MATHML, b as block, Z as EFFECT_TRANSPARENT, B as BranchManager, c as proxy, d as delegate, p as push, a as prop, u as user_effect, o as bind_this, j as delegated, k as append, l as pop, A as from_html, n as child } from "./disclose-version-et9wt-4m.js";
import { a as onMount, o as onDestroy } from "./index-client-6amB1qrM.js";
function html(node, get_value, is_controlled = false, svg = false, mathml = false, skip_warning = false) {
var anchor = node;
var value = "";
@@ -155,4 +155,4 @@ export {
PopupAnchor as P,
html as h
};
//# sourceMappingURL=PopupAnchor-C6OWGGTI.js.map
//# sourceMappingURL=PopupAnchor-D5Lvfjom.js.map
@@ -1,5 +1,5 @@
import { d as delegate, p as push, a as prop, s as state, c as proxy, u as user_effect, e as set, i as if_block, g as get, f as sibling, t as template_effect, x as set_attribute, q as set_value, j as delegated, k as append, l as pop, m as user_derived, n as child, o as bind_this, w as each, h as set_class, y as set_text, z as index, A as from_html } from "./disclose-version-uq4tn5Y6.js";
import { s as set_style } from "./style-Boi27oOu.js";
import { d as delegate, p as push, a as prop, s as state, c as proxy, u as user_effect, e as set, i as if_block, g as get, f as sibling, t as template_effect, x as set_attribute, q as set_value, j as delegated, k as append, l as pop, m as user_derived, n as child, o as bind_this, w as each, h as set_class, y as set_text, z as index, A as from_html } from "./disclose-version-et9wt-4m.js";
import { s as set_style } from "./style-B3hsaAru.js";
var root_2 = from_html(`<div class="pcr-slider-zone svelte-nuvs0d"></div>`);
var root_3 = from_html(`<div class="pcr-slider-marker svelte-nuvs0d"></div>`);
var root_4 = from_html(`<div><span class="pcr-slider-tick-label svelte-nuvs0d"> </span></div>`);
@@ -267,4 +267,4 @@ delegate([
export {
SettingsSlider as S
};
//# sourceMappingURL=SettingsSlider-CKF_XmgB.js.map
//# sourceMappingURL=SettingsSlider-C9Ps16J6.js.map
@@ -1,4 +1,4 @@
import { a4 as effect, E as untrack, a9 as render_effect, aa as deep_read_state, ab as safe_not_equal } from "./disclose-version-uq4tn5Y6.js";
import { a4 as effect, E as untrack, a9 as render_effect, aa as deep_read_state, ab as safe_not_equal } from "./disclose-version-et9wt-4m.js";
function action(dom, action2, get_value) {
effect(() => {
var payload = untrack(() => action2(dom, get_value == null ? void 0 : get_value()) || {});
@@ -29,4 +29,4 @@ function action(dom, action2, get_value) {
export {
action as a
};
//# sourceMappingURL=actions-WPfqiWYB.js.map
//# sourceMappingURL=actions-zjmT0mOr.js.map
@@ -1,4 +1,4 @@
import { P as setContext, O as getContext } from "./disclose-version-uq4tn5Y6.js";
import { P as setContext, O as getContext } from "./disclose-version-et9wt-4m.js";
const API_KEY = "pcr-api";
function provideApi(api) {
setContext(API_KEY, api);
@@ -41,4 +41,4 @@ export {
safeJson as s,
useApi as u
};
//# sourceMappingURL=api-context-BFKo1mCD.js.map
//# sourceMappingURL=api-context-BNqvELYR.js.map
+32 -2
View File
@@ -902,6 +902,7 @@
display: flex; align-items: center; gap: 8px;
padding: 5px 7px; border-radius: 4px; cursor: pointer;
background: #1c1c1c; border: 1px solid #2e2e2e;
-webkit-user-select: none; user-select: none; /* the name text must not hijack a press-drag */
}
.pcr-ed-layer-row.svelte-1ozzqkm:hover { border-color: #3d3d3d; }
.pcr-ed-layer-row.selected.svelte-1ozzqkm { background: rgba(200, 89, 9, 0.10); border-color: #5a4630; }
@@ -919,6 +920,20 @@
}
.pcr-ed-layer-menu.svelte-1ozzqkm button:where(.svelte-1ozzqkm):hover { background: #c85909; color: #fff; }
.pcr-ed-layer-menu.svelte-1ozzqkm button.danger:where(.svelte-1ozzqkm):hover { background: #b23b3b; }
.pcr-ed-menu-sep.svelte-1ozzqkm { height: 1px; margin: 4px 6px; background: #3a3a3a; }
/* Mask actions collapse into a hover flyout (CSS-only, no timers). The flyout is
a DOM descendant of .pcr-ed-layer-menu so the outside-click dismiss still works. */
.pcr-ed-submenu-host.svelte-1ozzqkm { position: relative; display: flex; flex-direction: column; }
.pcr-ed-submenu-trigger.svelte-1ozzqkm { display: flex; align-items: center; justify-content: space-between; }
.pcr-ed-submenu-caret.svelte-1ozzqkm { color: #888; font-size: 11px; margin-left: 8px; }
.pcr-ed-submenu.svelte-1ozzqkm {
position: absolute; left: 100%; top: -5px; min-width: 160px;
background: #232323; border: 1px solid #444; border-radius: 6px; padding: 4px;
box-shadow: 0 6px 22px rgba(0, 0, 0, 0.5); display: none; flex-direction: column; z-index: 1;
}
.pcr-ed-submenu-host.flip.svelte-1ozzqkm .pcr-ed-submenu:where(.svelte-1ozzqkm) { left: auto; right: 100%; }
.pcr-ed-submenu-host.svelte-1ozzqkm:hover .pcr-ed-submenu:where(.svelte-1ozzqkm), .pcr-ed-submenu.svelte-1ozzqkm:hover { display: flex; }
.pcr-ed-submenu-host.svelte-1ozzqkm:hover .pcr-ed-submenu-trigger:where(.svelte-1ozzqkm) { background: #c85909; color: #fff; }
/* (unused) .pcr-ed-layer-bg { cursor: default; opacity: 0.85; }*/
.pcr-ed-layer-actions.svelte-1ozzqkm { display: flex; gap: 4px; }
.pcr-ed-layer-btn.svelte-1ozzqkm {
@@ -928,8 +943,8 @@
.pcr-ed-layer-btn.svelte-1ozzqkm:hover:not(:disabled) { color: #fff; border-color: #555; }
.pcr-ed-layer-btn.svelte-1ozzqkm:disabled { opacity: 0.4; cursor: default; }
.pcr-ed-layer-grip.svelte-1ozzqkm {
flex: none; width: 12px; text-align: center;
font-size: 11px; line-height: 1; color: #555;
flex: none; width: 16px; text-align: center;
font-size: 12px; line-height: 1; color: #666;
cursor: grab; user-select: none;
}
.pcr-ed-layer-grip.svelte-1ozzqkm:hover { color: #aaa; }
@@ -940,11 +955,19 @@
user-select: none; color: #ccc;
}
.pcr-ed-eye.off.svelte-1ozzqkm { color: #666; }
/* Photoshop-style layer-mask thumbnail (shows the mask shape; white glow = active paint target). */
.pcr-ed-mask-thumb.svelte-1ozzqkm {
flex: none; width: 18px; height: 18px; border-radius: 3px;
object-fit: cover; background: #000; border: 1px solid #555; cursor: pointer;
}
.pcr-ed-mask-thumb.svelte-1ozzqkm:hover { border-color: #888; }
.pcr-ed-mask-thumb.editing.svelte-1ozzqkm { border-color: #fff; box-shadow: 0 0 0 1px #fff, 0 0 0 3px rgba(255, 255, 255, 0.30); }
.pcr-ed-layer-name.svelte-1ozzqkm { flex: 1; font-size: 12px; color: #ddd; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pcr-ed-layer-blendtag.svelte-1ozzqkm { margin-left: 5px; font-size: 10px; color: #8b96a5; }
.pcr-ed-layer-rename.svelte-1ozzqkm {
flex: 1; min-width: 0; font-size: 12px; color: #eee;
background: #1b1b1b; border: 1px solid #c85909; border-radius: 3px; padding: 1px 4px;
-webkit-user-select: text; user-select: text; /* stays editable under the row's user-select:none */
}
.pcr-ed-blend-select.svelte-1ozzqkm {
width: 100%; margin: 0 0 4px; padding: 3px 6px; font-size: 12px;
@@ -952,6 +975,13 @@
}
.pcr-ed-blend-select.svelte-1ozzqkm:disabled { opacity: 0.45; }
.pcr-ed-layer-opacity.svelte-1ozzqkm { width: 70px; accent-color: #c85909; }
.pcr-ed-group-row.svelte-1ozzqkm { display: flex; align-items: center; gap: 4px; padding: 3px 5px; border: 1px solid #333; border-radius: 4px;
background: rgba(120, 130, 150, 0.12); -webkit-user-select: none; user-select: none; }
.pcr-ed-group-row.svelte-1ozzqkm .pcr-ed-eye:where(.svelte-1ozzqkm) { cursor: pointer; }
.pcr-ed-group-name.svelte-1ozzqkm { flex: 1; font-size: 12px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: default; }
.pcr-ed-group-x.svelte-1ozzqkm { background: none; border: none; color: #999; cursor: pointer; font-size: 13px; padding: 0 2px; }
.pcr-ed-group-x.svelte-1ozzqkm:hover { color: #fff; }
.pcr-ed-layer-row.in-group.svelte-1ozzqkm { margin-left: 14px; }
.pcr-ed-hist.svelte-1ozzqkm {
display: flex; flex-direction: column;
max-height: 170px; overflow-y: auto;
@@ -4282,9 +4282,9 @@ export {
untrack as E,
update as F,
set_checked as G,
autofocus as H,
to_array as I,
from_svg as J,
to_array as H,
from_svg as I,
autofocus as J,
noop as K,
mount as L,
unmount as M,
@@ -4343,4 +4343,4 @@ export {
set_text as y,
index as z
};
//# sourceMappingURL=disclose-version-uq4tn5Y6.js.map
//# sourceMappingURL=disclose-version-et9wt-4m.js.map
@@ -1,4 +1,4 @@
import { _ as component_context, a0 as lifecycle_outside_component, u as user_effect, E as untrack } from "./disclose-version-uq4tn5Y6.js";
import { _ as component_context, a0 as lifecycle_outside_component, u as user_effect, E as untrack } from "./disclose-version-et9wt-4m.js";
function onMount(fn) {
if (component_context === null) {
lifecycle_outside_component();
@@ -23,4 +23,4 @@ export {
onMount as a,
onDestroy as o
};
//# sourceMappingURL=index-client-iMRCrpBY.js.map
//# sourceMappingURL=index-client-6amB1qrM.js.map
@@ -1,4 +1,4 @@
import { a2 as listen_to_event_and_reset_event, a3 as current_batch, C as tick, E as untrack, a9 as render_effect, a7 as is, a8 as teardown, ad as queue_micro_task } from "./disclose-version-uq4tn5Y6.js";
import { a2 as listen_to_event_and_reset_event, a3 as current_batch, C as tick, E as untrack, a9 as render_effect, a7 as is, a8 as teardown, ad as queue_micro_task } from "./disclose-version-et9wt-4m.js";
function bind_value(input, get, set = get) {
var batches = /* @__PURE__ */ new WeakSet();
listen_to_event_and_reset_event(input, "input", async (is_reset) => {
@@ -150,4 +150,4 @@ export {
bind_value as b,
bind_group as c
};
//# sourceMappingURL=input-DFQhebEz.js.map
//# sourceMappingURL=input-B9kD0bWJ.js.map
+5 -5
View File
@@ -5,11 +5,11 @@ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
var _activeNodeId, _treeRoots, _openTabs, _activeWildcardTab, _compiledOutput, _compiledNegOutput, _imageUrl, _previewUrl, _progress, _isGenerating, _sidebarView, _fontSize, _renamingNodeId;
import { d as delegate, p as push, a as prop, i as if_block, f as sibling, t as template_effect, e as set, g as get, j as delegated, k as append, l as pop, s as state, n as child, A as from_html, x as set_attribute, u as user_effect, h as set_class, r as event, m as user_derived, R as text, y as set_text, o as bind_this, w as each, z as index, D as comment, v as first_child, c as proxy, M as unmount, L as mount } from "./disclose-version-uq4tn5Y6.js";
import { a as onMount, o as onDestroy } from "./index-client-iMRCrpBY.js";
import { s as set_style } from "./style-Boi27oOu.js";
import { b as bind_value } from "./input-DFQhebEz.js";
import { M as Menubar, S as SearchableList } from "./Menubar-DC12j8gE.js";
import { d as delegate, p as push, a as prop, i as if_block, f as sibling, t as template_effect, e as set, g as get, j as delegated, k as append, l as pop, s as state, n as child, A as from_html, x as set_attribute, u as user_effect, h as set_class, r as event, m as user_derived, R as text, y as set_text, o as bind_this, w as each, z as index, D as comment, v as first_child, c as proxy, M as unmount, L as mount } from "./disclose-version-et9wt-4m.js";
import { a as onMount, o as onDestroy } from "./index-client-6amB1qrM.js";
import { s as set_style } from "./style-B3hsaAru.js";
import { b as bind_value } from "./input-B9kD0bWJ.js";
import { M as Menubar, S as SearchableList } from "./Menubar-CMm15KuK.js";
var root_1$4 = from_html(`<img class="pcr-fs-topbar-logo svelte-93wsqh" alt="PromptChain"/>`);
var root$5 = from_html(`<div class="pcr-fs-topbar svelte-93wsqh"><div class="pcr-fs-topbar-left svelte-93wsqh"><!> <span class="pcr-fs-topbar-title svelte-93wsqh"></span></div> <div class="pcr-fs-topbar-right svelte-93wsqh"><div class="pcr-fs-topbar-actions svelte-93wsqh"><input type="number" class="pcr-fs-batch-input svelte-93wsqh" min="1" max="99" title="Batch count"/> <span class="pcr-fs-run-btn svelte-93wsqh" title="Queue prompt (Ctrl+Enter)"><svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"></path></svg> <span>Run</span></span> <span class="pcr-fs-cancel-btn pcr-fs-inactive svelte-93wsqh" title="Cancel execution"><svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path></svg></span> <span class="pcr-fs-queue-badge pcr-fs-inactive svelte-93wsqh">0 active</span></div> <div class="pcr-fs-panel-toggles svelte-93wsqh"><span class="pcr-fs-header-btn svelte-93wsqh" title="Toggle 3D Poser panel" data-fs-action="pose"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M20.5 6c-2.61.7-5.67 1-8.5 1s-5.89-.3-8.5-1L3 8c1.86.5 4 .83 6 1v13h2v-6h2v6h2V9c2-.17 4.14-.5 6-1l-.5-2zM12 6c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"></path></svg></span> <span class="pcr-fs-header-btn svelte-93wsqh" title="Toggle output panel" data-fs-action="output"><svg width="16" height="16" viewBox="0 -960 960 960" fill="currentColor"><path d="M400-280h160v-80H400v80Zm0-160h280v-80H400v80ZM280-600h400v-80H280v80Zm200 120ZM80-80v-80h102q-48-23-77.5-68T75-330q0-79 55.5-134.5T265-520v80q-45 0-77.5 32T155-330q0 39 24 69t61 38v-97h80v240H80Zm320-40v-80h360v-560H200v160h-80v-160q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H400Z"></path></svg></span> <span class="pcr-fs-header-btn svelte-93wsqh" title="Toggle image preview" data-fs-action="image"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"></path></svg></span></div> <span class="pcr-fs-close-btn svelte-93wsqh" title="Close (Escape)"></span></div></div>`);
function Topbar($$anchor, $$props) {
+2 -2
View File
@@ -1,5 +1,5 @@
import { M as unmount, L as mount } from "./disclose-version-uq4tn5Y6.js";
import { G as GeneratedGallery } from "./GeneratedGallery-D5XbkMk7.js";
import { M as unmount, L as mount } from "./disclose-version-et9wt-4m.js";
import { G as GeneratedGallery } from "./GeneratedGallery-DgtlJz3l.js";
function mountGallery(target, props) {
return mount(GeneratedGallery, { target, props });
}
+4 -4
View File
@@ -1,9 +1,9 @@
import { p as push, a as prop, D as comment, v as first_child, i as if_block, g as get, k as append, l as pop, s as state, e as set, f as sibling, t as template_effect, r as event, A as from_html, n as child, y as set_text, x as set_attribute, d as delegate, c as proxy, u as user_effect, E as untrack, o as bind_this, j as delegated, C as tick, m as user_derived, w as each, z as index, h as set_class, F as update, M as unmount, L as mount } from "./disclose-version-uq4tn5Y6.js";
import { s as set_style } from "./style-Boi27oOu.js";
import { b as bind_value } from "./input-DFQhebEz.js";
import { p as push, a as prop, D as comment, v as first_child, i as if_block, g as get, k as append, l as pop, s as state, e as set, f as sibling, t as template_effect, r as event, A as from_html, n as child, y as set_text, x as set_attribute, d as delegate, c as proxy, u as user_effect, E as untrack, o as bind_this, j as delegated, C as tick, m as user_derived, w as each, z as index, h as set_class, F as update, M as unmount, L as mount } from "./disclose-version-et9wt-4m.js";
import { s as set_style } from "./style-B3hsaAru.js";
import { b as bind_value } from "./input-B9kD0bWJ.js";
import { e as extractPrecisions, r as resolveFilesForPrecision } from "./model-constants-cVxhUf51.js";
import { api } from "/scripts/api.js";
import { s as safeJson, H as HttpError } from "./api-context-BFKo1mCD.js";
import { s as safeJson, H as HttpError } from "./api-context-BNqvELYR.js";
var root_2$4 = from_html(`<div class="pcr-hover-card-version svelte-oulz7g"> </div>`);
var root_3$4 = from_html(`<div class="pcr-hover-card-meta svelte-oulz7g"> </div>`);
var root_4$3 = from_html(`<div class="pcr-hover-card-tags svelte-oulz7g"> </div>`);
+6 -6
View File
@@ -1,11 +1,11 @@
import { d as delegate, p as push, a as prop, s as state, c as proxy, u as user_effect, e as set, f as sibling, o as bind_this, t as template_effect, g as get, x as set_attribute, q as set_value, j as delegated, k as append, l as pop, m as user_derived, n as child, A as from_html, w as each, z as index, i as if_block, G as set_checked, y as set_text, h as set_class, v as first_child, D as comment, a1 as set_selected, I as to_array, R as text, E as untrack, M as unmount, L as mount } from "./disclose-version-uq4tn5Y6.js";
import { s as set_style } from "./style-Boi27oOu.js";
import { d as delegate, p as push, a as prop, s as state, c as proxy, u as user_effect, e as set, f as sibling, o as bind_this, t as template_effect, g as get, x as set_attribute, q as set_value, j as delegated, k as append, l as pop, m as user_derived, n as child, A as from_html, w as each, z as index, i as if_block, G as set_checked, y as set_text, h as set_class, v as first_child, D as comment, a1 as set_selected, H as to_array, R as text, E as untrack, M as unmount, L as mount } from "./disclose-version-et9wt-4m.js";
import { s as set_style } from "./style-B3hsaAru.js";
import { F as FAMILIES, A as ARCHITECTURES } from "./model-constants-cVxhUf51.js";
import { api } from "/scripts/api.js";
import { S as SettingsSlider } from "./SettingsSlider-CKF_XmgB.js";
import { b as bind_value } from "./input-DFQhebEz.js";
import { b as bind_select_value } from "./select-Dp4ExMMc.js";
import { a as action } from "./actions-WPfqiWYB.js";
import { S as SettingsSlider } from "./SettingsSlider-C9Ps16J6.js";
import { b as bind_value } from "./input-B9kD0bWJ.js";
import { b as bind_select_value } from "./select-Bp4FHWwK.js";
import { a as action } from "./actions-zjmT0mOr.js";
var root$7 = from_html(`<div class="pcr-slider-container svelte-1pxowmh"><div class="pcr-slider-track svelte-1pxowmh"><div class="pcr-slider-zone pcr-slider-zone-editable svelte-1pxowmh"></div> <div class="pcr-slider-thumb pcr-thumb-blue svelte-1pxowmh"></div> <div class="pcr-slider-thumb pcr-thumb-blue svelte-1pxowmh"></div></div> <input type="number" class="pcr-slider-input pcr-slider-input-half svelte-1pxowmh"/> <span class="pcr-slider-range-sep svelte-1pxowmh">&ndash;</span> <input type="number" class="pcr-slider-input pcr-slider-input-half svelte-1pxowmh"/></div>`);
function RangeSlider($$anchor, $$props) {
push($$props, true);
+8 -8
View File
@@ -5,15 +5,15 @@ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
var _mode, _switchIndex, _locked, _disabled, _collapsed, _connectedCount, _hasLabels, _switchLabel, _iterateCurrent, _iterateTotal, _iterateCycle, _switchOptions, _outputPanelOpen, _imagePanelVisible, _aiAssistantOpen, _posePanelVisible, _regionPanelVisible, _hasPoseStudio, _hasRegionBox, _compiledOutput, _compiledNegOutput, _imageUrl, _previewUrl, _progress, _isGenerating, _rollSelected;
import { p as push, o as bind_this, k as append, l as pop, A as from_html, d as delegate, a as prop, g as get, j as delegated, m as user_derived, s as state, c as proxy, u as user_effect, i as if_block, f as sibling, r as event, e as set, C as tick, n as child, D as comment, v as first_child, w as each, t as template_effect, y as set_text, h as set_class, x as set_attribute, Q as clsx, z as index, R as text, E as untrack, M as unmount, L as mount } from "./disclose-version-uq4tn5Y6.js";
import { u as useApi, p as provideApi } from "./api-context-BFKo1mCD.js";
import { M as Menubar } from "./Menubar-DC12j8gE.js";
import { o as onDestroy, a as onMount } from "./index-client-iMRCrpBY.js";
import { s as set_style } from "./style-Boi27oOu.js";
import { h as html, P as PopupAnchor } from "./PopupAnchor-C6OWGGTI.js";
import { p as push, o as bind_this, k as append, l as pop, A as from_html, d as delegate, a as prop, g as get, j as delegated, m as user_derived, s as state, c as proxy, u as user_effect, i as if_block, f as sibling, r as event, e as set, C as tick, n as child, D as comment, v as first_child, w as each, t as template_effect, y as set_text, h as set_class, x as set_attribute, Q as clsx, z as index, R as text, E as untrack, M as unmount, L as mount } from "./disclose-version-et9wt-4m.js";
import { u as useApi, p as provideApi } from "./api-context-BNqvELYR.js";
import { M as Menubar } from "./Menubar-CMm15KuK.js";
import { o as onDestroy, a as onMount } from "./index-client-6amB1qrM.js";
import { s as set_style } from "./style-B3hsaAru.js";
import { h as html, P as PopupAnchor } from "./PopupAnchor-D5Lvfjom.js";
import { api } from "/scripts/api.js";
import { c as clampGalleryRowHeight, z as zoomGalleryRowHeight, G as GeneratedGallery } from "./GeneratedGallery-D5XbkMk7.js";
import { b as bind_value } from "./input-DFQhebEz.js";
import { c as clampGalleryRowHeight, z as zoomGalleryRowHeight, G as GeneratedGallery } from "./GeneratedGallery-DgtlJz3l.js";
import { b as bind_value } from "./input-B9kD0bWJ.js";
import { app } from "/scripts/app.js";
import { r as readNodePrompt, c as cryptoId, e as extractNGrams, m as matchCharactersInDb, i as isStandaloneMainPromptChain, a as applyPromptText } from "./ai-patch-helpers-Bayqv0oF.js";
var root$d = from_html(`<div class="pcr-editor-frame"></div>`);
+11 -8
View File
@@ -1,11 +1,11 @@
import { c as proxy, N as effect_root, u as user_effect, d as delegate, p as push, r as event, $ as $window, v as first_child, w as each, f as sibling, i as if_block, g as get, o as bind_this, t as template_effect, j as delegated, k as append, l as pop, s as state, e as set, m as user_derived, n as child, A as from_html, J as from_svg, h as set_class, y as set_text, z as index, q as set_value, x as set_attribute, a as prop, O as getContext, C as tick, D as comment, P as setContext, L as mount, M as unmount } from "./disclose-version-uq4tn5Y6.js";
import { a as onMount } from "./index-client-iMRCrpBY.js";
import { s as set_style } from "./style-Boi27oOu.js";
import { c as proxy, N as effect_root, u as user_effect, d as delegate, p as push, r as event, $ as $window, v as first_child, w as each, f as sibling, i as if_block, g as get, o as bind_this, t as template_effect, j as delegated, k as append, l as pop, s as state, e as set, m as user_derived, n as child, A as from_html, I as from_svg, h as set_class, y as set_text, z as index, q as set_value, x as set_attribute, a as prop, O as getContext, C as tick, D as comment, P as setContext, L as mount, M as unmount } from "./disclose-version-et9wt-4m.js";
import { a as onMount } from "./index-client-6amB1qrM.js";
import { s as set_style } from "./style-B3hsaAru.js";
import { j as justifiedLayout } from "./justified-layout-cyVM7i96.js";
import { a as action } from "./actions-WPfqiWYB.js";
import { b as bind_value, c as bind_group } from "./input-DFQhebEz.js";
import { p as portal, C as ConfirmModal } from "./ConfirmModal-BGcVYndw.js";
import { s as safeJson, H as HttpError } from "./api-context-BFKo1mCD.js";
import { a as action } from "./actions-zjmT0mOr.js";
import { b as bind_value, c as bind_group } from "./input-B9kD0bWJ.js";
import { p as portal, C as ConfirmModal } from "./ConfirmModal-CtR7InHn.js";
import { s as safeJson, H as HttpError } from "./api-context-BNqvELYR.js";
const VIEW_MODES = [
{ id: "grid", label: "Grid" },
{ id: "justified", label: "Justified" },
@@ -3011,7 +3011,10 @@ function AssetBrowser($$anchor, $$props) {
const item = (_c = get(ctxMenu)) == null ? void 0 : _c.item;
if (item) {
const root2 = scopeRoots[nav.scope] || "";
const full = root2 ? `${root2}/${item.path}` : item.path;
let full = root2 ? `${root2}/${item.path}` : item.path;
if (/^[a-zA-Z]:[/\\]/.test(full) || full.startsWith("//")) {
full = full.replace(/\//g, "\\");
}
await navigator.clipboard.writeText(full);
(_d = $$props.toast) == null ? void 0 : _d.call($$props, "info", "Path copied to clipboard");
}
+5 -5
View File
@@ -1,9 +1,9 @@
import { d as delegate, p as push, a as prop, w as each, z as index, g as get, f as sibling, i as if_block, k as append, l as pop, m as user_derived, n as child, h as set_class, j as delegated, A as from_html, t as template_effect, x as set_attribute, q as set_value, v as first_child, y as set_text, D as comment, s as state, c as proxy, u as user_effect, e as set, I as to_array, E as untrack, o as bind_this, G as set_checked, M as unmount, L as mount } from "./disclose-version-uq4tn5Y6.js";
import { a as onMount, o as onDestroy } from "./index-client-iMRCrpBY.js";
import { a as action } from "./actions-WPfqiWYB.js";
import { a as bind_checked, b as bind_value } from "./input-DFQhebEz.js";
import { d as delegate, p as push, a as prop, w as each, z as index, g as get, f as sibling, i as if_block, k as append, l as pop, m as user_derived, n as child, h as set_class, j as delegated, A as from_html, t as template_effect, x as set_attribute, q as set_value, v as first_child, y as set_text, D as comment, s as state, c as proxy, u as user_effect, e as set, H as to_array, E as untrack, o as bind_this, G as set_checked, M as unmount, L as mount } from "./disclose-version-et9wt-4m.js";
import { a as onMount, o as onDestroy } from "./index-client-6amB1qrM.js";
import { a as action } from "./actions-zjmT0mOr.js";
import { a as bind_checked, b as bind_value } from "./input-B9kD0bWJ.js";
import { b as buildInsertText } from "./tag-builder-utils-ng134QDV.js";
import { b as bind_select_value } from "./select-Dp4ExMMc.js";
import { b as bind_select_value } from "./select-Bp4FHWwK.js";
const MULTI_SELECT_GROUPS = ["props"];
const CUSTOMIZABLE_CLOTHING_GROUPS = [
"legwear",
+5 -5
View File
@@ -1,8 +1,8 @@
import { d as delegate, p as push, a as prop, s as state, c as proxy, u as user_effect, e as set, f as sibling, n as child, i as if_block, g as get, t as template_effect, j as delegated, k as append, l as pop, v as first_child, h as set_class, y as set_text, A as from_html, m as user_derived, w as each, z as index, I as to_array, C as tick, o as bind_this, D as comment, x as set_attribute, q as set_value, R as text, M as unmount, L as mount } from "./disclose-version-uq4tn5Y6.js";
import { a as onMount } from "./index-client-iMRCrpBY.js";
import { a as action } from "./actions-WPfqiWYB.js";
import { s as set_style } from "./style-Boi27oOu.js";
import { a as bind_checked, b as bind_value } from "./input-DFQhebEz.js";
import { d as delegate, p as push, a as prop, s as state, c as proxy, u as user_effect, e as set, f as sibling, n as child, i as if_block, g as get, t as template_effect, j as delegated, k as append, l as pop, v as first_child, h as set_class, y as set_text, A as from_html, m as user_derived, w as each, z as index, H as to_array, C as tick, o as bind_this, D as comment, x as set_attribute, q as set_value, R as text, M as unmount, L as mount } from "./disclose-version-et9wt-4m.js";
import { a as onMount } from "./index-client-6amB1qrM.js";
import { a as action } from "./actions-zjmT0mOr.js";
import { s as set_style } from "./style-B3hsaAru.js";
import { a as bind_checked, b as bind_value } from "./input-B9kD0bWJ.js";
import { b as isStructuralLine, d as isInNegativeBlock } from "./ai-patch-helpers-Bayqv0oF.js";
import { f as formatTagsForModel } from "./tag-builder-utils-ng134QDV.js";
var root_1$3 = from_html(`<div class="pcr-atb2-cust-loading svelte-1mwxie1">Loading…</div>`);
File diff suppressed because it is too large Load Diff
@@ -1,4 +1,4 @@
import { a2 as listen_to_event_and_reset_event, a3 as current_batch, a4 as effect, a5 as is_array, a6 as select_multiple_invalid_value, a7 as is, a8 as teardown } from "./disclose-version-uq4tn5Y6.js";
import { a2 as listen_to_event_and_reset_event, a3 as current_batch, a4 as effect, a5 as is_array, a6 as select_multiple_invalid_value, a7 as is, a8 as teardown } from "./disclose-version-et9wt-4m.js";
function select_option(select, value, mounting = false) {
if (select.multiple) {
if (value == void 0) {
@@ -97,4 +97,4 @@ export {
init_select as i,
select_option as s
};
//# sourceMappingURL=select-Dp4ExMMc.js.map
//# sourceMappingURL=select-Bp4FHWwK.js.map
@@ -1,4 +1,4 @@
import { ac as to_style } from "./disclose-version-uq4tn5Y6.js";
import { ac as to_style } from "./disclose-version-et9wt-4m.js";
function update_styles(dom, prev = {}, next, priority) {
for (var key in next) {
var value = next[key];
@@ -36,4 +36,4 @@ function set_style(dom, value, prev_styles, next_styles) {
export {
set_style as s
};
//# sourceMappingURL=style-Boi27oOu.js.map
//# sourceMappingURL=style-B3hsaAru.js.map
+38 -8
View File
@@ -813,32 +813,57 @@ function ensureDepthBranch(graph, LG, usNode, depthSource, loadNode) {
const poser = graph._nodes.find((n) => n.comfyClass === "PromptChain_PoseStudio");
const sourceNode = depthSource === "pose" && poser ? poser : loadNode;
const sourceSlot = sourceNode === poser ? (poser.outputs?.findIndex((o) => o.name === "IMAGE") ?? 0) : 0;
// The Poser feeds its IMAGE straight into Apply ControlNet when it ALREADY renders
// true depth — its uploaded control map is the depth map itself, so DepthAnything
// is skipped. A legacy clay-mode Poser keeps the estimator: nothing re-captures a
// depth render at queue time, so pointing a clay render at a depth net would be wrong.
const poseDirect = sourceNode === poser &&
(poser.properties?.pcrOutputMode === "depth" || poser.properties?.pcrTrueDepth === true);
const existing = findDepthApply(graph, usNode);
if (existing) {
if (existing.pre) {
if (poseDirect) {
// Hint the apply straight from the Poser; retire any DepthAnything in between.
sourceNode.connect(sourceSlot, existing.apply, inputIdx(existing.apply, "image", 3));
if (existing.pre) existing.pre.mode = 4;
} else if (existing.pre) {
const cur = traceInput(graph, existing.pre, "image");
if (cur?.node !== sourceNode) {
sourceNode.connect(sourceSlot, existing.pre, inputIdx(existing.pre, "image", 0));
}
} else {
// The apply was built pose-direct (no preprocessor) but the new source isn't
// a true-depth render — a clay-mode Poser or an image. Its raw pixels aren't a
// depth map, so splice a DepthAnything estimator back in between source and apply.
const pre = LG.createNode(resolvePre(CONTROLNET_TYPES.depth.pre));
if (pre) {
pre.pos = [existing.apply.pos[0] - 340, existing.apply.pos[1]];
graph.add(pre);
pre.size = [291, pre.size?.[1] ?? 130];
const resW = pre.widgets?.find((w) => w.name === "resolution");
if (resW) { resW.value = 1024; resW.callback?.(1024); }
sourceNode.connect(sourceSlot, pre, inputIdx(pre, "image", 0));
pre.connect(0, existing.apply, inputIdx(existing.apply, "image", 3));
}
}
return true;
}
const cfg = CONTROLNET_TYPES.depth;
const pre = LG.createNode(resolvePre(cfg.pre));
const pre = poseDirect ? null : LG.createNode(resolvePre(cfg.pre));
const loader = LG.createNode("ControlNetLoader");
const setUnion = LG.createNode("SetUnionControlNetType");
const applyNode = LG.createNode("ControlNetApplyAdvanced");
if (!pre || !loader || !setUnion || !applyNode) return false;
if ((!poseDirect && !pre) || !loader || !setUnion || !applyNode) return false;
const ax = usNode.pos[0];
const ay = usNode.pos[1];
pre.pos = [ax - 700, ay + 420];
if (pre) pre.pos = [ax - 700, ay + 420];
loader.pos = [ax - 700, ay + 640];
setUnion.pos = [ax - 700, ay + 780];
applyNode.pos = [ax - 360, ay + 420];
graph.add(pre); graph.add(loader); graph.add(setUnion); graph.add(applyNode);
if (pre) graph.add(pre);
graph.add(loader); graph.add(setUnion); graph.add(applyNode);
applyNode.title = `ControlNet — Depth${sourceNode === poser ? " (pose)" : ""}`;
applyNode.properties = { ...(applyNode.properties || {}), pcrControlType: "depth" };
@@ -850,15 +875,20 @@ function ensureDepthBranch(graph, LG, usNode, depthSource, loadNode) {
else if (w.name === "start_percent") { w.value = 0.0; w.callback?.(0.0); }
else if (w.name === "end_percent") { w.value = cfg.end; w.callback?.(cfg.end); }
}
const resW = pre.widgets?.find((w) => w.name === "resolution");
const resW = pre?.widgets?.find((w) => w.name === "resolution");
if (resW) { resW.value = 1024; resW.callback?.(1024); }
const posSrc = traceInput(graph, usNode, "positive");
const negSrc = traceInput(graph, usNode, "negative");
if (!posSrc?.node || !negSrc?.node) return false;
sourceNode.connect(sourceSlot, pre, inputIdx(pre, "image", 0));
pre.connect(0, applyNode, inputIdx(applyNode, "image", 3));
if (pre) {
sourceNode.connect(sourceSlot, pre, inputIdx(pre, "image", 0));
pre.connect(0, applyNode, inputIdx(applyNode, "image", 3));
} else {
// Pose-direct: the Poser's true-depth IMAGE is the control map itself.
sourceNode.connect(sourceSlot, applyNode, inputIdx(applyNode, "image", 3));
}
loader.connect(0, setUnion, 0);
setUnion.connect(0, applyNode, inputIdx(applyNode, "control_net", 2));
posSrc.node.connect(posSrc.slot, applyNode, inputIdx(applyNode, "positive", 0));
+9 -1
View File
@@ -306,7 +306,15 @@ export async function openViewer(images, startIndex = 0, workflowId = "", onDele
onInpaintInstallPack: (injectable) => installInpaintPack(injectable),
onMountPromptEditor: mountPromptEditor,
onEditPrepare: (entry) => prepareEdit(entry),
onEditSave: (prepared, blob, prefix) => saveEditedImage(prepared, blob, prefix),
onEditSave: async (prepared, blob, prefix) => {
const entry = await saveEditedImage(prepared, blob, prefix);
// Opened from a node (e.g. LoadImage) that wants the result wired back.
if (entry && typeof opts.onAfterEditSave === "function") {
try { await opts.onAfterEditSave(entry); }
catch (e) { console.error("[PromptChain] onAfterEditSave failed", e); }
}
return entry;
},
onInpaintRegion: (data, args) => inpaintRegion(data, args),
onReposeCaps: () => fetchReposeCaps(),
onReposeRun: (opts) => runReposeInBackground(opts),
+123
View File
@@ -0,0 +1,123 @@
// 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;
};
},
});
+33 -14
View File
@@ -367,20 +367,6 @@ function setupImagePreviewListeners() {
// here describe the user's active graph, not that prompt's.
if (detail.prompt_id && externallyTrackedPrompts.has(detail.prompt_id)) return;
const nodeId = Number(detail.node);
let pcNodes = _outputToPC.get(nodeId);
let wfId = _executionWorkflowId;
if (!pcNodes?.length) {
// The SaveImage didn't map to a PromptChain node via the execution_start
// graph scan (trace miss / subgraph / workflow-id drift). Don't silently
// drop the generation — diagnose, then attribute it to the active graph's
// PromptChain node so the generation panel still catches it.
if (!wfId) wfId = getWorkflowId();
const fallbackPC = (app.graph?._nodes || []).find((n) => n.comfyClass === NODE_TYPE);
console.warn(`[PromptChain][record] unmapped output node=${detail.node}->${nodeId} mapKeys=[${[..._outputToPC.keys()]}] wfId=${wfId} graphNodes=${app.graph?._nodes?.length} rootGraph=${!!app.rootGraph} fallbackPC=${fallbackPC?.id ?? "none"}`);
if (!fallbackPC || !wfId) return;
pcNodes = [fallbackPC];
}
const img = detail.output.images[detail.output.images.length - 1];
const params = new URLSearchParams({
filename: img.filename,
@@ -389,6 +375,39 @@ function setupImagePreviewListeners() {
});
const imageUrl = api.apiURL(`/view?${params}&rand=${Math.random()}`);
// A PreviewImage emits type "temp" — a transient guide map (e.g. the 3D
// Poser depth control map), not a saved render. It still flashes in the
// node panel to guide the user, but must never be recorded: the Generated
// gallery holds saved outputs (SaveImage → "output") only.
const isSavedOutput = (img.type || "output") === "output";
let pcNodes = _outputToPC.get(nodeId);
let wfId = _executionWorkflowId;
if (!pcNodes?.length) {
// The SaveImage didn't map to a PromptChain node via the execution_start
// graph scan (trace miss / subgraph / workflow-id drift). Don't silently
// drop the generation — diagnose, then attribute it to the active graph's
// PromptChain node so the generation panel still catches it. A side-branch
// preview (the pose depth map) is EXPECTED to be unmapped — don't warn.
if (!wfId) wfId = getWorkflowId();
const fallbackPC = (app.graph?._nodes || []).find((n) => n.comfyClass === NODE_TYPE);
if (isSavedOutput) {
console.warn(`[PromptChain][record] unmapped output node=${detail.node}->${nodeId} mapKeys=[${[..._outputToPC.keys()]}] wfId=${wfId} graphNodes=${app.graph?._nodes?.length} rootGraph=${!!app.rootGraph} fallbackPC=${fallbackPC?.id ?? "none"}`);
}
if (!fallbackPC || (isSavedOutput && !wfId)) return;
pcNodes = [fallbackPC];
}
// Transient preview (PreviewImage / non-"output"): flash in the panel to
// guide the user, then stop — no metadata, no gallery record.
if (!isSavedOutput) {
for (const pc of pcNodes) {
pc._pcrImagePanel?.updateImage(imageUrl, null);
pc._pcrImagePanel?.hideProgress();
}
return;
}
// metadata from graph-determined nearest KSampler
const entry = _saveNodeGraph.get(nodeId);
let meta = entry?.ksampleId != null ? { ..._executionMeta.get(entry.ksampleId) } : {};
+5 -1
View File
@@ -113,9 +113,13 @@ function installPoseStyles() {
// Stop pointer/key events from bubbling to LiteGraph (which would drag the node
// or pop its context menu), but only in the BUBBLE phase — OrbitControls listens
// on the canvas in the target phase, which runs first, so it still gets to orbit.
// `mousedown` is isolated too: when the viewport is docked inside the PromptChain
// fullscreen overlay, that overlay preventDefaults stray mousedowns to keep focus
// in the editor — which silently suppresses the popup of every native <select>
// here (foot/hand pose, body pose, shoe pickers), making them open then snap shut.
function isolateViewportEvents(container) {
const stop = (event) => event.stopPropagation();
for (const type of ["pointerdown", "pointerup", "pointermove", "keydown", "keyup", "contextmenu"]) {
for (const type of ["pointerdown", "mousedown", "pointerup", "pointermove", "keydown", "keyup", "contextmenu"]) {
container.addEventListener(type, stop);
}
// LiteGraph captures the pointer on its canvas; release it so a drag here
+1 -1
View File
@@ -5,7 +5,7 @@
[project]
name = "comfyui-promptchain"
version = "0.1.8"
version = "0.1.9"
description = "Prompt-authoring and image-iteration suite for ComfyUI: structured prompts, 3D posing, model awareness, regional control, and layered editing."
license = { file = "LICENSE" }
requires-python = ">=3.10"
+6 -1
View File
@@ -18,7 +18,7 @@
let {
open,
sourceCanvas = null, // flattened composite to adjust
sourceCanvas = null, // the active layer's pixels to adjust (caller scopes this)
filename = "",
onApply = null, // (ImageData) => void
onCancel,
@@ -269,6 +269,11 @@
if (cH && y > 0.75) add += cH * (1 - Math.cos(Math.PI * (y - 0.75) / 0.25)) / 2;
lut[i] = Math.max(0, Math.min(1, y + 0.25 * add));
}
// Enforce monotonicity: large/opposing parametric bumps can fold the curve back
// on itself (output decreasing as input rises), which inverts/solarizes tones.
// Clamp any fold to a flat plateau instead — graceful, and keeps full slider
// strength wherever the curve is still rising.
for (let i = 1; i < N; i++) if (lut[i] < lut[i - 1]) lut[i] = lut[i - 1];
return lut;
}
File diff suppressed because it is too large Load Diff
@@ -456,7 +456,13 @@
const item = ctxMenu?.item;
if (item) {
const root = scopeRoots[nav.scope] || "";
const full = root ? `${root}/${item.path}` : item.path;
let full = root ? `${root}/${item.path}` : item.path;
// The backend normalizes root/paths to forward slashes for web use; restore
// native separators when the path is Windows-rooted (drive letter or UNC) so
// it pastes correctly into Explorer and Windows apps.
if (/^[a-zA-Z]:[/\\]/.test(full) || full.startsWith("//")) {
full = full.replace(/\//g, "\\");
}
await navigator.clipboard.writeText(full);
toast?.("info", "Path copied to clipboard");
}