Size dropped image nodes to intrinsic aspect ratio

Dropped images landed in fixed 420x280 nodes and rendered with
object-cover, cropping portraits and wide photos. Read the image's
natural size during ingestion and fit the node frame to the aspect
ratio (landscape caps height, portrait caps width) so the full
picture stays visible like the reference product.
This commit is contained in:
134 2026-09-18 18:34:23 +08:00
parent 885cab9fca
commit b4b6e1a738

View File

@ -5375,14 +5375,54 @@ export function LiblibVideoCanvasReplica({
};
}, []);
const readImageIntrinsicSize = useCallback(async (file: File) => {
if (!file.type.startsWith("image/")) return null;
try {
const bitmap = await createImageBitmap(file);
const size = { width: bitmap.width, height: bitmap.height };
bitmap.close();
return size;
} catch {
try {
return await new Promise<{ width: number; height: number }>((resolve, reject) => {
const url = URL.createObjectURL(file);
const image = new Image();
image.onload = () => {
URL.revokeObjectURL(url);
resolve({ width: image.naturalWidth, height: image.naturalHeight });
};
image.onerror = () => {
URL.revokeObjectURL(url);
reject(new Error("image decode failed"));
};
image.src = url;
});
} catch {
return null;
}
}
}, []);
const addUploadedFilesAtPoint = useCallback(
(files: File[], center: { x: number; y: number }, connectTargetId?: string) => {
async (files: File[], center: { x: number; y: number }, connectTargetId?: string) => {
if (files.length === 0) return;
const { files: batch, errors } = prepareCanvasMediaFiles(files);
if (errors.length) toast.error(errors.slice(0, 3).join("\n"));
if (!batch.length) return;
const sizes = await Promise.all(batch.map(file => readImageIntrinsicSize(file)));
const newNodes = batch.map((file, index) => {
const created = createUploadedMediaNode(file, "", 0, 0, nodes.length + index + 1);
const size = sizes[index];
if (size && size.width > 0 && size.height > 0) {
const aspect = size.width / size.height;
if (aspect >= 1) {
created.width = 420;
created.height = Math.min(560, Math.max(180, Math.round(420 / aspect)));
} else {
created.height = 560;
created.width = Math.min(420, Math.max(220, Math.round(560 * aspect)));
}
}
return { ...created, x: Math.round(center.x - created.width / 2 + index * 450),
y: Math.round(center.y - created.height / 2),
draftValues: { ...created.draftValues, uploadStatus: "uploading" } };
@ -5412,12 +5452,12 @@ export function LiblibVideoCanvasReplica({
}
});
},
[addNodes, nodes.length, uploadCanvasMediaFile]
[addNodes, nodes.length, readImageIntrinsicSize, uploadCanvasMediaFile]
);
const addUploadedFiles = useCallback(
(files: File[]) => {
addUploadedFilesAtPoint(files, toCanvasPoint(window.innerWidth / 2, window.innerHeight / 2));
void addUploadedFilesAtPoint(files, toCanvasPoint(window.innerWidth / 2, window.innerHeight / 2));
},
[addUploadedFilesAtPoint, toCanvasPoint]
);
@ -5763,7 +5803,7 @@ export function LiblibVideoCanvasReplica({
const addCanvasCommandUploadedFiles = useCallback(
(files: File[]) => {
if (!canvasCommand) return;
addUploadedFilesAtPoint(files, { x: canvasCommand.canvasX, y: canvasCommand.canvasY });
void addUploadedFilesAtPoint(files, { x: canvasCommand.canvasX, y: canvasCommand.canvasY });
},
[addUploadedFilesAtPoint, canvasCommand]
);
@ -10111,7 +10151,7 @@ export function LiblibVideoCanvasReplica({
if (isBusy) { toast.error("画布正在加载,请稍候再拖入素材"); return; }
const files = Array.from(event.dataTransfer.files);
if (!files.length) { toast.error("未能读取拖入的文件,请从桌面直接拖拽素材后松开"); return; }
addUploadedFilesAtPoint(files, toCanvasPoint(event.clientX, event.clientY));
void addUploadedFilesAtPoint(files, toCanvasPoint(event.clientX, event.clientY));
}}
>
<div className="pointer-events-none absolute inset-0" style={{backgroundImage:"radial-gradient(circle,rgba(255,255,255,0.14) 0.7px,transparent 0.7px)",backgroundPosition:"0.5px 0.5px",backgroundSize:"16px 16px"}} />
@ -10547,7 +10587,7 @@ export function LiblibVideoCanvasReplica({
setNodes(current => current.map(node => node.id === selectedNode.id ? {...node,draftValues:{...node.draftValues,smartEditDraft:{...(node.draftValues?.smartEditDraft as Record<string,any> || {}),assets:((node.draftValues?.smartEditDraft as Record<string,any>)?.assets || []).filter((asset:{id:string}) => asset.id !== source)}}} : node));
}}
onReference={() => setLineConnectState({sourceId:null,targetId:selectedNode.id,cursorCanvasX:selectedNode.x-80,cursorCanvasY:selectedNode.y+selectedNode.height/2})}
onUpload={files => addUploadedFilesAtPoint(files,{x:selectedNode.x-600,y:selectedNode.y},selectedNode.id)}
onUpload={files => void addUploadedFilesAtPoint(files,{x:selectedNode.x-600,y:selectedNode.y},selectedNode.id)}
onSubmit={async () => {
assertConnectedInputsReady(selectedTargetSourceNodes);
const draft = resolveSmartEditIntent((selectedNode.draftValues?.smartEditDraft || {}) as Record<string,any>);