From 03f07564e0e2d132628dd059bf8f89839ecb5da6 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 10 Apr 2026 22:21:31 +0800 Subject: [PATCH] feat: customize PSD dimension annotations --- .../agent-psd-template-export-preview.tsx | 361 +++++++++++++----- 1 file changed, 267 insertions(+), 94 deletions(-) diff --git a/components/agents/agent-psd-template-export-preview.tsx b/components/agents/agent-psd-template-export-preview.tsx index 21eea40e2..230ef9a18 100644 --- a/components/agents/agent-psd-template-export-preview.tsx +++ b/components/agents/agent-psd-template-export-preview.tsx @@ -13,6 +13,7 @@ import { import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { Switch } from "@/components/ui/switch"; import { buildImageProxyUrl } from "@/lib/image-delivery"; import { cn } from "@/lib/utils"; @@ -43,6 +44,8 @@ type DimensionAnnotation = { x2: number; y2: number; label: string; + color: string; + glow: boolean; }; type Point = { @@ -85,8 +88,16 @@ const DIMENSION_UNIT = "CM"; const LABEL_PLACEHOLDER = "例如:120"; const IMAGE_EXTENSION_REGEX = /\.(png|jpg|jpeg|webp)$/i; const DIMENSION_SUFFIX_REGEX = /-尺寸标注(?=\.)/; -const DIMENSION_LINE_COLOR = "#666b73"; -const DIMENSION_TEXT_COLOR = "#2a2f35"; +const DEFAULT_DIMENSION_COLOR = "#666b73"; +const DIMENSION_COLOR_OPTIONS = [ + "#666b73", + "#f8fafc", + "#fb923c", + "#f43f5e", + "#38bdf8", + "#34d399", + "#facc15", +]; const DIMENSION_TEXT_HALO_COLOR = "rgba(255,255,255,0.92)"; const PREVIEW_DIMENSION_FONT_SIZE = 10.4; const PREVIEW_DIMENSION_LABEL_OFFSET = 13; @@ -101,6 +112,7 @@ const PREVIEW_DIMENSION_HANDLE_STROKE_WIDTH = 1.2; const PREVIEW_DIMENSION_TEXT_HALO_WIDTH = 2.2; const SIZE_INFO_LINE_REGEX = /尺寸信息[::]\s*([^\n]+)/i; const NUMBER_REGEX = /\d+(?:\.\d+)?/g; +const HEX_COLOR_REGEX = /^#(?:[0-9a-fA-F]{3}){1,2}$/; type AutoDimensionValues = { widthCm?: number; @@ -160,6 +172,43 @@ function formatDimensionValue(value: number) { : `${value.toFixed(1)}${DIMENSION_UNIT}`; } +function normalizeDimensionColor(value: string) { + const normalized = String(value || "").trim(); + if (HEX_COLOR_REGEX.test(normalized)) { + return normalized.toUpperCase(); + } + + return DEFAULT_DIMENSION_COLOR; +} + +function colorWithAlpha(color: string, alpha: number) { + const normalized = normalizeDimensionColor(color).slice(1); + const expanded = + normalized.length === 3 + ? normalized + .split("") + .map((item) => `${item}${item}`) + .join("") + : normalized; + const red = Number.parseInt(expanded.slice(0, 2), 16); + const green = Number.parseInt(expanded.slice(2, 4), 16); + const blue = Number.parseInt(expanded.slice(4, 6), 16); + return `rgba(${red}, ${green}, ${blue}, ${clamp(alpha, 0, 1)})`; +} + +function getAnnotationColor(annotation: DimensionAnnotation) { + return normalizeDimensionColor(annotation.color); +} + +function getAnnotationGlowFilter(annotation: DimensionAnnotation) { + if (!annotation.glow) { + return; + } + + const color = getAnnotationColor(annotation); + return `drop-shadow(0 0 4px ${colorWithAlpha(color, 0.42)}) drop-shadow(0 0 10px ${colorWithAlpha(color, 0.3)})`; +} + function normalizeDimensionInput(value: string) { return String(value || "") .trim() @@ -510,6 +559,7 @@ function drawAnnotationToCanvas(params: { }) { const { context, annotation, imageSize, annotations, autoDimensionValues } = params; + const annotationColor = getAnnotationColor(annotation); const label = getDimensionLabel({ annotation, annotations, @@ -544,10 +594,14 @@ function drawAnnotationToCanvas(params: { const centerY = (annotation.y1 + annotation.y2) / 2 + vector.ny * labelOffset; context.save(); - context.strokeStyle = DIMENSION_LINE_COLOR; + context.strokeStyle = annotationColor; context.lineWidth = strokeWidth; context.lineCap = "round"; context.lineJoin = "round"; + context.shadowColor = annotation.glow + ? colorWithAlpha(annotationColor, 0.52) + : "transparent"; + context.shadowBlur = annotation.glow ? Math.max(8, fontSize * 0.7) : 0; context.beginPath(); context.moveTo(annotation.x1, annotation.y1); @@ -575,7 +629,7 @@ function drawAnnotationToCanvas(params: { context.lineWidth = Math.max(1.5, fontSize * 0.18); context.strokeStyle = DIMENSION_TEXT_HALO_COLOR; context.strokeText(label, 0, 0); - context.fillStyle = DIMENSION_TEXT_COLOR; + context.fillStyle = annotationColor; context.fillText(label, 0, 0); context.restore(); } @@ -654,6 +708,25 @@ export function AgentPsdTemplateExportPreview({ useEffect(() => clearPointerSession, [clearPointerSession]); + useEffect(() => { + if (!enableDimensionAnnotations) { + return; + } + + if (annotations.length === 0) { + if (selectedAnnotationId) { + setSelectedAnnotationId(""); + } + return; + } + + if ( + !annotations.some((annotation) => annotation.id === selectedAnnotationId) + ) { + setSelectedAnnotationId(annotations.at(-1)?.id || ""); + } + }, [annotations, enableDimensionAnnotations, selectedAnnotationId]); + const getCoordinateSpace = useCallback(() => { const element = previewViewportRef.current; if (!element || !previewRect || !imageSize) { @@ -724,7 +797,6 @@ export function AgentPsdTemplateExportPreview({ } if (!isDrawMode) { - setSelectedAnnotationId(""); return; } @@ -755,6 +827,8 @@ export function AgentPsdTemplateExportPreview({ x2: startPoint.x, y2: startPoint.y, label: "", + color: DEFAULT_DIMENSION_COLOR, + glow: false, }, ]); setSelectedAnnotationId(annotationId); @@ -1118,6 +1192,7 @@ export function AgentPsdTemplateExportPreview({ > {annotations.map((annotation) => { const isSelected = annotation.id === selectedAnnotationId; + const annotationColor = getAnnotationColor(annotation); const geometry = getPreviewAnnotationGeometry({ annotation, annotations, @@ -1145,53 +1220,56 @@ export function AgentPsdTemplateExportPreview({ y1={geometry.start.y} y2={geometry.end.y} /> - - - + + + + + handleAnnotationBodyPointerDown(annotation, event) } + style={{ filter: getAnnotationGlowFilter(annotation) }} > @@ -1280,57 +1358,152 @@ export function AgentPsdTemplateExportPreview({ {enableDimensionAnnotations ? (
{selectedAnnotation ? ( -
-
-
-
尺寸文字
-
- {autoDimensionValues - ? "留空时会优先使用商品尺寸信息自动显示 CM;手动时只输入数值即可。" - : "当前没有拿到商品尺寸信息,请直接输入数值,系统会自动补 CM。"} +
+
+
+
+
+ 尺寸文字 +
+
+ {autoDimensionValues + ? "留空时会优先使用商品尺寸信息自动显示 CM;手动时只输入数值即可。" + : "当前没有拿到商品尺寸信息,请直接输入数值,系统会自动补 CM。"} +
+
+ {getDimensionLabel({ + annotation: selectedAnnotation, + annotations, + autoDimensionValues, + }) ? ( +
+ 建议值{" "} + {getDimensionLabel({ + annotation: selectedAnnotation, + annotations, + autoDimensionValues, + })} +
+ ) : null} +
+
+ + setAnnotations((current) => + current.map((annotation) => + annotation.id === selectedAnnotation.id + ? { + ...annotation, + label: normalizeDimensionInput( + event.target.value + ), + } + : annotation + ) + ) + } + placeholder={LABEL_PLACEHOLDER} + value={normalizeDimensionInput(selectedAnnotation.label)} + /> + + {DIMENSION_UNIT} + +
+
+ +
+
+
+
+ 线条样式 +
+
+ 可为当前尺寸线设置颜色,并控制是否开启外发光。 +
+
+
+ 外发光 + + setAnnotations((current) => + current.map((annotation) => + annotation.id === selectedAnnotation.id + ? { ...annotation, glow: checked } + : annotation + ) + ) + } + />
- {getDimensionLabel({ - annotation: selectedAnnotation, - annotations, - autoDimensionValues, - }) ? ( -
- 建议值{" "} - {getDimensionLabel({ - annotation: selectedAnnotation, - annotations, - autoDimensionValues, - })} -
- ) : null} -
-
- - setAnnotations((current) => - current.map((annotation) => - annotation.id === selectedAnnotation.id - ? { - ...annotation, - label: normalizeDimensionInput( - event.target.value - ), - } - : annotation - ) - ) - } - placeholder={LABEL_PLACEHOLDER} - value={normalizeDimensionInput(selectedAnnotation.label)} - /> - - {DIMENSION_UNIT} - +
+ {DIMENSION_COLOR_OPTIONS.map((color) => { + const normalizedColor = normalizeDimensionColor(color); + const isActive = + getAnnotationColor(selectedAnnotation) === + normalizedColor; + + return ( +
+ ) : annotations.length > 0 ? ( +
+ 当前已有尺寸线,正在为你自动保留最近一条为选中状态。 + 如果样式面板没刷新,点一下对应尺寸线即可继续编辑。 +
) : (
选中一条尺寸线后,可以在这里输入真实尺寸文案。