fix(canvas): 保护生成中与失败节点画幅尺寸,支持变体即时持久化与 2K 分辨率高亮 (#359)

Co-authored-by: Danborad <danborad@github.com>
This commit is contained in:
Danborad
2026-08-31 12:28:12 +08:00
committed by GitHub
parent 76e5a166dc
commit 8d9ab03a66
10 changed files with 127 additions and 77 deletions

View File

@@ -17,11 +17,34 @@ export function fitNodeSize(width: number, height: number, maxWidth = 720, maxHe
export function nodeSizeFromRatio(size: string, baseWidth: number, baseHeight: number) {
const raw = String(size || "").trim();
if (!raw || raw.toLowerCase() === "auto") return null;
// 支持 16:9 / 1024x576 / 16:9-2k
let width = 0;
let height = 0;
const match = raw.match(/^(\d+(?:\.\d+)?)(?:x|:)(\d+(?:\.\d+)?)/i);
if (!match) return null;
const width = Number(match[1]);
const height = Number(match[2]);
if (match) {
width = Number(match[1]);
height = Number(match[2]);
} else if (raw.includes("竖") || raw.includes("portrait") || raw.includes("9:16")) {
width = 9;
height = 16;
} else if (raw.includes("横") || raw.includes("landscape") || raw.includes("16:9")) {
width = 16;
height = 9;
} else if (raw.includes("(1:1)") || raw.includes("1:1") || raw.includes("square")) {
width = 1;
height = 1;
} else if (raw.includes("3:4")) {
width = 3;
height = 4;
} else if (raw.includes("4:3")) {
width = 4;
height = 3;
} else if (raw.includes("2:3")) {
width = 2;
height = 3;
} else if (raw.includes("3:2")) {
width = 3;
height = 2;
}
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return null;
const ratio = width / Math.max(1, height);
if (ratio < 0.25 || ratio > 4) return { width: baseWidth, height: baseHeight };
@@ -35,14 +58,22 @@ export function ensureMediaNodeMinimumSize(node: CanvasNodeData) {
let width = node.width;
let height = node.height;
const emptyStage = isBuiltinCanvasNodeType(node.type) ? NODE_DEFAULT_SIZE[node.type] : undefined;
const shouldPromoteEmptyStage = !node.metadata?.content
&& !node.metadata?.freeResize
&& !node.metadata?.locked
&& emptyStage !== undefined
&& width * height < emptyStage.width * emptyStage.height;
if (shouldPromoteEmptyStage) {
width = emptyStage.width;
height = emptyStage.height;
// 如果未完成节点(生成中/失败/空节点)指定了目标比例(如 3:4, 9:16),按目标比例保持占位框尺寸,不能强制变成 16:9 横屏。
const targetSize = node.metadata?.size ? nodeSizeFromRatio(node.metadata.size, emptyStage?.width || 720, emptyStage?.height || 405) : null;
if (targetSize && !node.metadata?.content && !node.metadata?.freeResize && !node.metadata?.locked) {
width = targetSize.width;
height = targetSize.height;
} else {
const shouldPromoteEmptyStage = !node.metadata?.content
&& !node.metadata?.freeResize
&& !node.metadata?.locked
&& emptyStage !== undefined
&& (width <= 0 || height <= 0);
if (shouldPromoteEmptyStage) {
width = emptyStage.width;
height = emptyStage.height;
}
}
const naturalWidth = node.metadata?.naturalWidth || 0;
const naturalHeight = node.metadata?.naturalHeight || 0;

View File

@@ -20,7 +20,7 @@ export function desktopLocalChannelFormState(desktopLocalChannelsEnabled: boolea
export function projectDesktopLocalChannelRuntime<T extends { allowLocalChannel?: boolean }>(config: T): T {
const desktopLocalChannelsEnabled = useUserStore.getState().features.desktopLocalChannelsEnabled;
const hostname = typeof window === "undefined" ? "" : window.location.hostname;
const hostname = typeof window === "undefined" ? "" : window.location?.hostname || "";
const allowLocalChannel = desktopLocalChannelPayloadValue(desktopLocalChannelsEnabled, hostname, config.allowLocalChannel);
return config.allowLocalChannel === allowLocalChannel ? config : { ...config, allowLocalChannel };
}

View File

@@ -11,16 +11,35 @@ export function normalizeVideoDuration(value: string | number | undefined) {
export function normalizeVideoResolution(value: string | number | undefined) {
const raw = String(value || "").trim();
const token = raw.toLowerCase();
if (!token) return "";
if (token === "low") return "480";
if (token === "auto" || token === "medium" || token === "high") return "720";
if (token === "2k") return "1440";
if (token === "4k") return "2160";
const resolution = Number(token.replace(/p$/i, ""));
if (Number.isFinite(resolution)) return resolution > 0 ? String(Math.floor(resolution)) : "720";
if (Number.isFinite(resolution) && resolution > 0) return String(Math.floor(resolution));
// Channel-declared values are opaque enums, not necessarily numeric tiers.
// Preserve them verbatim so values such as `768p竖` remain selectable and
// can be sent back to the provider without being collapsed to 720p.
return raw || "720";
return raw;
}
export function normalizeVideoResolutionValue(value: string | number | undefined): string {
return normalizeVideoResolution(value) || "720";
}
export function isVideoResolutionMatch(selected: string | undefined, target: string | undefined) {
const s = String(selected || "").trim().toLowerCase().replace(/p$/i, "");
const t = String(target || "").trim().toLowerCase().replace(/p$/i, "");
if (!s && !t) return true;
if (s === t) return true;
if ((s === "2k" || s === "1440") && (t === "2k" || t === "1440")) return true;
if ((s === "4k" || s === "2160") && (t === "4k" || t === "2160")) return true;
if ((s === "768" || s === "768p") && (t === "768" || t === "768p")) return true;
if ((s === "1080" || s === "1080p") && (t === "1080" || t === "1080p")) return true;
if ((s === "720" || s === "720p") && (t === "720" || t === "720p")) return true;
if ((s === "480" || s === "480p") && (t === "480" || t === "480p")) return true;
return false;
}
export function videoResolutionComparisonKey(value: string | number | undefined) {
@@ -34,7 +53,8 @@ export function formatVideoResolutionLabel(value: string | number | undefined) {
const raw = String(value || "").trim();
if (!raw) return "";
const normalized = normalizeVideoResolution(raw);
if (normalized === "2160") return "4K";
if (normalized === "1440" || normalized.toLowerCase() === "2k") return "2K";
if (normalized === "2160" || normalized.toLowerCase() === "4k") return "4K";
if (/^\d+$/.test(normalized)) return `${normalized}P`;
return normalized.replace(/^(\d+)p/i, "$1P");
}