fix: use cm dimensions in PSD annotation labels
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-04-02 21:47:27 +08:00
parent d53308126d
commit b26f46cc80
3 changed files with 224 additions and 30 deletions

View File

@ -6028,6 +6028,7 @@ export function AgentGenerationWorkspaceDarkV2({
}}
sourceImageUrl={psdTemplateDialogSourceUrl}
brandName={workflowContext?.brand || null}
sizeInfoText={workflowContext?.summaryText || null}
templateMode={psdTemplateDialogMode}
/>
</ClientErrorBoundary>

View File

@ -564,12 +564,14 @@ export function AgentPsdTemplateEditorDialog({
onOpenChange,
sourceImageUrl,
brandName,
sizeInfoText,
templateMode,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
sourceImageUrl: string | null;
brandName?: string | null;
sizeInfoText?: string | null;
templateMode?: AgentPsdTemplateMode | null;
}) {
const normalizedBrandName = String(brandName || "").trim();
@ -2169,6 +2171,7 @@ export function AgentPsdTemplateEditorDialog({
imageAlt="PSD 模板导出结果"
imageUrl={renderedResultUrl}
key={`${selectedTemplate?.id || "template"}-${renderedResultUrl}`}
sizeInfoText={sizeInfoText}
/>
) : selectedTextLayer ? (
<div className="flex min-h-0 flex-1 flex-col rounded-2xl border border-white/10 bg-white/[0.02] p-3.5 xl:p-4">

View File

@ -21,6 +21,7 @@ type AgentPsdTemplateExportPreviewProps = {
imageAlt?: string;
enableDimensionAnnotations?: boolean;
downloadName?: string;
sizeInfoText?: string | null;
};
type ImageSize = {
@ -86,6 +87,15 @@ const DIMENSION_SUFFIX_REGEX = /-尺寸标注(?=\.)/;
const DIMENSION_LINE_COLOR = "#666b73";
const DIMENSION_TEXT_COLOR = "#2a2f35";
const DIMENSION_TEXT_HALO_COLOR = "rgba(255,255,255,0.92)";
const SIZE_INFO_LINE_REGEX = /尺寸信息[::]\s*([^\n]+)/i;
const NUMBER_REGEX = /\d+(?:\.\d+)?/g;
type AutoDimensionValues = {
widthCm?: number;
depthCm?: number;
heightCm?: number;
rawValues: number[];
};
function clamp(value: number, min: number, max: number) {
return Math.max(min, Math.min(max, value));
@ -132,13 +142,145 @@ function getDimensionLength(annotation: DimensionAnnotation) {
);
}
function getDimensionLabel(annotation: DimensionAnnotation) {
function formatDimensionValue(value: number) {
return Number.isInteger(value) ? `${value}cm` : `${value.toFixed(1)}cm`;
}
function extractAutoDimensionValues(
sizeInfoText: string | null | undefined
): AutoDimensionValues | null {
const normalizedText = String(sizeInfoText || "").trim();
if (!normalizedText) {
return null;
}
const matchedLine =
normalizedText.match(SIZE_INFO_LINE_REGEX)?.[1] || normalizedText;
const values = Array.from(matchedLine.matchAll(NUMBER_REGEX))
.map((item) => Number(item[0]))
.filter((item) => Number.isFinite(item) && item > 0)
.slice(0, 3);
if (values.length === 0) {
return null;
}
if (values.length === 1) {
return {
heightCm: values[0],
rawValues: values,
widthCm: values[0],
};
}
if (values.length === 2) {
const [larger, smaller] = [...values].sort((left, right) => right - left);
return {
heightCm: larger,
rawValues: values,
widthCm: smaller,
};
}
const sortedValues = [...values].sort((left, right) => right - left);
const [heightCm, widthCm, depthCm] = sortedValues;
return {
depthCm,
heightCm,
rawValues: values,
widthCm,
};
}
function getAnnotationOrientation(annotation: DimensionAnnotation) {
const deltaX = Math.abs(annotation.x2 - annotation.x1);
const deltaY = Math.abs(annotation.y2 - annotation.y1);
if (deltaY > deltaX * 1.25) {
return "vertical";
}
if (deltaX > deltaY * 1.25) {
return "horizontal";
}
return "diagonal";
}
function getAutoDimensionLabel(params: {
annotation: DimensionAnnotation;
annotations: DimensionAnnotation[];
autoDimensionValues?: AutoDimensionValues | null;
}) {
const { annotation, annotations, autoDimensionValues } = params;
if (!autoDimensionValues) {
return "";
}
if (autoDimensionValues.rawValues.length === 1) {
return formatDimensionValue(autoDimensionValues.rawValues[0]);
}
const orientation = getAnnotationOrientation(annotation);
if (orientation === "vertical" && autoDimensionValues.heightCm) {
return formatDimensionValue(autoDimensionValues.heightCm);
}
if (orientation === "diagonal" && autoDimensionValues.depthCm) {
return formatDimensionValue(autoDimensionValues.depthCm);
}
if (orientation === "horizontal") {
const horizontalAnnotations = annotations
.filter((item) => getAnnotationOrientation(item) === "horizontal")
.sort(
(left, right) => getDimensionLength(right) - getDimensionLength(left)
);
const currentIndex = horizontalAnnotations.findIndex(
(item) => item.id === annotation.id
);
if (currentIndex <= 0 && autoDimensionValues.widthCm) {
return formatDimensionValue(autoDimensionValues.widthCm);
}
if (autoDimensionValues.depthCm) {
return formatDimensionValue(autoDimensionValues.depthCm);
}
if (autoDimensionValues.widthCm) {
return formatDimensionValue(autoDimensionValues.widthCm);
}
}
if (autoDimensionValues.widthCm) {
return formatDimensionValue(autoDimensionValues.widthCm);
}
if (autoDimensionValues.heightCm) {
return formatDimensionValue(autoDimensionValues.heightCm);
}
return "";
}
function getDimensionLabel(params: {
annotation: DimensionAnnotation;
annotations: DimensionAnnotation[];
autoDimensionValues?: AutoDimensionValues | null;
}) {
const { annotation, annotations, autoDimensionValues } = params;
const customLabel = String(annotation.label || "").trim();
if (customLabel) {
return customLabel;
}
return `${Math.max(1, Math.round(getDimensionLength(annotation)))} px`;
return getAutoDimensionLabel({
annotation,
annotations,
autoDimensionValues,
});
}
function mapImagePointToPreviewPoint(
@ -205,11 +347,20 @@ function getAnnotationVector(x1: number, y1: number, x2: number, y2: number) {
};
}
function getPreviewAnnotationGeometry(
annotation: DimensionAnnotation,
previewRect: PreviewRect,
imageSize: ImageSize
) {
function getPreviewAnnotationGeometry(params: {
annotation: DimensionAnnotation;
annotations: DimensionAnnotation[];
previewRect: PreviewRect;
imageSize: ImageSize;
autoDimensionValues?: AutoDimensionValues | null;
}) {
const {
annotation,
annotations,
previewRect,
imageSize,
autoDimensionValues,
} = params;
const start = mapImagePointToPreviewPoint(
{ x: annotation.x1, y: annotation.y1 },
previewRect,
@ -221,7 +372,11 @@ function getPreviewAnnotationGeometry(
imageSize
);
const vector = getAnnotationVector(start.x, start.y, end.x, end.y);
const label = getDimensionLabel(annotation);
const label = getDimensionLabel({
annotation,
annotations,
autoDimensionValues,
});
const labelOffset = 16;
const rawAngleRadians = Math.atan2(vector.dy, vector.dx);
const angleRadians =
@ -323,12 +478,20 @@ function drawDimensionCap(params: {
context.stroke();
}
function drawAnnotationToCanvas(
context: CanvasRenderingContext2D,
annotation: DimensionAnnotation,
imageSize: ImageSize
) {
const label = getDimensionLabel(annotation);
function drawAnnotationToCanvas(params: {
context: CanvasRenderingContext2D;
annotation: DimensionAnnotation;
imageSize: ImageSize;
annotations: DimensionAnnotation[];
autoDimensionValues?: AutoDimensionValues | null;
}) {
const { context, annotation, imageSize, annotations, autoDimensionValues } =
params;
const label = getDimensionLabel({
annotation,
annotations,
autoDimensionValues,
});
const vector = getAnnotationVector(
annotation.x1,
annotation.y1,
@ -399,6 +562,7 @@ export function AgentPsdTemplateExportPreview({
imageAlt = "PSD 模板导出结果",
enableDimensionAnnotations = false,
downloadName,
sizeInfoText,
}: AgentPsdTemplateExportPreviewProps) {
const previewViewportRef = useRef<HTMLDivElement | null>(null);
const dragStateRef = useRef<DragState | null>(null);
@ -416,6 +580,10 @@ export function AgentPsdTemplateExportPreview({
() => buildImageProxyUrl(imageUrl),
[imageUrl]
);
const autoDimensionValues = useMemo(
() => extractAutoDimensionValues(sizeInfoText),
[sizeInfoText]
);
const selectedAnnotation = useMemo(
() =>
annotations.find(
@ -794,9 +962,15 @@ export function AgentPsdTemplateExportPreview({
context.drawImage(image, 0, 0, canvas.width, canvas.height);
for (const annotation of annotations) {
drawAnnotationToCanvas(context, annotation, {
width: canvas.width,
height: canvas.height,
drawAnnotationToCanvas({
annotation,
annotations,
autoDimensionValues,
context,
imageSize: {
width: canvas.width,
height: canvas.height,
},
});
}
@ -824,7 +998,13 @@ export function AgentPsdTemplateExportPreview({
} finally {
setIsDownloading(false);
}
}, [annotations, downloadName, enableDimensionAnnotations, imageUrl]);
}, [
annotations,
autoDimensionValues,
downloadName,
enableDimensionAnnotations,
imageUrl,
]);
return (
<div className="flex min-h-0 flex-1 flex-col gap-3 rounded-2xl border border-white/10 bg-white/[0.02] p-3.5 xl:p-4">
@ -915,11 +1095,13 @@ export function AgentPsdTemplateExportPreview({
>
{annotations.map((annotation) => {
const isSelected = annotation.id === selectedAnnotationId;
const geometry = getPreviewAnnotationGeometry(
const geometry = getPreviewAnnotationGeometry({
annotation,
annotations,
autoDimensionValues,
imageSize,
previewRect,
imageSize
);
});
const strokeWidth = isSelected ? 2 : 1.5;
const tickLength = isSelected ? 8 : 6.5;
@ -1068,17 +1250,25 @@ export function AgentPsdTemplateExportPreview({
<div>
<div className="font-medium text-sm text-white">尺寸文字</div>
<div className="text-white/45 text-xs">
留空时会自动显示像素长度,可手动改成真实尺寸。
{autoDimensionValues
? "留空时会优先使用商品尺寸信息自动显示 cm,可继续手动覆盖。"
: "当前没有拿到商品尺寸信息,请直接手动输入 cm 尺寸。"}
</div>
</div>
<div className="rounded-full border border-white/10 bg-black/40 px-2.5 py-1 text-[11px] text-white/65">
长度{" "}
{Math.max(
1,
Math.round(getDimensionLength(selectedAnnotation))
)}{" "}
px
</div>
{getDimensionLabel({
annotation: selectedAnnotation,
annotations,
autoDimensionValues,
}) ? (
<div className="rounded-full border border-white/10 bg-black/40 px-2.5 py-1 text-[11px] text-white/65">
建议值{" "}
{getDimensionLabel({
annotation: selectedAnnotation,
annotations,
autoDimensionValues,
})}
</div>
) : null}
</div>
<Input
className="border-white/10 bg-black/35 text-white placeholder:text-white/28"