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 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { CanvasNode } from "@/domain/design";
|
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 { booleanShapePathResult } from "@/ui/pages/CanvasWorkspace/canvas/booleanShapePath";
|
||||||
import { isBooleanShapeGroupNode, parseBooleanShapeOptions, parseShapeOptions, shapeEndpointToWorldPoint, type ShapeStrokeSegment } from "@/ui/pages/CanvasWorkspace/canvas/shapeNode";
|
import { isBooleanShapeGroupNode, parseBooleanShapeOptions, parseShapeOptions, shapeEndpointToWorldPoint, type ShapeStrokeSegment } from "@/ui/pages/CanvasWorkspace/canvas/shapeNode";
|
||||||
import { normalizeStrokeStyle } from "@/ui/pages/CanvasWorkspace/canvas/frameStyle";
|
import { normalizeStrokeStyle } from "@/ui/pages/CanvasWorkspace/canvas/frameStyle";
|
||||||
@@ -27,11 +27,12 @@ export function ShapeNodePreview({ node }: { node: CanvasNode }) {
|
|||||||
|
|
||||||
if (options.kind === "path") {
|
if (options.kind === "path") {
|
||||||
const path = normalizedPathDataToSvg(options.pathData, width, height);
|
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 (
|
return (
|
||||||
<svg className="shape-node-preview" viewBox={`0 0 ${width} ${height}`} preserveAspectRatio="none">
|
<svg className="shape-node-preview" viewBox={`0 0 ${width} ${height}`} preserveAspectRatio="none">
|
||||||
<path d={path} fill={fill} fillOpacity={opacity} fillRule="evenodd" stroke={hasSegmentStrokes ? "none" : stroke} strokeWidth={strokeWidth} strokeDasharray={dashArray} strokeLinecap="round" strokeLinejoin="round" vectorEffect="non-scaling-stroke" />
|
<path d={path} fill={fill} fillOpacity={opacity} fillRule="evenodd" stroke={hasSegmentStrokes ? "none" : stroke} strokeWidth={strokeWidth} strokeDasharray={dashArray} strokeLinecap="round" strokeLinejoin="round" vectorEffect="non-scaling-stroke" />
|
||||||
{hasSegmentStrokes && <ShapeStrokeSegments segments={options.strokeSegments || []} width={width} height={height} />}
|
{hasSegmentStrokes && <ShapeStrokeSegments segments={strokeSegments} width={width} height={height} />}
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -90,7 +91,8 @@ function BooleanShapePreview({ node }: { node: CanvasNode }) {
|
|||||||
const height = Math.max(node.height, 1);
|
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 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 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 path = normalizedPathDataToSvg(pathData, width, height);
|
||||||
const fill = node.fillColor || "transparent";
|
const fill = node.fillColor || "transparent";
|
||||||
const stroke = node.strokeColor || "#111827";
|
const stroke = node.strokeColor || "#111827";
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { Justification, Layer as PsdLayer, LayerTextData, Psd, RGB } from "
|
|||||||
import { imageAdjustmentImageStyle, isDefaultImageAdjustments, parseImageAdjustments } from "@/ui/lib/imageAdjustments";
|
import { imageAdjustmentImageStyle, isDefaultImageAdjustments, parseImageAdjustments } from "@/ui/lib/imageAdjustments";
|
||||||
import { booleanShapePathResult } from "@/ui/pages/CanvasWorkspace/canvas/booleanShapePath";
|
import { booleanShapePathResult } from "@/ui/pages/CanvasWorkspace/canvas/booleanShapePath";
|
||||||
import { isBooleanShapeGroupNode, isShapeNode, parseBooleanShapeOptions, parseShapeOptions, shapeEndpointToWorldPoint, type ShapeStrokeSegment } from "@/ui/pages/CanvasWorkspace/canvas/shapeNode";
|
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";
|
import { defaultFrameFillColor, defaultFrameStrokeColor, defaultFrameStrokeStyle, defaultFrameStrokeWidth, normalizeHexColor, normalizeStrokeStyle } from "./frameStyle";
|
||||||
|
|
||||||
export type ImageExportFormat = "png" | "jpg" | "svg" | "psd";
|
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 options = parseBooleanShapeOptions(node.content);
|
||||||
const computed = options.pathData?.length && options.strokeSegments?.length ? null : booleanShapePathResult(options.operands, options.operation, options.width, options.height);
|
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 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);
|
const path = normalizedPathDataToSvg(pathData, width, height, x, y);
|
||||||
if (!path) return "";
|
if (!path) return "";
|
||||||
const strokeWidth = Math.max(0, node.strokeWidth ?? defaultFrameStrokeWidth);
|
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;
|
const inset = strokeWidth / 2;
|
||||||
|
|
||||||
if (options.kind === "path") {
|
if (options.kind === "path") {
|
||||||
const strokeSegments = options.strokeSegments || [];
|
const strokeSegments = options.pathData ? strokeSegmentsForEditedPath(options.pathData, options.strokeSegments) || options.strokeSegments || [] : [];
|
||||||
return `<path d="${normalizedPathDataToSvg(options.pathData, width, height, x, y)}" ${fill} ${strokeSegments.length ? `stroke="none"` : stroke} fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"${transform}/>${shapeStrokeSvgFragments(strokeSegments, x, y, width, height, transform)}`;
|
return `<path d="${normalizedPathDataToSvg(options.pathData, width, height, x, y)}" ${fill} ${strokeSegments.length ? `stroke="none"` : stroke} fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"${transform}/>${shapeStrokeSvgFragments(strokeSegments, x, y, width, height, transform)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user