Additive scene load nudged each region further than the one before it, so a saved composition was sheared. Every region now moves by the same offset and is clamped back inside the canvas. Co-authored-by: AE <by-ae@users.noreply.github.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com>
122 lines
3.5 KiB
JavaScript
122 lines
3.5 KiB
JavaScript
/** Pure region layout helpers for Ideogram 4 captions. No DOM. */
|
|
|
|
export function finiteCoord(value) {
|
|
if (typeof value === "boolean" || value == null) return null;
|
|
if (typeof value === "string") {
|
|
if (value.trim() === "") return null;
|
|
} else if (typeof value !== "number") {
|
|
return null;
|
|
}
|
|
const n = Number(value);
|
|
return Number.isFinite(n) ? n : null;
|
|
}
|
|
|
|
function clampBboxAxis(value) {
|
|
return Math.max(0, Math.min(1000, value));
|
|
}
|
|
|
|
/** Ideogram bbox is [ymin, xmin, ymax, xmax] in 0-1000, origin top-left. */
|
|
export function ideogramBboxToRect(bbox) {
|
|
if (!Array.isArray(bbox) || bbox.length !== 4) return null;
|
|
const coords = bbox.map(finiteCoord);
|
|
if (coords.some((v) => v == null)) return null;
|
|
let [ymin, xmin, ymax, xmax] = coords;
|
|
if (ymin > ymax) [ymin, ymax] = [ymax, ymin];
|
|
if (xmin > xmax) [xmin, xmax] = [xmax, xmin];
|
|
ymin = clampBboxAxis(ymin);
|
|
xmin = clampBboxAxis(xmin);
|
|
ymax = clampBboxAxis(ymax);
|
|
xmax = clampBboxAxis(xmax);
|
|
return {
|
|
x: xmin / 1000,
|
|
y: ymin / 1000,
|
|
w: (xmax - xmin) / 1000,
|
|
h: (ymax - ymin) / 1000,
|
|
};
|
|
}
|
|
|
|
export function fallbackRegion(index) {
|
|
const i = Number.isInteger(index) && index >= 0 ? index : 0;
|
|
const row = Math.floor(i / 4);
|
|
const col = i % 4;
|
|
return {
|
|
x: 0.02 + col * 0.25,
|
|
y: 0.02 + row * 0.17,
|
|
w: 0.22,
|
|
h: 0.14,
|
|
nobbox: true,
|
|
};
|
|
}
|
|
|
|
export function elementToRegion(el, idx) {
|
|
if (!el || typeof el !== "object") return null;
|
|
const box = {
|
|
type: el.type === "text" ? "text" : "obj",
|
|
text: el.text || "",
|
|
desc: el.desc || "",
|
|
palette: Array.isArray(el.color_palette) ? el.color_palette.slice() : [],
|
|
};
|
|
const rect = ideogramBboxToRect(el.bbox);
|
|
if (rect) Object.assign(box, rect);
|
|
else Object.assign(box, fallbackRegion(idx));
|
|
return box;
|
|
}
|
|
|
|
export function regionCoordsFinite(b) {
|
|
return !!b && [b.x, b.y, b.w, b.h].every((n) => typeof n === "number" && Number.isFinite(n));
|
|
}
|
|
|
|
const HEX6 = /^#[0-9A-F]{6}$/;
|
|
const HEX3 = /^#[0-9A-F]{3}$/;
|
|
|
|
/** Ideogram 4 accepts uppercase #RRGGBB. #RGB is expanded; anything else is dropped. */
|
|
export function normalizeHex(color) {
|
|
if (typeof color !== "string") return null;
|
|
const text = color.trim().toUpperCase();
|
|
if (HEX6.test(text)) return text;
|
|
if (HEX3.test(text)) return "#" + text.slice(1).split("").map((ch) => ch + ch).join("");
|
|
return null;
|
|
}
|
|
|
|
export function cleanPalette(colors, limit) {
|
|
const out = [];
|
|
for (const color of colors || []) {
|
|
const hex = normalizeHex(color);
|
|
if (!hex) continue;
|
|
out.push(hex);
|
|
if (limit != null && out.length >= limit) break;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function clamp01(v) {
|
|
return Math.max(0, Math.min(1, v));
|
|
}
|
|
|
|
function normalizeBoxCoords(b) {
|
|
let x = b.x;
|
|
let y = b.y;
|
|
let w = b.w;
|
|
let h = b.h;
|
|
if (typeof x !== "number" || !Number.isFinite(x)) x = 0;
|
|
if (typeof y !== "number" || !Number.isFinite(y)) y = 0;
|
|
if (typeof w !== "number" || !Number.isFinite(w)) w = 0;
|
|
if (typeof h !== "number" || !Number.isFinite(h)) h = 0;
|
|
if (w < 0) { x += w; w = -w; }
|
|
if (h < 0) { y += h; h = -h; }
|
|
x = clamp01(x);
|
|
y = clamp01(y);
|
|
w = Math.min(w, 1 - x);
|
|
h = Math.min(h, 1 - y);
|
|
return { ...b, x, y, w: Math.max(0, w), h: Math.max(0, h) };
|
|
}
|
|
|
|
/** Move every region by the same nudge, then keep it inside the canvas. */
|
|
export function offsetAddedRegions(boxes, nudge = 0.05) {
|
|
return (boxes || []).map((b) => normalizeBoxCoords({
|
|
...b,
|
|
x: (typeof b.x === "number" && Number.isFinite(b.x) ? b.x : 0) + nudge,
|
|
y: (typeof b.y === "number" && Number.isFinite(b.y) ? b.y : 0) + nudge,
|
|
}));
|
|
}
|