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:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user