From 56a33b3454e3a75e59cd9ffb907794f6b57063e5 Mon Sep 17 00:00:00 2001 From: liangxu Date: Sat, 18 Jul 2026 00:59:50 +0800 Subject: [PATCH] feat(canvas): render remapped stroke segments in previews and exports Shape previews, boolean previews, and SVG export now run stored stroke segments through strokeSegmentsForEditedPath so the drawn boundary styles always match the current path geometry, falling back to the stored segments when no edge indexes are available. Co-Authored-By: Claude Fable 5 --- .../CanvasWorkspace/canvas/ShapeNodePreview/index.tsx | 10 ++++++---- .../src/ui/pages/CanvasWorkspace/canvas/nodeExport.ts | 7 ++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/frontend/src/ui/pages/CanvasWorkspace/canvas/ShapeNodePreview/index.tsx b/frontend/src/ui/pages/CanvasWorkspace/canvas/ShapeNodePreview/index.tsx index 6252134..a082506 100644 --- a/frontend/src/ui/pages/CanvasWorkspace/canvas/ShapeNodePreview/index.tsx +++ b/frontend/src/ui/pages/CanvasWorkspace/canvas/ShapeNodePreview/index.tsx @@ -1,7 +1,7 @@ "use client"; import type { CanvasNode } from "@/domain/design"; -import { normalizedPathDataToSvg } from "@/ui/pages/CanvasWorkspace/canvas/editablePath"; +import { normalizedPathDataToSvg, strokeSegmentsForEditedPath } from "@/ui/pages/CanvasWorkspace/canvas/editablePath"; import { booleanShapePathResult } from "@/ui/pages/CanvasWorkspace/canvas/booleanShapePath"; import { isBooleanShapeGroupNode, parseBooleanShapeOptions, parseShapeOptions, shapeEndpointToWorldPoint, type ShapeStrokeSegment } from "@/ui/pages/CanvasWorkspace/canvas/shapeNode"; import { normalizeStrokeStyle } from "@/ui/pages/CanvasWorkspace/canvas/frameStyle"; @@ -27,11 +27,12 @@ export function ShapeNodePreview({ node }: { node: CanvasNode }) { if (options.kind === "path") { const path = normalizedPathDataToSvg(options.pathData, width, height); - const hasSegmentStrokes = Boolean(options.strokeSegments?.length); + const strokeSegments = options.pathData ? strokeSegmentsForEditedPath(options.pathData, options.strokeSegments) || options.strokeSegments || [] : []; + const hasSegmentStrokes = strokeSegments.length > 0; return ( - {hasSegmentStrokes && } + {hasSegmentStrokes && } ); } @@ -90,7 +91,8 @@ function BooleanShapePreview({ node }: { node: CanvasNode }) { const height = Math.max(node.height, 1); const computed = options.pathData?.length && options.strokeSegments?.length ? null : booleanShapePathResult(options.operands, options.operation, options.width, options.height); const pathData = options.pathData?.length ? options.pathData : computed?.pathData; - const strokeSegments = options.strokeSegments?.length ? options.strokeSegments : computed?.strokeSegments || []; + const storedStrokeSegments = options.strokeSegments?.length ? options.strokeSegments : computed?.strokeSegments || []; + const strokeSegments = pathData ? strokeSegmentsForEditedPath(pathData, storedStrokeSegments) || storedStrokeSegments : []; const path = normalizedPathDataToSvg(pathData, width, height); const fill = node.fillColor || "transparent"; const stroke = node.strokeColor || "#111827"; diff --git a/frontend/src/ui/pages/CanvasWorkspace/canvas/nodeExport.ts b/frontend/src/ui/pages/CanvasWorkspace/canvas/nodeExport.ts index bdc4378..44b2501 100644 --- a/frontend/src/ui/pages/CanvasWorkspace/canvas/nodeExport.ts +++ b/frontend/src/ui/pages/CanvasWorkspace/canvas/nodeExport.ts @@ -3,7 +3,7 @@ import type { Justification, Layer as PsdLayer, LayerTextData, Psd, RGB } from " import { imageAdjustmentImageStyle, isDefaultImageAdjustments, parseImageAdjustments } from "@/ui/lib/imageAdjustments"; import { booleanShapePathResult } from "@/ui/pages/CanvasWorkspace/canvas/booleanShapePath"; import { isBooleanShapeGroupNode, isShapeNode, parseBooleanShapeOptions, parseShapeOptions, shapeEndpointToWorldPoint, type ShapeStrokeSegment } from "@/ui/pages/CanvasWorkspace/canvas/shapeNode"; -import { normalizedPathDataToSvg } from "@/ui/pages/CanvasWorkspace/canvas/editablePath"; +import { normalizedPathDataToSvg, strokeSegmentsForEditedPath } from "@/ui/pages/CanvasWorkspace/canvas/editablePath"; import { defaultFrameFillColor, defaultFrameStrokeColor, defaultFrameStrokeStyle, defaultFrameStrokeWidth, normalizeHexColor, normalizeStrokeStyle } from "./frameStyle"; export type ImageExportFormat = "png" | "jpg" | "svg" | "psd"; @@ -503,7 +503,8 @@ function booleanShapeSvgFragment(node: CanvasNode, x: number, y: number, width: const options = parseBooleanShapeOptions(node.content); const computed = options.pathData?.length && options.strokeSegments?.length ? null : booleanShapePathResult(options.operands, options.operation, options.width, options.height); const pathData = options.pathData?.length ? options.pathData : computed?.pathData; - const strokeSegments = options.strokeSegments?.length ? options.strokeSegments : computed?.strokeSegments || []; + const storedStrokeSegments = options.strokeSegments?.length ? options.strokeSegments : computed?.strokeSegments || []; + const strokeSegments = pathData ? strokeSegmentsForEditedPath(pathData, storedStrokeSegments) || storedStrokeSegments : []; const path = normalizedPathDataToSvg(pathData, width, height, x, y); if (!path) return ""; const strokeWidth = Math.max(0, node.strokeWidth ?? defaultFrameStrokeWidth); @@ -526,7 +527,7 @@ function shapeSvgFragment(node: CanvasNode, x: number, y: number, width: number, const inset = strokeWidth / 2; if (options.kind === "path") { - const strokeSegments = options.strokeSegments || []; + const strokeSegments = options.pathData ? strokeSegmentsForEditedPath(options.pathData, options.strokeSegments) || options.strokeSegments || [] : []; return `${shapeStrokeSvgFragments(strokeSegments, x, y, width, height, transform)}`; }