feat: add reference images to local edit regions
This commit is contained in:
parent
e415de06d8
commit
4c86e01956
@ -30,6 +30,12 @@ type LocalEditRegionInput = {
|
|||||||
color?: string;
|
color?: string;
|
||||||
prompt?: string;
|
prompt?: string;
|
||||||
rect?: LocalEditRect;
|
rect?: LocalEditRect;
|
||||||
|
referenceImages?: Array<{
|
||||||
|
id?: string;
|
||||||
|
url?: string;
|
||||||
|
name?: string;
|
||||||
|
source?: string;
|
||||||
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type LocalEditRequest = {
|
type LocalEditRequest = {
|
||||||
@ -47,6 +53,12 @@ type NormalizedLocalEditRegion = {
|
|||||||
color: string;
|
color: string;
|
||||||
colorName: string;
|
colorName: string;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
|
referenceImages: Array<{
|
||||||
|
id: string;
|
||||||
|
url: string;
|
||||||
|
name?: string;
|
||||||
|
source: "upload" | "canvas";
|
||||||
|
}>;
|
||||||
rect: {
|
rect: {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
@ -62,6 +74,7 @@ type NormalizedLocalEditRegion = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MAX_LOCAL_EDIT_REGIONS = 8;
|
const MAX_LOCAL_EDIT_REGIONS = 8;
|
||||||
|
const MAX_REFERENCE_IMAGES_PER_REGION = 3;
|
||||||
const MIN_REGION_EDGE_PX = 24;
|
const MIN_REGION_EDGE_PX = 24;
|
||||||
const DEFAULT_REGION_COLORS = [
|
const DEFAULT_REGION_COLORS = [
|
||||||
"#ff3b30",
|
"#ff3b30",
|
||||||
@ -100,6 +113,41 @@ function normalizeHexColor(input: unknown, fallback: string) {
|
|||||||
return /^#[0-9a-f]{6}$/i.test(expanded) ? expanded.toLowerCase() : fallback;
|
return /^#[0-9a-f]{6}$/i.test(expanded) ? expanded.toLowerCase() : fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeHttpImageUrl(input: unknown) {
|
||||||
|
const raw = String(input || "").trim();
|
||||||
|
if (!/^https?:\/\//i.test(raw)) return "";
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRegionReferenceImages(
|
||||||
|
region: LocalEditRegionInput,
|
||||||
|
itemIndex: number
|
||||||
|
): NormalizedLocalEditRegion["referenceImages"] {
|
||||||
|
if (!Array.isArray(region.referenceImages)) return [];
|
||||||
|
return region.referenceImages
|
||||||
|
.map((reference, referenceIndex) => {
|
||||||
|
const url = normalizeHttpImageUrl(reference?.url);
|
||||||
|
if (!url) return null;
|
||||||
|
const name =
|
||||||
|
typeof reference?.name === "string" && reference.name.trim()
|
||||||
|
? reference.name.trim().slice(0, 80)
|
||||||
|
: "";
|
||||||
|
return {
|
||||||
|
id:
|
||||||
|
typeof reference?.id === "string" && reference.id.trim()
|
||||||
|
? reference.id.trim()
|
||||||
|
: `region-${itemIndex + 1}-reference-${referenceIndex + 1}`,
|
||||||
|
url,
|
||||||
|
...(name ? { name } : {}),
|
||||||
|
source: reference?.source === "canvas" ? "canvas" as const : "upload" as const,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((item): item is NormalizedLocalEditRegion["referenceImages"][number] =>
|
||||||
|
Boolean(item)
|
||||||
|
)
|
||||||
|
.slice(0, MAX_REFERENCE_IMAGES_PER_REGION);
|
||||||
|
}
|
||||||
|
|
||||||
function getColorName(hex: string) {
|
function getColorName(hex: string) {
|
||||||
const normalized = hex.toLowerCase();
|
const normalized = hex.toLowerCase();
|
||||||
const names: Record<string, string> = {
|
const names: Record<string, string> = {
|
||||||
@ -203,6 +251,7 @@ function normalizeRegions(
|
|||||||
color,
|
color,
|
||||||
colorName: getColorName(color),
|
colorName: getColorName(color),
|
||||||
prompt: prompt.slice(0, 300),
|
prompt: prompt.slice(0, 300),
|
||||||
|
referenceImages: normalizeRegionReferenceImages(region, itemIndex),
|
||||||
rect,
|
rect,
|
||||||
pixelRect,
|
pixelRect,
|
||||||
};
|
};
|
||||||
@ -278,18 +327,32 @@ function buildLocalEditPrompt(regions: NormalizedLocalEditRegion[]) {
|
|||||||
const top = Math.round(region.rect.y * 1000) / 10;
|
const top = Math.round(region.rect.y * 1000) / 10;
|
||||||
const width = Math.round(region.rect.width * 1000) / 10;
|
const width = Math.round(region.rect.width * 1000) / 10;
|
||||||
const height = Math.round(region.rect.height * 1000) / 10;
|
const height = Math.round(region.rect.height * 1000) / 10;
|
||||||
return `区域 ${region.index}(${region.colorName},编号 ${region.index},原图位置 x=${left}%,y=${top}%,w=${width}%,h=${height}%):${region.prompt}`;
|
const referenceNote =
|
||||||
|
region.referenceImages.length > 0
|
||||||
|
? `;该区域有 ${region.referenceImages.length} 张专属参考图,依次为后续输入图片中的“区域 ${region.index} 参考图”,只可用于该区域`
|
||||||
|
: "";
|
||||||
|
return `区域 ${region.index}(${region.colorName},编号 ${region.index},原图位置 x=${left}%,y=${top}%,w=${width}%,h=${height}%${referenceNote}):${region.prompt}`;
|
||||||
})
|
})
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
const referenceLines = regions
|
||||||
|
.flatMap((region) =>
|
||||||
|
region.referenceImages.map(
|
||||||
|
(reference, referenceIndex) =>
|
||||||
|
`区域 ${region.index} 参考图 ${referenceIndex + 1}:${reference.name || "未命名参考图"},只能影响区域 ${region.index}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
return [
|
return [
|
||||||
"输入图片1是原始干净图,也是最终输出必须继承的唯一画面基底。",
|
"输入图片1是原始干净图,也是最终输出必须继承的唯一画面基底。",
|
||||||
"输入图片2是同一张图的彩色标注说明图,只用于理解编辑区域位置和区域提示词,不是画面参考。",
|
"输入图片2是同一张图的彩色标注说明图,只用于理解编辑区域位置和区域提示词,不是画面参考。",
|
||||||
|
"输入图片3及之后如存在,是各区域专属参考图;专属参考图只服务于对应区域,不得改变其它区域或整张图的主体身份。",
|
||||||
"请不要复刻、保留、描摹或生成输入图片2里的任何彩色框、编号圆点、提示词气泡、边框线、半透明色块或标注文字。",
|
"请不要复刻、保留、描摹或生成输入图片2里的任何彩色框、编号圆点、提示词气泡、边框线、半透明色块或标注文字。",
|
||||||
"如果输入图片2的标注元素与输入图片1冲突,必须完全忽略标注元素,只保留输入图片1的真实画面内容。",
|
"如果输入图片2的标注元素与输入图片1冲突,必须完全忽略标注元素,只保留输入图片1的真实画面内容。",
|
||||||
"",
|
"",
|
||||||
"请严格根据每个区域下方的提示词,只在对应彩色框附近完成局部编辑:",
|
"请严格根据每个区域下方的提示词,只在对应彩色框附近完成局部编辑:",
|
||||||
regionLines,
|
regionLines,
|
||||||
|
referenceLines ? `\n区域专属参考图说明:\n${referenceLines}` : "",
|
||||||
"",
|
"",
|
||||||
"最终输出必须是干净照片:不得出现任何彩色框、蓝框、绿框、红框、矩形边框、编号、提示词气泡、标注文字或 UI 元素。",
|
"最终输出必须是干净照片:不得出现任何彩色框、蓝框、绿框、红框、矩形边框、编号、提示词气泡、标注文字或 UI 元素。",
|
||||||
"未被彩色框标注的区域必须尽量保持原图不变。",
|
"未被彩色框标注的区域必须尽量保持原图不变。",
|
||||||
@ -368,6 +431,9 @@ export async function POST(request: NextRequest) {
|
|||||||
toArrayBuffer(annotatedBuffer)
|
toArrayBuffer(annotatedBuffer)
|
||||||
);
|
);
|
||||||
const prompt = buildLocalEditPrompt(regions);
|
const prompt = buildLocalEditPrompt(regions);
|
||||||
|
const regionReferenceImageUrls = regions.flatMap((region) =>
|
||||||
|
region.referenceImages.map((reference) => reference.url)
|
||||||
|
);
|
||||||
|
|
||||||
const imageRequest: CanvasGenerateImageRequest = {
|
const imageRequest: CanvasGenerateImageRequest = {
|
||||||
aspectRatio:
|
aspectRatio:
|
||||||
@ -378,6 +444,7 @@ export async function POST(request: NextRequest) {
|
|||||||
debugLabel: "canvas-local-edit",
|
debugLabel: "canvas-local-edit",
|
||||||
debugMeta: {
|
debugMeta: {
|
||||||
annotatedReferenceUrl: annotatedUpload.url,
|
annotatedReferenceUrl: annotatedUpload.url,
|
||||||
|
regionReferenceImageCount: regionReferenceImageUrls.length,
|
||||||
regionCount: regions.length,
|
regionCount: regions.length,
|
||||||
sourceNodeId: body.sourceNodeId || null,
|
sourceNodeId: body.sourceNodeId || null,
|
||||||
},
|
},
|
||||||
@ -394,7 +461,11 @@ export async function POST(request: NextRequest) {
|
|||||||
"彩色框、蓝色边框、绿色边框、红色边框、矩形框、编号圆点、提示词气泡、标注文字、UI 控件、半透明色块、选区边框",
|
"彩色框、蓝色边框、绿色边框、红色边框、矩形框、编号圆点、提示词气泡、标注文字、UI 控件、半透明色块、选区边框",
|
||||||
prompt,
|
prompt,
|
||||||
promptExtend: false,
|
promptExtend: false,
|
||||||
referenceImages: [sourceImageUrl, annotatedUpload.url],
|
referenceImages: [
|
||||||
|
sourceImageUrl,
|
||||||
|
annotatedUpload.url,
|
||||||
|
...regionReferenceImageUrls,
|
||||||
|
],
|
||||||
ownerKey: session.user.id,
|
ownerKey: session.user.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -248,12 +248,21 @@ interface BoxCutoutPoint {
|
|||||||
y: number;
|
y: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LocalEditReferenceImage {
|
||||||
|
id: string;
|
||||||
|
url: string;
|
||||||
|
name?: string;
|
||||||
|
source?: "upload" | "canvas";
|
||||||
|
createdAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface LocalEditRegion {
|
interface LocalEditRegion {
|
||||||
id: string;
|
id: string;
|
||||||
index: number;
|
index: number;
|
||||||
color: string;
|
color: string;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
rect: NormalizedCropRect;
|
rect: NormalizedCropRect;
|
||||||
|
referenceImages?: LocalEditReferenceImage[];
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,6 +270,7 @@ const BOX_CUTOUT_DEFAULT_PADDING_RATIO = 0.08;
|
|||||||
const BOX_CUTOUT_MIN_EDGE_PX = 24;
|
const BOX_CUTOUT_MIN_EDGE_PX = 24;
|
||||||
const LOCAL_EDIT_METADATA_KEY = "localEditRegions";
|
const LOCAL_EDIT_METADATA_KEY = "localEditRegions";
|
||||||
const LOCAL_EDIT_MAX_REGIONS = 8;
|
const LOCAL_EDIT_MAX_REGIONS = 8;
|
||||||
|
const LOCAL_EDIT_MAX_REFERENCE_IMAGES_PER_REGION = 3;
|
||||||
const LOCAL_EDIT_MIN_EDGE_PX = BOX_CUTOUT_MIN_EDGE_PX;
|
const LOCAL_EDIT_MIN_EDGE_PX = BOX_CUTOUT_MIN_EDGE_PX;
|
||||||
const LOCAL_EDIT_POLL_INTERVAL_MS = 2500;
|
const LOCAL_EDIT_POLL_INTERVAL_MS = 2500;
|
||||||
const LOCAL_EDIT_MAX_POLL_ATTEMPTS = 180;
|
const LOCAL_EDIT_MAX_POLL_ATTEMPTS = 180;
|
||||||
@ -875,6 +885,8 @@ const normalizeTextEditItems = (
|
|||||||
|
|
||||||
const createLocalEditRegionId = () =>
|
const createLocalEditRegionId = () =>
|
||||||
`local-edit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
`local-edit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
const createLocalEditReferenceImageId = () =>
|
||||||
|
`local-edit-ref-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
|
||||||
const normalizeLocalEditRect = (value: unknown): NormalizedCropRect | null => {
|
const normalizeLocalEditRect = (value: unknown): NormalizedCropRect | null => {
|
||||||
if (!value || typeof value !== "object") return null;
|
if (!value || typeof value !== "object") return null;
|
||||||
@ -896,6 +908,40 @@ const normalizeLocalEditRect = (value: unknown): NormalizedCropRect | null => {
|
|||||||
return { x, y, width, height };
|
return { x, y, width, height };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const normalizeLocalEditReferenceImages = (
|
||||||
|
value: unknown
|
||||||
|
): LocalEditReferenceImage[] => {
|
||||||
|
if (!Array.isArray(value)) return [];
|
||||||
|
const references: LocalEditReferenceImage[] = [];
|
||||||
|
for (const item of value) {
|
||||||
|
if (!item || typeof item !== "object") continue;
|
||||||
|
const record = item as Record<string, unknown>;
|
||||||
|
const url = typeof record.url === "string" ? record.url.trim() : "";
|
||||||
|
if (!isHttpImageUrl(url)) continue;
|
||||||
|
const name =
|
||||||
|
typeof record.name === "string" && record.name.trim()
|
||||||
|
? record.name.trim().slice(0, 80)
|
||||||
|
: "";
|
||||||
|
references.push({
|
||||||
|
id:
|
||||||
|
typeof record.id === "string" && record.id.trim()
|
||||||
|
? record.id.trim()
|
||||||
|
: createLocalEditReferenceImageId(),
|
||||||
|
url,
|
||||||
|
...(name ? { name } : {}),
|
||||||
|
source: record.source === "canvas" ? "canvas" : "upload",
|
||||||
|
createdAt:
|
||||||
|
typeof record.createdAt === "number" && Number.isFinite(record.createdAt)
|
||||||
|
? record.createdAt
|
||||||
|
: Date.now(),
|
||||||
|
});
|
||||||
|
if (references.length >= LOCAL_EDIT_MAX_REFERENCE_IMAGES_PER_REGION) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return references;
|
||||||
|
};
|
||||||
|
|
||||||
const reindexLocalEditRegions = (
|
const reindexLocalEditRegions = (
|
||||||
regions: LocalEditRegion[]
|
regions: LocalEditRegion[]
|
||||||
): LocalEditRegion[] =>
|
): LocalEditRegion[] =>
|
||||||
@ -907,38 +953,36 @@ const reindexLocalEditRegions = (
|
|||||||
|
|
||||||
const normalizeLocalEditRegions = (value: unknown): LocalEditRegion[] => {
|
const normalizeLocalEditRegions = (value: unknown): LocalEditRegion[] => {
|
||||||
if (!Array.isArray(value)) return [];
|
if (!Array.isArray(value)) return [];
|
||||||
return reindexLocalEditRegions(
|
const regions: LocalEditRegion[] = [];
|
||||||
value
|
value.forEach((item, index) => {
|
||||||
.map((item, index) => {
|
if (!item || typeof item !== "object") return;
|
||||||
if (!item || typeof item !== "object") return null;
|
const record = item as Record<string, unknown>;
|
||||||
const record = item as Record<string, unknown>;
|
const rect = normalizeLocalEditRect(record.rect);
|
||||||
const rect = normalizeLocalEditRect(record.rect);
|
if (!rect) return;
|
||||||
if (!rect) return null;
|
regions.push({
|
||||||
return {
|
id:
|
||||||
id:
|
typeof record.id === "string" && record.id.trim()
|
||||||
typeof record.id === "string" && record.id.trim()
|
? record.id.trim()
|
||||||
? record.id.trim()
|
: createLocalEditRegionId(),
|
||||||
: createLocalEditRegionId(),
|
index:
|
||||||
index:
|
typeof record.index === "number" && Number.isFinite(record.index)
|
||||||
typeof record.index === "number" && Number.isFinite(record.index)
|
? record.index
|
||||||
? record.index
|
: index + 1,
|
||||||
: index + 1,
|
color:
|
||||||
color:
|
typeof record.color === "string" && record.color.trim()
|
||||||
typeof record.color === "string" && record.color.trim()
|
? record.color.trim()
|
||||||
? record.color.trim()
|
: LOCAL_EDIT_COLORS[index % LOCAL_EDIT_COLORS.length],
|
||||||
: LOCAL_EDIT_COLORS[index % LOCAL_EDIT_COLORS.length],
|
prompt:
|
||||||
prompt:
|
typeof record.prompt === "string" ? record.prompt.trim() : "",
|
||||||
typeof record.prompt === "string" ? record.prompt.trim() : "",
|
rect,
|
||||||
rect,
|
referenceImages: normalizeLocalEditReferenceImages(record.referenceImages),
|
||||||
createdAt:
|
createdAt:
|
||||||
typeof record.createdAt === "number" && Number.isFinite(record.createdAt)
|
typeof record.createdAt === "number" && Number.isFinite(record.createdAt)
|
||||||
? record.createdAt
|
? record.createdAt
|
||||||
: Date.now(),
|
: Date.now(),
|
||||||
} satisfies LocalEditRegion;
|
});
|
||||||
})
|
});
|
||||||
.filter((item): item is LocalEditRegion => !!item)
|
return reindexLocalEditRegions(regions.slice(0, LOCAL_EDIT_MAX_REGIONS));
|
||||||
.slice(0, LOCAL_EDIT_MAX_REGIONS)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLocalEditRegionsFromMetadata = (
|
const getLocalEditRegionsFromMetadata = (
|
||||||
@ -1838,6 +1882,10 @@ export function ImageNode({
|
|||||||
const [editingLocalEditRegionId, setEditingLocalEditRegionId] =
|
const [editingLocalEditRegionId, setEditingLocalEditRegionId] =
|
||||||
useState<string | null>(null);
|
useState<string | null>(null);
|
||||||
const [isSubmittingLocalEdit, setIsSubmittingLocalEdit] = useState(false);
|
const [isSubmittingLocalEdit, setIsSubmittingLocalEdit] = useState(false);
|
||||||
|
const [
|
||||||
|
uploadingLocalEditReferenceRegionId,
|
||||||
|
setUploadingLocalEditReferenceRegionId,
|
||||||
|
] = useState<string | null>(null);
|
||||||
const anglePreviewDragRef = useRef<AnglePreviewDragState | null>(null);
|
const anglePreviewDragRef = useRef<AnglePreviewDragState | null>(null);
|
||||||
const activeImageAdjustments = imageAdjustPanelOpen
|
const activeImageAdjustments = imageAdjustPanelOpen
|
||||||
? imageAdjustmentsDraft
|
? imageAdjustmentsDraft
|
||||||
@ -2384,9 +2432,13 @@ export function ImageNode({
|
|||||||
const width = Number(style.width || 0);
|
const width = Number(style.width || 0);
|
||||||
const height = Number(style.height || 0);
|
const height = Number(style.height || 0);
|
||||||
const imageBottom = boxCutoutMetrics.offsetY + boxCutoutMetrics.renderedHeight;
|
const imageBottom = boxCutoutMetrics.offsetY + boxCutoutMetrics.renderedHeight;
|
||||||
const placeAbove = top + height + 58 > imageBottom;
|
const referenceCount = region.referenceImages?.length || 0;
|
||||||
const promptTop = placeAbove ? top - 46 : top + height + 8;
|
const promptStackHeight = referenceCount > 0 ? 82 : 42;
|
||||||
const promptWidth = Math.min(Math.max(width, 176), 300);
|
const placeAbove = top + height + promptStackHeight + 14 > imageBottom;
|
||||||
|
const promptTop = placeAbove
|
||||||
|
? top - promptStackHeight - 8
|
||||||
|
: top + height + 8;
|
||||||
|
const promptWidth = Math.min(Math.max(width, 236), 360);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
region,
|
region,
|
||||||
@ -2467,6 +2519,7 @@ export function ImageNode({
|
|||||||
setLocalEditDragEnd(null);
|
setLocalEditDragEnd(null);
|
||||||
setLocalEditRegionInteraction(null);
|
setLocalEditRegionInteraction(null);
|
||||||
setEditingLocalEditRegionId(null);
|
setEditingLocalEditRegionId(null);
|
||||||
|
setUploadingLocalEditReferenceRegionId(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const focusLocalEditPromptInput = useCallback((regionId: string) => {
|
const focusLocalEditPromptInput = useCallback((regionId: string) => {
|
||||||
@ -3402,6 +3455,7 @@ export function ImageNode({
|
|||||||
LOCAL_EDIT_COLORS[localEditRegions.length % LOCAL_EDIT_COLORS.length],
|
LOCAL_EDIT_COLORS[localEditRegions.length % LOCAL_EDIT_COLORS.length],
|
||||||
prompt: "",
|
prompt: "",
|
||||||
rect: nextRect,
|
rect: nextRect,
|
||||||
|
referenceImages: [],
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
};
|
};
|
||||||
commitLocalEditRegions([...localEditRegions, nextRegion]);
|
commitLocalEditRegions([...localEditRegions, nextRegion]);
|
||||||
@ -3618,6 +3672,95 @@ export function ImageNode({
|
|||||||
[commitLocalEditRegions, editingLocalEditRegionId]
|
[commitLocalEditRegions, editingLocalEditRegionId]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleLocalEditReferenceUpload = useCallback(
|
||||||
|
async (regionId: string, fileList: FileList | null) => {
|
||||||
|
const files = Array.from(fileList || []).filter((file) =>
|
||||||
|
file.type.startsWith("image/")
|
||||||
|
);
|
||||||
|
if (files.length === 0) return;
|
||||||
|
|
||||||
|
const currentRegion = localEditRegionsRef.current.find(
|
||||||
|
(region) => region.id === regionId
|
||||||
|
);
|
||||||
|
if (!currentRegion) return;
|
||||||
|
|
||||||
|
const currentReferences = currentRegion.referenceImages || [];
|
||||||
|
const remainingSlots =
|
||||||
|
LOCAL_EDIT_MAX_REFERENCE_IMAGES_PER_REGION - currentReferences.length;
|
||||||
|
if (remainingSlots <= 0) {
|
||||||
|
toast.info(
|
||||||
|
`每个局部编辑区域最多添加 ${LOCAL_EDIT_MAX_REFERENCE_IMAGES_PER_REGION} 张参考图`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedFiles = files.slice(0, remainingSlots);
|
||||||
|
if (files.length > selectedFiles.length) {
|
||||||
|
toast.info(
|
||||||
|
`已自动保留前 ${LOCAL_EDIT_MAX_REFERENCE_IMAGES_PER_REGION} 张参考图`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setUploadingLocalEditReferenceRegionId(regionId);
|
||||||
|
try {
|
||||||
|
const uploadedReferences = await Promise.all(
|
||||||
|
selectedFiles.map(async (file) => {
|
||||||
|
const uploaded = await uploadChatAttachmentBlob(file, file.name);
|
||||||
|
return {
|
||||||
|
id: createLocalEditReferenceImageId(),
|
||||||
|
url: uploaded.url,
|
||||||
|
name: file.name,
|
||||||
|
source: "upload" as const,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
} satisfies LocalEditReferenceImage;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
commitLocalEditRegions(
|
||||||
|
localEditRegionsRef.current.map((region) =>
|
||||||
|
region.id === regionId
|
||||||
|
? {
|
||||||
|
...region,
|
||||||
|
referenceImages: [
|
||||||
|
...(region.referenceImages || []),
|
||||||
|
...uploadedReferences,
|
||||||
|
].slice(0, LOCAL_EDIT_MAX_REFERENCE_IMAGES_PER_REGION),
|
||||||
|
}
|
||||||
|
: region
|
||||||
|
)
|
||||||
|
);
|
||||||
|
toast.success(
|
||||||
|
uploadedReferences.length > 1
|
||||||
|
? `已添加 ${uploadedReferences.length} 张区域参考图`
|
||||||
|
: "已添加区域参考图"
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error instanceof Error ? error.message : "参考图上传失败");
|
||||||
|
} finally {
|
||||||
|
setUploadingLocalEditReferenceRegionId(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[commitLocalEditRegions]
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeLocalEditReferenceImage = useCallback(
|
||||||
|
(regionId: string, referenceId: string) => {
|
||||||
|
commitLocalEditRegions(
|
||||||
|
localEditRegionsRef.current.map((region) =>
|
||||||
|
region.id === regionId
|
||||||
|
? {
|
||||||
|
...region,
|
||||||
|
referenceImages: (region.referenceImages || []).filter(
|
||||||
|
(reference) => reference.id !== referenceId
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: region
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[commitLocalEditRegions]
|
||||||
|
);
|
||||||
|
|
||||||
const clearLocalEditRegions = useCallback(() => {
|
const clearLocalEditRegions = useCallback(() => {
|
||||||
localEditPromptDraftRefs.current = {};
|
localEditPromptDraftRefs.current = {};
|
||||||
commitLocalEditRegions([]);
|
commitLocalEditRegions([]);
|
||||||
@ -3696,6 +3839,12 @@ export function ImageNode({
|
|||||||
color: region.color,
|
color: region.color,
|
||||||
prompt: region.prompt,
|
prompt: region.prompt,
|
||||||
rect: region.rect,
|
rect: region.rect,
|
||||||
|
referenceImages: (region.referenceImages || []).map((reference) => ({
|
||||||
|
id: reference.id,
|
||||||
|
url: reference.url,
|
||||||
|
name: reference.name,
|
||||||
|
source: reference.source,
|
||||||
|
})),
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -8366,7 +8515,7 @@ export function ImageNode({
|
|||||||
localEditPromptPortalOverlays.map(({ region, isEditing, style }) =>
|
localEditPromptPortalOverlays.map(({ region, isEditing, style }) =>
|
||||||
createPortal(
|
createPortal(
|
||||||
<div
|
<div
|
||||||
className="nodrag nopan nowheel fixed z-[10000] flex items-center gap-1.5"
|
className="nodrag nopan nowheel fixed z-[10000] flex flex-col items-stretch gap-1.5"
|
||||||
data-canvas-no-drag="true"
|
data-canvas-no-drag="true"
|
||||||
data-local-edit-control="true"
|
data-local-edit-control="true"
|
||||||
key={region.id}
|
key={region.id}
|
||||||
@ -8378,110 +8527,190 @@ export function ImageNode({
|
|||||||
onPointerDownCapture={(event) => event.stopPropagation()}
|
onPointerDownCapture={(event) => event.stopPropagation()}
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
{isEditing ? (
|
<div className="flex items-center gap-1.5">
|
||||||
<input
|
{isEditing ? (
|
||||||
autoFocus={editingLocalEditRegionId === region.id}
|
<input
|
||||||
className="nodrag nopan nowheel h-9 min-w-0 flex-1 rounded-full border border-slate-300 bg-white/95 px-3 text-[12px] font-medium text-slate-950 caret-slate-950 shadow-[0_10px_24px_rgba(15,23,42,0.14)] outline-none ring-2 ring-transparent transition placeholder:text-slate-400 focus:border-slate-950 focus:ring-slate-950/15 dark:border-white/20 dark:bg-slate-950/88 dark:text-white dark:caret-white dark:placeholder:text-white/42 dark:focus:border-white/40 dark:focus:ring-white/18"
|
autoFocus={editingLocalEditRegionId === region.id}
|
||||||
data-canvas-no-drag="true"
|
className="nodrag nopan nowheel h-9 min-w-0 flex-1 rounded-full border border-slate-300 bg-white/95 px-3 text-[12px] font-medium text-slate-950 caret-slate-950 shadow-[0_10px_24px_rgba(15,23,42,0.14)] outline-none ring-2 ring-transparent transition placeholder:text-slate-400 focus:border-slate-950 focus:ring-slate-950/15 dark:border-white/20 dark:bg-slate-950/88 dark:text-white dark:caret-white dark:placeholder:text-white/42 dark:focus:border-white/40 dark:focus:ring-white/18"
|
||||||
data-local-edit-control="true"
|
data-canvas-no-drag="true"
|
||||||
defaultValue={
|
data-local-edit-control="true"
|
||||||
localEditPromptDraftRefs.current[region.id] ?? region.prompt
|
defaultValue={
|
||||||
}
|
localEditPromptDraftRefs.current[region.id] ?? region.prompt
|
||||||
onBlur={(event) => {
|
|
||||||
const nextPrompt = event.currentTarget.value;
|
|
||||||
localEditPromptDraftRefs.current[region.id] = nextPrompt;
|
|
||||||
persistLocalEditRegionPrompt(region.id, nextPrompt);
|
|
||||||
if (isLocalEditControlTarget(event.relatedTarget)) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
window.setTimeout(() => {
|
onBlur={(event) => {
|
||||||
if (isLocalEditControlTarget(document.activeElement)) {
|
const nextPrompt = event.currentTarget.value;
|
||||||
|
localEditPromptDraftRefs.current[region.id] = nextPrompt;
|
||||||
|
persistLocalEditRegionPrompt(region.id, nextPrompt);
|
||||||
|
if (isLocalEditControlTarget(event.relatedTarget)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setEditingLocalEditRegionId((current) => {
|
window.setTimeout(() => {
|
||||||
if (current === region.id) {
|
if (isLocalEditControlTarget(document.activeElement)) {
|
||||||
focusLocalEditPromptInput(region.id);
|
return;
|
||||||
}
|
}
|
||||||
return current;
|
setEditingLocalEditRegionId((current) => {
|
||||||
});
|
if (current === region.id) {
|
||||||
}, 80);
|
focusLocalEditPromptInput(region.id);
|
||||||
}}
|
}
|
||||||
onClick={(event) => {
|
return current;
|
||||||
event.nativeEvent.stopImmediatePropagation?.();
|
});
|
||||||
event.stopPropagation();
|
}, 80);
|
||||||
event.currentTarget.focus({ preventScroll: true });
|
}}
|
||||||
}}
|
onClick={(event) => {
|
||||||
onFocus={(event) => {
|
event.nativeEvent.stopImmediatePropagation?.();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
setEditingLocalEditRegionId((current) =>
|
event.currentTarget.focus({ preventScroll: true });
|
||||||
current === region.id ? current : region.id
|
}}
|
||||||
);
|
onFocus={(event) => {
|
||||||
}}
|
event.stopPropagation();
|
||||||
onInput={(event) => {
|
setEditingLocalEditRegionId((current) =>
|
||||||
localEditPromptDraftRefs.current[region.id] =
|
current === region.id ? current : region.id
|
||||||
event.currentTarget.value;
|
);
|
||||||
}}
|
}}
|
||||||
|
onInput={(event) => {
|
||||||
|
localEditPromptDraftRefs.current[region.id] =
|
||||||
|
event.currentTarget.value;
|
||||||
|
}}
|
||||||
|
onChange={(event) => {
|
||||||
|
localEditPromptDraftRefs.current[region.id] =
|
||||||
|
event.currentTarget.value;
|
||||||
|
}}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
event.nativeEvent.stopImmediatePropagation?.();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
persistLocalEditRegionPrompt(
|
||||||
|
region.id,
|
||||||
|
event.currentTarget.value
|
||||||
|
);
|
||||||
|
setEditingLocalEditRegionId(null);
|
||||||
|
}
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
event.preventDefault();
|
||||||
|
persistLocalEditRegionPrompt(
|
||||||
|
region.id,
|
||||||
|
event.currentTarget.value
|
||||||
|
);
|
||||||
|
setEditingLocalEditRegionId(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onKeyUp={(event) => {
|
||||||
|
event.nativeEvent.stopImmediatePropagation?.();
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
onMouseDown={(event) => {
|
||||||
|
event.nativeEvent.stopImmediatePropagation?.();
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
onMouseDownCapture={(event) => {
|
||||||
|
event.nativeEvent.stopImmediatePropagation?.();
|
||||||
|
event.stopPropagation();
|
||||||
|
event.currentTarget.focus({ preventScroll: true });
|
||||||
|
}}
|
||||||
|
onPointerDown={(event) => {
|
||||||
|
event.nativeEvent.stopImmediatePropagation?.();
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
onPointerDownCapture={(event) => {
|
||||||
|
event.nativeEvent.stopImmediatePropagation?.();
|
||||||
|
event.stopPropagation();
|
||||||
|
event.currentTarget.focus({ preventScroll: true });
|
||||||
|
}}
|
||||||
|
placeholder={`区域 ${region.index} 提示词`}
|
||||||
|
ref={(input) => {
|
||||||
|
localEditPromptInputRefs.current[region.id] = input;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className="nodrag nopan nowheel h-9 min-w-0 flex-1 truncate rounded-full border border-slate-200 bg-white/95 px-3 text-left text-[12px] font-semibold text-slate-900 shadow-[0_10px_24px_rgba(15,23,42,0.14)] hover:bg-slate-50 dark:border-white/18 dark:bg-slate-950/82 dark:text-white dark:shadow-[0_10px_24px_rgba(0,0,0,0.24)] dark:hover:bg-slate-900"
|
||||||
|
data-canvas-no-drag="true"
|
||||||
|
data-local-edit-control="true"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.nativeEvent.stopImmediatePropagation?.();
|
||||||
|
event.stopPropagation();
|
||||||
|
setEditingLocalEditRegionId(region.id);
|
||||||
|
}}
|
||||||
|
onMouseDown={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.nativeEvent.stopImmediatePropagation?.();
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
onPointerDown={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.nativeEvent.stopImmediatePropagation?.();
|
||||||
|
event.stopPropagation();
|
||||||
|
setEditingLocalEditRegionId(region.id);
|
||||||
|
}}
|
||||||
|
title={region.prompt}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{region.index}. {region.prompt}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
accept="image/jpeg,image/png,image/webp"
|
||||||
|
className="hidden"
|
||||||
|
data-canvas-no-drag="true"
|
||||||
|
data-local-edit-control="true"
|
||||||
|
id={`local-edit-reference-upload-${region.id}`}
|
||||||
|
multiple
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
localEditPromptDraftRefs.current[region.id] =
|
void handleLocalEditReferenceUpload(
|
||||||
event.currentTarget.value;
|
region.id,
|
||||||
}}
|
event.currentTarget.files
|
||||||
onKeyDown={(event) => {
|
);
|
||||||
event.nativeEvent.stopImmediatePropagation?.();
|
event.currentTarget.value = "";
|
||||||
event.stopPropagation();
|
|
||||||
if (event.key === "Enter") {
|
|
||||||
event.preventDefault();
|
|
||||||
persistLocalEditRegionPrompt(
|
|
||||||
region.id,
|
|
||||||
event.currentTarget.value
|
|
||||||
);
|
|
||||||
setEditingLocalEditRegionId(null);
|
|
||||||
}
|
|
||||||
if (event.key === "Escape") {
|
|
||||||
event.preventDefault();
|
|
||||||
persistLocalEditRegionPrompt(
|
|
||||||
region.id,
|
|
||||||
event.currentTarget.value
|
|
||||||
);
|
|
||||||
setEditingLocalEditRegionId(null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onKeyUp={(event) => {
|
|
||||||
event.nativeEvent.stopImmediatePropagation?.();
|
|
||||||
event.stopPropagation();
|
|
||||||
}}
|
|
||||||
onMouseDown={(event) => {
|
|
||||||
event.nativeEvent.stopImmediatePropagation?.();
|
|
||||||
event.stopPropagation();
|
|
||||||
}}
|
|
||||||
onMouseDownCapture={(event) => {
|
|
||||||
event.nativeEvent.stopImmediatePropagation?.();
|
|
||||||
event.stopPropagation();
|
|
||||||
event.currentTarget.focus({ preventScroll: true });
|
|
||||||
}}
|
|
||||||
onPointerDown={(event) => {
|
|
||||||
event.nativeEvent.stopImmediatePropagation?.();
|
|
||||||
event.stopPropagation();
|
|
||||||
}}
|
|
||||||
onPointerDownCapture={(event) => {
|
|
||||||
event.nativeEvent.stopImmediatePropagation?.();
|
|
||||||
event.stopPropagation();
|
|
||||||
event.currentTarget.focus({ preventScroll: true });
|
|
||||||
}}
|
|
||||||
placeholder={`区域 ${region.index} 提示词`}
|
|
||||||
ref={(input) => {
|
|
||||||
localEditPromptInputRefs.current[region.id] = input;
|
|
||||||
}}
|
}}
|
||||||
|
type="file"
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<button
|
<button
|
||||||
className="nodrag nopan nowheel h-9 min-w-0 flex-1 truncate rounded-full border border-slate-200 bg-white/95 px-3 text-left text-[12px] font-semibold text-slate-900 shadow-[0_10px_24px_rgba(15,23,42,0.14)] hover:bg-slate-50 dark:border-white/18 dark:bg-slate-950/82 dark:text-white dark:shadow-[0_10px_24px_rgba(0,0,0,0.24)] dark:hover:bg-slate-900"
|
className="nodrag nopan nowheel flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-slate-200 bg-white/95 text-slate-600 shadow-[0_10px_24px_rgba(15,23,42,0.12)] hover:bg-slate-950 hover:text-white disabled:cursor-not-allowed disabled:opacity-50 dark:border-white/18 dark:bg-slate-950/82 dark:text-white/72 dark:shadow-[0_10px_24px_rgba(0,0,0,0.24)] dark:hover:bg-white dark:hover:text-slate-950"
|
||||||
data-canvas-no-drag="true"
|
data-canvas-no-drag="true"
|
||||||
data-local-edit-control="true"
|
data-local-edit-control="true"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.nativeEvent.stopImmediatePropagation?.();
|
event.nativeEvent.stopImmediatePropagation?.();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
setEditingLocalEditRegionId(region.id);
|
document
|
||||||
|
.getElementById(`local-edit-reference-upload-${region.id}`)
|
||||||
|
?.click();
|
||||||
|
}}
|
||||||
|
disabled={
|
||||||
|
uploadingLocalEditReferenceRegionId === region.id ||
|
||||||
|
(region.referenceImages?.length || 0) >=
|
||||||
|
LOCAL_EDIT_MAX_REFERENCE_IMAGES_PER_REGION
|
||||||
|
}
|
||||||
|
onMouseDown={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.nativeEvent.stopImmediatePropagation?.();
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
onPointerDown={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.nativeEvent.stopImmediatePropagation?.();
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
title="添加区域参考图"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{uploadingLocalEditReferenceRegionId === region.id ? (
|
||||||
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<ImagePlus className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="nodrag nopan nowheel flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-slate-200 bg-white/95 text-slate-500 shadow-[0_10px_24px_rgba(15,23,42,0.12)] hover:bg-red-500 hover:text-white dark:border-white/18 dark:bg-slate-950/82 dark:text-white/72 dark:shadow-[0_10px_24px_rgba(0,0,0,0.24)]"
|
||||||
|
data-canvas-no-drag="true"
|
||||||
|
data-local-edit-control="true"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.nativeEvent.stopImmediatePropagation?.();
|
||||||
|
event.stopPropagation();
|
||||||
|
deleteLocalEditRegion(region.id);
|
||||||
}}
|
}}
|
||||||
onMouseDown={(event) => {
|
onMouseDown={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -8492,40 +8721,50 @@ export function ImageNode({
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.nativeEvent.stopImmediatePropagation?.();
|
event.nativeEvent.stopImmediatePropagation?.();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
setEditingLocalEditRegionId(region.id);
|
deleteLocalEditRegion(region.id);
|
||||||
}}
|
}}
|
||||||
title={region.prompt}
|
title="删除区域"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{region.index}. {region.prompt}
|
<X className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
</div>
|
||||||
<button
|
{region.referenceImages?.length ? (
|
||||||
className="nodrag nopan nowheel flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-slate-200 bg-white/95 text-slate-500 shadow-[0_10px_24px_rgba(15,23,42,0.12)] hover:bg-red-500 hover:text-white dark:border-white/18 dark:bg-slate-950/82 dark:text-white/72 dark:shadow-[0_10px_24px_rgba(0,0,0,0.24)]"
|
<div className="flex max-w-full items-center gap-1 overflow-hidden rounded-2xl border border-slate-200 bg-white/92 px-1.5 py-1 shadow-[0_8px_20px_rgba(15,23,42,0.1)] dark:border-white/12 dark:bg-slate-950/80">
|
||||||
data-canvas-no-drag="true"
|
<span className="shrink-0 px-1 text-[10px] font-semibold text-slate-500 dark:text-white/52">
|
||||||
data-local-edit-control="true"
|
参考
|
||||||
onClick={(event) => {
|
</span>
|
||||||
event.preventDefault();
|
{region.referenceImages.map((reference) => (
|
||||||
event.nativeEvent.stopImmediatePropagation?.();
|
<div
|
||||||
event.stopPropagation();
|
className="group relative h-8 w-8 shrink-0 overflow-hidden rounded-lg border border-slate-200 bg-slate-100 dark:border-white/12 dark:bg-slate-800"
|
||||||
deleteLocalEditRegion(region.id);
|
key={reference.id}
|
||||||
}}
|
title={reference.name || "区域参考图"}
|
||||||
onMouseDown={(event) => {
|
>
|
||||||
event.preventDefault();
|
<img
|
||||||
event.nativeEvent.stopImmediatePropagation?.();
|
alt={reference.name || "区域参考图"}
|
||||||
event.stopPropagation();
|
className="h-full w-full object-cover"
|
||||||
}}
|
draggable={false}
|
||||||
onPointerDown={(event) => {
|
src={reference.url}
|
||||||
event.preventDefault();
|
/>
|
||||||
event.nativeEvent.stopImmediatePropagation?.();
|
<button
|
||||||
event.stopPropagation();
|
className="absolute inset-0 hidden items-center justify-center bg-black/52 text-white group-hover:flex"
|
||||||
deleteLocalEditRegion(region.id);
|
data-canvas-no-drag="true"
|
||||||
}}
|
data-local-edit-control="true"
|
||||||
title="删除区域"
|
onClick={(event) => {
|
||||||
type="button"
|
event.preventDefault();
|
||||||
>
|
event.nativeEvent.stopImmediatePropagation?.();
|
||||||
<X className="h-3.5 w-3.5" />
|
event.stopPropagation();
|
||||||
</button>
|
removeLocalEditReferenceImage(region.id, reference.id);
|
||||||
|
}}
|
||||||
|
title="移除参考图"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<X className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>,
|
</div>,
|
||||||
document.body
|
document.body
|
||||||
)
|
)
|
||||||
|
|||||||
@ -307,6 +307,12 @@ export const imageGenerationService = {
|
|||||||
color: string;
|
color: string;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
rect: NormalizedCropRect;
|
rect: NormalizedCropRect;
|
||||||
|
referenceImages?: Array<{
|
||||||
|
id?: string;
|
||||||
|
url: string;
|
||||||
|
name?: string;
|
||||||
|
source?: "upload" | "canvas";
|
||||||
|
}>;
|
||||||
}>;
|
}>;
|
||||||
model?: string;
|
model?: string;
|
||||||
imageSize?: string;
|
imageSize?: string;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user