feat(canvas): per-generator target size, placement and draft preservation

Persist each image-generator node's target resolution in metadata and
surface it in the selection frame and node preview. Place new generators
without overlapping existing ones, keep local draft generators when a
compressed snapshot omits them, and add a dedicated selection-frame
variant plus scale-aware preview styling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-09 01:46:16 +08:00
parent 82683c40ea
commit cbf4e21e2b
6 changed files with 236 additions and 36 deletions
@@ -104,43 +104,75 @@
}
.image-generator-preview {
--image-generator-viewport-scale: 1;
position: relative;
width: 100%;
height: 100%;
overflow: visible;
}
.image-generator-label {
.image-generator-meta {
position: absolute;
left: 0;
top: -28px;
display: inline-flex;
align-items: center;
gap: 6px;
right: 0;
top: calc(-30px / var(--image-generator-viewport-scale));
z-index: 2;
height: calc(24px / var(--image-generator-viewport-scale));
color: #2f80ff;
font-size: 15px;
font-size: calc(16px / var(--image-generator-viewport-scale));
font-weight: 500;
line-height: 22px;
line-height: calc(24px / var(--image-generator-viewport-scale));
letter-spacing: 0;
pointer-events: none;
white-space: nowrap;
}
.image-generator-label {
position: absolute;
left: 0;
top: 0;
display: inline-flex;
align-items: center;
gap: calc(6px / var(--image-generator-viewport-scale));
max-width: calc(100% - (126px / var(--image-generator-viewport-scale)));
overflow: hidden;
text-overflow: ellipsis;
}
.image-generator-label svg {
width: 15px;
height: 15px;
width: calc(17px / var(--image-generator-viewport-scale));
height: calc(17px / var(--image-generator-viewport-scale));
flex: 0 0 auto;
fill: #2f80ff;
stroke: #2f80ff;
}
.image-generator-label span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.image-generator-size-label {
position: absolute;
top: 0;
right: 0;
text-align: right;
}
.image-generator-frame {
box-sizing: border-box;
width: 100%;
height: 100%;
display: grid;
place-items: center;
border: 2px solid #2f80ff;
border: calc(2px / var(--image-generator-viewport-scale)) solid #2f80ff;
background: #eeeeed;
}
.image-generator-frame svg {
width: calc(34px / var(--image-generator-viewport-scale));
height: calc(34px / var(--image-generator-viewport-scale));
color: #cfd0d2;
fill: #cfd0d2;
stroke: #cfd0d2;
@@ -10,9 +10,10 @@ import { imageAdjustmentImageStyle, imageAdjustmentLayerStyle, isDefaultImageAdj
import { canvasImageUrl } from "@/ui/lib/imageDelivery";
import { isImageUrl } from "@/ui/pages/CanvasWorkspace/canvas/canvasNodeOps";
import { framePreviewStyle } from "@/ui/pages/CanvasWorkspace/canvas/frameStyle";
import { imageGeneratorTargetSizeFromNode, type ImageGeneratorTargetSize } from "@/ui/pages/CanvasWorkspace/canvas/imageGeneratorSizing";
import "./index.css";
export function ArtboardPreview({ node, viewportScale = 1 }: { node: CanvasNode; viewportScale?: number }) {
export function ArtboardPreview({ node, viewportScale = 1, imageGeneratorTargetSize }: { node: CanvasNode; viewportScale?: number; imageGeneratorTargetSize?: ImageGeneratorTargetSize }) {
const { t } = useI18n();
const overlayUiStyle = workOverlayUiStyle(viewportScale, node);
@@ -57,7 +58,7 @@ export function ArtboardPreview({ node, viewportScale = 1 }: { node: CanvasNode;
}
if (node.type === "image" && node.layerRole === "image-generator") {
return <ImageGeneratorPreview node={node} />;
return <ImageGeneratorPreview node={node} viewportScale={viewportScale} targetSize={imageGeneratorTargetSize} />;
}
if (node.type === "frame" && node.layerRole === "manual-frame") {
@@ -110,21 +111,31 @@ function clampNumber(value: number, min: number, max: number) {
return Math.min(Math.max(value, min), max);
}
function imageGeneratorUiStyle(viewportScale: number) {
return {
"--image-generator-viewport-scale": String(Math.max(viewportScale, 0.01))
} as CSSProperties;
}
function artboardKind(node: CanvasNode) {
if (node.height > node.width * 1.24) return "tall";
if (node.width > node.height * 1.35) return "wide";
return "square";
}
function ImageGeneratorPreview({ node }: { node: CanvasNode }) {
function ImageGeneratorPreview({ node, viewportScale, targetSize }: { node: CanvasNode; viewportScale: number; targetSize?: ImageGeneratorTargetSize }) {
const size = targetSize ?? imageGeneratorTargetSizeFromNode(node);
return (
<div className="image-generator-preview">
<div className="image-generator-label">
<ImageIcon size={14} />
<span>{node.title}</span>
<div className="image-generator-preview" style={imageGeneratorUiStyle(viewportScale)}>
<div className="image-generator-meta">
<div className="image-generator-label">
<ImageIcon aria-hidden="true" />
<span>{node.title}</span>
</div>
<div className="image-generator-size-label">{size.width} × {size.height}</div>
</div>
<div className="image-generator-frame">
<ImageIcon size={34} />
<ImageIcon aria-hidden="true" />
</div>
</div>
);