feat(canvas): round-trip Pen settings with the selected path style
Add pathPenSettingsForNode / pathPenSettingsToShapeStyle so the Pen tool controls can display and edit the selected path node's stroke color and width in screen space, and let CanvasPathEditor re-apply fill/stroke/opacity to the mounted leafer path when the node's style changes during editing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,7 @@ export function CanvasPathEditor({
|
||||
}) {
|
||||
const mountRef = useRef<HTMLDivElement | null>(null);
|
||||
const appRef = useRef<import("leafer-ui").App | null>(null);
|
||||
const pathRef = useRef<import("leafer-ui").Path | null>(null);
|
||||
const viewportRef = useRef(viewport);
|
||||
const latestPathRef = useRef<EditablePathData | null>(null);
|
||||
const commitRef = useRef(onCommit);
|
||||
@@ -95,13 +96,10 @@ export function CanvasPathEditor({
|
||||
x: node.x,
|
||||
y: node.y,
|
||||
path: initialPath,
|
||||
fill: node.fillColor || "transparent",
|
||||
stroke: node.strokeColor || "#111827",
|
||||
strokeWidth: Math.max(node.strokeWidth ?? 2, 0),
|
||||
dashPattern: strokeDashPattern(node.strokeStyle, node.strokeWidth),
|
||||
opacity: typeof node.opacity === "number" ? node.opacity : 1,
|
||||
...pathNodeStyle(node),
|
||||
editable: true
|
||||
});
|
||||
pathRef.current = path;
|
||||
applyViewport(nextApp, viewportRef.current);
|
||||
nextApp.resize({ width: stageSize.width, height: stageSize.height, pixelRatio: window.devicePixelRatio || 1 });
|
||||
nextApp.tree.add(path);
|
||||
@@ -125,6 +123,7 @@ export function CanvasPathEditor({
|
||||
disposed = true;
|
||||
document.removeEventListener("keydown", handleKeyDown, true);
|
||||
controlRef.current = null;
|
||||
pathRef.current = null;
|
||||
appRef.current?.destroy();
|
||||
appRef.current = null;
|
||||
};
|
||||
@@ -140,6 +139,10 @@ export function CanvasPathEditor({
|
||||
appRef.current?.resize({ width: stageSize.width, height: stageSize.height, pixelRatio: window.devicePixelRatio || 1 });
|
||||
}, [stageSize.height, stageSize.width]);
|
||||
|
||||
useEffect(() => {
|
||||
pathRef.current?.set(pathNodeStyle(node));
|
||||
}, [node.fillColor, node.opacity, node.strokeColor, node.strokeStyle, node.strokeWidth]);
|
||||
|
||||
const stopPropagation = (event: ReactPointerEvent<HTMLDivElement>) => event.stopPropagation();
|
||||
return <div ref={mountRef} className="canvas-path-editor-overlay" onPointerDown={stopPropagation} />;
|
||||
}
|
||||
@@ -148,6 +151,16 @@ function applyViewport(app: import("leafer-ui").App, viewport: CanvasViewport) {
|
||||
app.tree.set({ x: viewport.x, y: viewport.y, scaleX: viewport.k, scaleY: viewport.k });
|
||||
}
|
||||
|
||||
function pathNodeStyle(node: CanvasNode) {
|
||||
return {
|
||||
fill: node.fillColor || "transparent",
|
||||
stroke: node.strokeColor || "#111827",
|
||||
strokeWidth: Math.max(node.strokeWidth ?? 2, 0),
|
||||
dashPattern: strokeDashPattern(node.strokeStyle, node.strokeWidth) ?? [],
|
||||
opacity: typeof node.opacity === "number" ? node.opacity : 1
|
||||
};
|
||||
}
|
||||
|
||||
function strokeDashPattern(style: string | undefined, width = 1) {
|
||||
if (style === "dashed") return [Math.max(width * 3, 8), Math.max(width * 2, 6)];
|
||||
if (style === "dotted") return [1, Math.max(width * 2, 6)];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { CanvasNode } from "@/domain/design";
|
||||
import { shapeOptionsToContent } from "./shapeNode.ts";
|
||||
import { defaultShapeStrokeColor, defaultShapeStrokeWidth, shapeOptionsToContent, type ShapeStylePatch } from "./shapeNode.ts";
|
||||
|
||||
export type PathPenPoint = {
|
||||
x: number;
|
||||
@@ -25,7 +25,7 @@ export type PathPenDraft = {
|
||||
viewportScale: number;
|
||||
};
|
||||
|
||||
type PathPenSettings = {
|
||||
export type PathPenSettings = {
|
||||
color: string;
|
||||
width: number;
|
||||
};
|
||||
@@ -183,6 +183,20 @@ export function createPathPenNode(draft: PathPenDraft, title: string): CanvasNod
|
||||
};
|
||||
}
|
||||
|
||||
export function pathPenSettingsForNode(node: CanvasNode, viewportScale: number): PathPenSettings {
|
||||
return {
|
||||
color: node.strokeColor || defaultShapeStrokeColor,
|
||||
width: round(Math.max(1, (node.strokeWidth || defaultShapeStrokeWidth) * Math.max(viewportScale, 0.01)))
|
||||
};
|
||||
}
|
||||
|
||||
export function pathPenSettingsToShapeStyle(settings: PathPenSettings, viewportScale: number): ShapeStylePatch {
|
||||
return {
|
||||
strokeColor: settings.color,
|
||||
strokeWidth: Math.max(1, settings.width / Math.max(viewportScale, 0.01))
|
||||
};
|
||||
}
|
||||
|
||||
function toAnchor(position: PathPenPosition): PathPenAnchor {
|
||||
return {
|
||||
screen: { ...position.screen },
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
constrainPathPenPoint,
|
||||
createPathPenNode,
|
||||
pathPenDraftToPathData,
|
||||
pathPenSettingsForNode,
|
||||
pathPenSettingsToShapeStyle,
|
||||
setLastPathPenAnchorHandles,
|
||||
startPathPenDraft
|
||||
} from "../src/ui/pages/CanvasWorkspace/canvas/pathPen.ts";
|
||||
@@ -66,3 +68,12 @@ test("persists a completed Pen path as an editable shape node", () => {
|
||||
assert.equal(options.kind, "path");
|
||||
assert.deepEqual(options.pathData, [1, 0.04955, 0.5, 2, 0.95045, 0.5]);
|
||||
});
|
||||
|
||||
test("round-trips the selected path style through screen-space Pen settings", () => {
|
||||
let draft = startPathPenDraft(settings, 0.5, point(10, 20));
|
||||
draft = appendPathPenAnchor(draft, point(110, 20));
|
||||
const node = createPathPenNode(draft, "Pen");
|
||||
|
||||
assert.deepEqual(pathPenSettingsForNode(node, 0.5), settings);
|
||||
assert.deepEqual(pathPenSettingsToShapeStyle({ color: "#A855F7", width: 8 }, 0.5), { strokeColor: "#A855F7", strokeWidth: 16 });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user