修复了一点bug,然后增加了图层拆分更多的功能
This commit is contained in:
1 parent
1305a9cdd8
commit
ce8372e878
31 files changed
+2567
-391
No files matched your search
@@ -0,0 +1,71 @@
|
||||
import type { Asset, GenerationJob } from "@/lib/types";
|
||||
|
||||
export async function waitForJobOutputAssets(job: GenerationJob, signal: AbortSignal) {
|
||||
let currentJob = job;
|
||||
for (let attempt = 0; attempt < 25; attempt += 1) {
|
||||
if (attempt > 0 || currentJob.outputAssetIds.length === 0) {
|
||||
currentJob = await fetchImageJob(job.id, signal);
|
||||
if (["failed", "expired", "cancelled"].includes(currentJob.status)) {
|
||||
throw new Error(currentJob.error?.message || "图层编辑任务未成功完成");
|
||||
}
|
||||
}
|
||||
if (currentJob.outputAssetIds.length) {
|
||||
const outputs = await fetchAssetsByID(currentJob.outputAssetIds, signal);
|
||||
if (outputs.length) return outputs;
|
||||
}
|
||||
await abortableDelay(800, signal);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
async function fetchImageJob(jobId: string, signal: AbortSignal) {
|
||||
const response = await fetch(`/api/generations/image/${encodeURIComponent(jobId)}`, { cache: "no-store", signal });
|
||||
const payload = await readJSON(response);
|
||||
if (!response.ok) throw new Error(payloadError(payload, "刷新图层编辑任务失败"));
|
||||
const job = payload.job as GenerationJob | undefined;
|
||||
if (!job?.id || !Array.isArray(job.outputAssetIds)) throw new Error("图层编辑任务返回数据不完整");
|
||||
return job;
|
||||
}
|
||||
|
||||
async function fetchAssetsByID(ids: string[], signal: AbortSignal) {
|
||||
const assets = await Promise.all(ids.map(async (id) => {
|
||||
const response = await fetch(`/api/assets/${encodeURIComponent(id)}`, { cache: "no-store", signal });
|
||||
const payload = await readJSON(response);
|
||||
if (response.status === 404) return null;
|
||||
if (!response.ok) throw new Error(payloadError(payload, "读取编辑结果素材失败"));
|
||||
return isAsset(payload.asset) ? payload.asset : null;
|
||||
}));
|
||||
return assets.filter((asset): asset is Asset => Boolean(asset));
|
||||
}
|
||||
|
||||
function isAsset(value: unknown): value is Asset {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
const record = value as Record<string, unknown>;
|
||||
return typeof record.id === "string" && typeof record.name === "string" && typeof record.url === "string";
|
||||
}
|
||||
|
||||
async function readJSON(response: Response): Promise<Record<string, unknown>> {
|
||||
return response.json().catch(() => ({})) as Promise<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
function payloadError(payload: Record<string, unknown>, fallback: string) {
|
||||
return typeof payload.error === "string" && payload.error.trim() ? payload.error : fallback;
|
||||
}
|
||||
|
||||
function abortableDelay(milliseconds: number, signal: AbortSignal) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (signal.aborted) {
|
||||
reject(new DOMException("Aborted", "AbortError"));
|
||||
return;
|
||||
}
|
||||
const onAbort = () => {
|
||||
window.clearTimeout(timer);
|
||||
reject(new DOMException("Aborted", "AbortError"));
|
||||
};
|
||||
const timer = window.setTimeout(() => {
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
resolve();
|
||||
}, milliseconds);
|
||||
signal.addEventListener("abort", onAbort, { once: true });
|
||||
});
|
||||
}
|
||||
@@ -2,6 +2,7 @@ export type SeedreamLayerCompositionItem = {
|
||||
url: string;
|
||||
visible: boolean;
|
||||
box: [number, number, number, number];
|
||||
rotation?: number;
|
||||
};
|
||||
|
||||
export async function downloadSeedreamLayerComposition(options: {
|
||||
@@ -14,7 +15,9 @@ export async function downloadSeedreamLayerComposition(options: {
|
||||
|
||||
const decoded: Array<{ item: SeedreamLayerCompositionItem; bitmap: ImageBitmap }> = [];
|
||||
try {
|
||||
for (const item of options.items) decoded.push({ item, bitmap: await fetchBitmap(item.url) });
|
||||
for (const item of options.items) {
|
||||
if (item.visible) decoded.push({ item, bitmap: await fetchBitmap(item.url) });
|
||||
}
|
||||
const reference = decoded[0].bitmap;
|
||||
const width = positiveDimension(options.width) || reference.width;
|
||||
const height = positiveDimension(options.height) || reference.height;
|
||||
@@ -26,15 +29,21 @@ export async function downloadSeedreamLayerComposition(options: {
|
||||
context.clearRect(0, 0, width, height);
|
||||
|
||||
for (const { item, bitmap } of decoded) {
|
||||
if (!item.visible) continue;
|
||||
const [left, top, right, bottom] = item.box;
|
||||
context.drawImage(
|
||||
bitmap,
|
||||
left / 1000 * width,
|
||||
top / 1000 * height,
|
||||
Math.max(1, (right - left) / 1000 * width),
|
||||
Math.max(1, (bottom - top) / 1000 * height)
|
||||
);
|
||||
const x = left / 1000 * width;
|
||||
const y = top / 1000 * height;
|
||||
const itemWidth = Math.max(1, (right - left) / 1000 * width);
|
||||
const itemHeight = Math.max(1, (bottom - top) / 1000 * height);
|
||||
const rotation = Number.isFinite(item.rotation) ? item.rotation || 0 : 0;
|
||||
if (!rotation) {
|
||||
context.drawImage(bitmap, x, y, itemWidth, itemHeight);
|
||||
continue;
|
||||
}
|
||||
context.save();
|
||||
context.translate(x + itemWidth / 2, y + itemHeight / 2);
|
||||
context.rotate(rotation * Math.PI / 180);
|
||||
context.drawImage(bitmap, -itemWidth / 2, -itemHeight / 2, itemWidth, itemHeight);
|
||||
context.restore();
|
||||
}
|
||||
|
||||
const blob = await canvasBlob(canvas, "image/png");
|
||||
|
||||
Reference in new issue
Block a user