107 lines
5.0 KiB
JavaScript
107 lines
5.0 KiB
JavaScript
// Region → mask-row binding for the Edit isolate workflow. JS port of
|
|
// core/compiler.py region_figure_indices — keep the two in lockstep: mask
|
|
// rows are REGION ENTITIES (figures in figure order, then named props,
|
|
// stamped in pose_state v3 regionEntities), name match first, then the
|
|
// legacy fallbacks (trailing-int id, block order) clamped into figure space.
|
|
|
|
export function regionFigureIndices(regionList, poseJson) {
|
|
let names = [];
|
|
let numFigures = null; // null = no entity list -> legacy unclamped fallbacks
|
|
if (poseJson && poseJson.trim()) {
|
|
try {
|
|
const pose = JSON.parse(poseJson);
|
|
const ents = Array.isArray(pose.regionEntities) && pose.regionEntities.length
|
|
? pose.regionEntities : null;
|
|
if (ents) {
|
|
names = ents.map((e) => String((e && e.name) || "").toLowerCase());
|
|
numFigures = ents.filter((e) => e && e.kind === "figure").length;
|
|
} else if (Array.isArray(pose.figures)) {
|
|
names = pose.figures.map((f, i) => String((f && f.name) || `mannequin${i + 1}`).toLowerCase());
|
|
}
|
|
} catch { /* fall through to the positional fallbacks */ }
|
|
}
|
|
return regionList.map((r, n) => {
|
|
const rname = String(r?.name || "").toLowerCase();
|
|
const hit = rname ? names.indexOf(rname) : -1;
|
|
if (hit >= 0) return hit;
|
|
let idx = parseInt(r?.id, 10);
|
|
idx = Number.isFinite(idx) ? idx - 1 : n;
|
|
if (numFigures !== null) idx = Math.min(Math.max(idx, 0), Math.max(numFigures, 1) - 1);
|
|
return idx;
|
|
});
|
|
}
|
|
|
|
// Which region does a probe shape (the painted inpaint mask, or a filled
|
|
// scene-rect for a moved copy) sit on? Loads each region's silhouette PNG,
|
|
// scales it to the document, and scores overlap = (probe∩region)/probe. The
|
|
// best-scoring region wins, but only past a floor — painting the background
|
|
// or a gap between figures matches nothing, so the caller keeps the global
|
|
// prompt. This is the inpaint analog of RegionalConditioning's spatial
|
|
// masking: in a regioned workflow you paint a figure and get that figure's
|
|
// prompt with no manual pick.
|
|
export async function matchRegionByOverlap(probeCanvas, regions, width, height) {
|
|
if (!probeCanvas || !regions?.length || !(width > 0) || !(height > 0)) return null;
|
|
const pdata = probeCanvas.getContext("2d").getImageData(0, 0, width, height).data;
|
|
let probeCount = 0;
|
|
for (let i = 3; i < pdata.length; i += 4) if (pdata[i] > 127) probeCount++;
|
|
if (!probeCount) return null;
|
|
|
|
let best = null, bestScore = 0;
|
|
for (const r of regions) {
|
|
if (!r.maskUrl || !r.text) continue;
|
|
let bmp;
|
|
try {
|
|
const resp = await fetch(r.maskUrl);
|
|
if (!resp.ok) continue;
|
|
bmp = await createImageBitmap(await resp.blob());
|
|
} catch { continue; }
|
|
const rc = document.createElement("canvas");
|
|
rc.width = width; rc.height = height;
|
|
const rctx = rc.getContext("2d");
|
|
rctx.drawImage(bmp, 0, 0, width, height);
|
|
const rdata = rctx.getImageData(0, 0, width, height).data;
|
|
let overlap = 0;
|
|
for (let i = 0; i < pdata.length; i += 4) {
|
|
if (pdata[i + 3] > 127 && rdata[i] > 64) overlap++;
|
|
}
|
|
const score = overlap / probeCount;
|
|
if (score > bestScore) { bestScore = score; best = r; }
|
|
}
|
|
return bestScore >= 0.2 ? best : null;
|
|
}
|
|
|
|
// Assemble the per-region rows the Edit isolate UI consumes: each region of
|
|
// the displayed image with its prompt text (DB regions JSON) and the URL of
|
|
// its on-disk silhouette mask (content-addressed beside the pose control
|
|
// map). Empty when the image has no pose scene, no DB regions, or the pose
|
|
// files are known missing.
|
|
export function buildFigureRegions({ workflow, regionsRaw, poseFilesOk, apiURL }) {
|
|
try {
|
|
if (!workflow || poseFilesOk === false) return [];
|
|
let regions = regionsRaw;
|
|
if (typeof regions === "string") { try { regions = JSON.parse(regions); } catch { return []; } }
|
|
const list = Array.isArray(regions?.regions) ? regions.regions : [];
|
|
if (!list.length) return [];
|
|
const poser = (workflow.nodes || []).find((n) => n.type === "PromptChain_PoseStudio");
|
|
const values = poser?.widgets_values || [];
|
|
// widget order: control_map, pose_state, width, height — the map ref can
|
|
// carry ComfyUI's " [input]" annotation suffix.
|
|
const mapRef = typeof values[0] === "string" ? values[0].trim().replace(/\s*\[\w+\]$/, "") : "";
|
|
if (!mapRef) return [];
|
|
const poseState = typeof values[1] === "string" ? values[1] : "";
|
|
const indices = regionFigureIndices(list, poseState);
|
|
// The map ref is input-dir-relative and can carry a subfolder
|
|
// (promptchain_pose/...); /view wants subfolder and filename SPLIT.
|
|
const slash = mapRef.lastIndexOf("/");
|
|
const subfolder = slash >= 0 ? mapRef.slice(0, slash) : "";
|
|
const baseName = (slash >= 0 ? mapRef.slice(slash + 1) : mapRef).replace(/\.[^.]+$/, "");
|
|
return list.map((r, i) => ({
|
|
name: r.name || `region ${i + 1}`,
|
|
text: (r.text || "").trim(),
|
|
maskUrl: apiURL(`/view?filename=${encodeURIComponent(`${baseName}_mask${indices[i]}.png`)}&type=input&subfolder=${encodeURIComponent(subfolder)}`),
|
|
}));
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|