From ebb2092ec1f405498bd8e1f7d85b7460c8115415 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Sat, 13 Jun 2026 16:46:37 +0330 Subject: [PATCH] fix bug custom shape --- .../editor/components/tools/CustomShape.tsx | 48 ++++++++++-- src/pages/editor/store/editorStore.helpers.ts | 4 + src/pages/editor/store/editorStore.ts | 4 +- src/pages/editor/utils/customShape.ts | 73 +++++++++++++++++++ src/pages/editor/utils/gradient.ts | 28 ++++++- src/pages/viewer/components/BookPage.tsx | 37 ++++++---- src/pages/viewer/utils/dataTransformer.ts | 11 +-- 7 files changed, 171 insertions(+), 34 deletions(-) create mode 100644 src/pages/editor/utils/customShape.ts diff --git a/src/pages/editor/components/tools/CustomShape.tsx b/src/pages/editor/components/tools/CustomShape.tsx index 4d2ed17..f7a84e0 100644 --- a/src/pages/editor/components/tools/CustomShape.tsx +++ b/src/pages/editor/components/tools/CustomShape.tsx @@ -1,8 +1,9 @@ -import { useRef } from "react"; +import { useLayoutEffect, useRef } from "react"; import { Line } from "react-konva"; import Konva from "konva"; import type { ShapeProps } from "./types"; import { getKonvaGradientProps } from "../../utils/gradient"; +import { getCustomShapePointBounds } from "../../utils/customShape"; /** * Renders a completed custom polygon shape using Konva.Line with closed=true. @@ -30,11 +31,8 @@ const CustomShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapePr ? (hasStroke ? actualStrokeWidth : 2) : actualStrokeWidth; - // Derive bounding box width/height from points for gradient calculation - const xs = points.filter((_, i) => i % 2 === 0); - const ys = points.filter((_, i) => i % 2 !== 0); - const bboxW = xs.length > 0 ? Math.max(...xs) - Math.min(...xs) : 100; - const bboxH = ys.length > 0 ? Math.max(...ys) - Math.min(...ys) : 100; + const { minX, minY, width: bboxW, height: bboxH } = + getCustomShapePointBounds(points); const gradientProps = getKonvaGradientProps( obj.fillType, @@ -42,8 +40,46 @@ const CustomShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapePr bboxW, bboxH, "rect", + { x: minX, y: minY }, ); + // Konva Line does not always react to gradient prop changes from react-konva. + // Sync fill state imperatively so color / gradient edits apply immediately. + useLayoutEffect(() => { + const node = shapeRef.current; + if (!node) return; + + if (obj.fillType === "gradient" && obj.gradient) { + const props = getKonvaGradientProps( + obj.fillType, + obj.gradient, + bboxW, + bboxH, + "rect", + { x: minX, y: minY }, + ); + node.fillPriority("linear-gradient"); + node.fillLinearGradientStartPoint(props.fillLinearGradientStartPoint!); + node.fillLinearGradientEndPoint(props.fillLinearGradientEndPoint!); + node.fillLinearGradientColorStops(props.fillLinearGradientColorStops!); + } else { + node.fillPriority("color"); + node.fill(obj.fill ?? "#3b82f6"); + } + + node.getLayer()?.batchDraw(); + }, [ + obj.fillType, + obj.fill, + obj.gradient?.from, + obj.gradient?.to, + obj.gradient?.angle, + bboxW, + bboxH, + minX, + minY, + ]); + return ( `cell-${row}-${col}`; @@ -378,3 +379,6 @@ export const ensureMissingGroupObjects = ( return toAppend.length > 0 ? [...next, ...toAppend] : next; }; + +export const normalizePageObjects = (objects: EditorObject[]): EditorObject[] => + ensureMissingGroupObjects(objects).map(normalizeCustomShapeObject); diff --git a/src/pages/editor/store/editorStore.ts b/src/pages/editor/store/editorStore.ts index 001ca68..a39269f 100644 --- a/src/pages/editor/store/editorStore.ts +++ b/src/pages/editor/store/editorStore.ts @@ -9,7 +9,7 @@ import { isInvalidPageId, reorderByIds, ungroupById, - ensureMissingGroupObjects, + normalizePageObjects, } from "./editorStore.helpers"; import type { EditorObject, @@ -1024,7 +1024,7 @@ export const useEditorStore = create((set, get) => { ...page, id: isInvalidPageId(page.id) ? createScopedId("page") : page.id, guides: page.guides || [], - objects: ensureMissingGroupObjects(page.objects || []), + objects: normalizePageObjects(page.objects || []), backgroundType: page.backgroundType ?? fallbackSettings.backgroundType ?? "color", backgroundColor: diff --git a/src/pages/editor/utils/customShape.ts b/src/pages/editor/utils/customShape.ts new file mode 100644 index 0000000..ae3885a --- /dev/null +++ b/src/pages/editor/utils/customShape.ts @@ -0,0 +1,73 @@ +import type { EditorObject } from "../store/editorStore.types"; + +export type CustomShapePointBounds = { + minX: number; + minY: number; + width: number; + height: number; +}; + +export const getCustomShapePointBounds = ( + points: number[], +): CustomShapePointBounds => { + const xs = points.filter((_, i) => i % 2 === 0); + const ys = points.filter((_, i) => i % 2 !== 0); + + if (xs.length === 0 || ys.length === 0) { + return { minX: 0, minY: 0, width: 100, height: 100 }; + } + + const minX = Math.min(...xs); + const minY = Math.min(...ys); + const maxX = Math.max(...xs); + const maxY = Math.max(...ys); + + return { + minX, + minY, + width: Math.max(1, maxX - minX), + height: Math.max(1, maxY - minY), + }; +}; + +/** + * Ensures custom-shape points start at (0,0), width/height match the polygon, + * and opacity uses the editor's 0–100 scale (not Konva's 0–1). + */ +export const normalizeCustomShapeObject = ( + obj: EditorObject, +): EditorObject => { + if (obj.type !== "custom-shape" || !obj.points || obj.points.length < 6) { + return obj; + } + + let next: EditorObject = { ...obj }; + + if ( + next.opacity !== undefined && + next.opacity > 0 && + next.opacity <= 1 + ) { + next = { ...next, opacity: next.opacity * 100 }; + } + + const points = next.points!; + const { minX, minY, width, height } = getCustomShapePointBounds(points); + + if (minX !== 0 || minY !== 0) { + next = { + ...next, + x: (next.x ?? 0) + minX, + y: (next.y ?? 0) + minY, + width, + height, + points: points.map((val, i) => + i % 2 === 0 ? val - minX : val - minY, + ), + }; + } else if (next.width !== width || next.height !== height) { + next = { ...next, width, height }; + } + + return next; +}; diff --git a/src/pages/editor/utils/gradient.ts b/src/pages/editor/utils/gradient.ts index fd3e9c8..28a6bbb 100644 --- a/src/pages/editor/utils/gradient.ts +++ b/src/pages/editor/utils/gradient.ts @@ -51,13 +51,20 @@ export const getKonvaGradientProps = ( width: number, height: number, mode: GradientPointMode, + offset: Point = { x: 0, y: 0 }, ) => { if (fillType !== "gradient" || !gradient) return {}; const points = getGradientPoints(width, height, gradient.angle, mode); return { fillPriority: "linear-gradient" as const, - fillLinearGradientStartPoint: points.start, - fillLinearGradientEndPoint: points.end, + fillLinearGradientStartPoint: { + x: points.start.x + offset.x, + y: points.start.y + offset.y, + }, + fillLinearGradientEndPoint: { + x: points.end.x + offset.x, + y: points.end.y + offset.y, + }, fillLinearGradientColorStops: [0, gradient.from, 1, gradient.to] as [ number, string, @@ -67,6 +74,23 @@ export const getKonvaGradientProps = ( }; }; +/** SVG linearGradient endpoints matching the Konva gradient math. */ +export const getSvgGradientEndpoints = ( + gradient: LinearGradient, + width: number, + height: number, + mode: GradientPointMode = "rect", + offset: Point = { x: 0, y: 0 }, +) => { + const points = getGradientPoints(width, height, gradient.angle, mode); + return { + x1: points.start.x + offset.x, + y1: points.start.y + offset.y, + x2: points.end.x + offset.x, + y2: points.end.y + offset.y, + }; +}; + export const toCssLinearGradient = (gradient: LinearGradient | undefined) => { if (!gradient) return undefined; return `linear-gradient(${normalizeAngle(gradient.angle)}deg, ${gradient.from} 0%, ${gradient.to} 100%)`; diff --git a/src/pages/viewer/components/BookPage.tsx b/src/pages/viewer/components/BookPage.tsx index 8abc2f8..529ee6c 100644 --- a/src/pages/viewer/components/BookPage.tsx +++ b/src/pages/viewer/components/BookPage.tsx @@ -1,7 +1,8 @@ import { forwardRef } from 'react'; import { type PageData } from '../types'; import type { EditorObject } from '@/pages/editor/store/editorStore'; -import { toCssLinearGradient } from '@/pages/editor/utils/gradient'; +import { toCssLinearGradient, getSvgGradientEndpoints } from '@/pages/editor/utils/gradient'; +import { getCustomShapePointBounds } from '@/pages/editor/utils/customShape'; import { getFontFamily } from '@/pages/editor/utils/fontFamily'; import { getCssFontWeight, @@ -628,17 +629,24 @@ const BookPage = forwardRef( const minLength = isClosed ? 6 : 4; if (rawPoints.length < minLength) return null; - const width = (obj.width || 100) * scale; - const height = (obj.height || 100) * scale; + const { minX, minY, width: bboxW, height: bboxH } = + getCustomShapePointBounds(rawPoints); + const width = bboxW * scale; + const height = bboxH * scale; const pointsStr = Array.from( { length: rawPoints.length / 2 }, - (_, i) => `${rawPoints[i * 2] * scale},${rawPoints[i * 2 + 1] * scale}`, + (_, i) => + `${(rawPoints[i * 2] - minX) * scale},${(rawPoints[i * 2 + 1] - minY) * scale}`, ).join(' '); const strokeWidth = hasStroke && obj.stroke ? actualStrokeWidth * scale : 0; const fillColor = obj.fill || '#3b82f6'; const strokeColor = obj.stroke || 'transparent'; const gradientId = `custom-shape-grad-${obj.id}-${index}`; + const gradientEndpoints = + obj.fillType === 'gradient' && obj.gradient + ? getSvgGradientEndpoints(obj.gradient, width, height, 'rect') + : null; return ( ( style={applyStyle( { position: 'absolute', - left: `${(obj.x || 0) * scale}px`, - top: `${(obj.y || 0) * scale}px`, + left: `${((obj.x || 0) + minX) * scale}px`, + top: `${((obj.y || 0) + minY) * scale}px`, width: `${width}px`, height: `${height}px`, opacity: (obj.opacity ?? 100) / 100, @@ -661,26 +669,25 @@ const BookPage = forwardRef( )} viewBox={`0 0 ${width} ${height}`} > - {obj.fillType === 'gradient' && obj.gradient ? ( + {gradientEndpoints ? ( - - + + ) : null} {isClosed ? ( diff --git a/src/pages/viewer/utils/dataTransformer.ts b/src/pages/viewer/utils/dataTransformer.ts index 605db7c..29259db 100644 --- a/src/pages/viewer/utils/dataTransformer.ts +++ b/src/pages/viewer/utils/dataTransformer.ts @@ -1,4 +1,5 @@ import type { EditorObject, TableData } from "@/pages/editor/store/editorStore"; +import { normalizeCustomShapeObject } from "@/pages/editor/utils/customShape"; import type { PageData } from "../types"; type ViewerDataPage = { @@ -170,17 +171,9 @@ export function transformViewerDataToPages(data: ViewerData): PageData[] { if (obj.type === "custom-shape") { if (obj.points !== undefined) baseObject.points = obj.points; if (obj.closed !== undefined) baseObject.closed = obj.closed; - // Older custom shapes stored Konva opacity (0–1) instead of 0–100 - if ( - obj.opacity !== undefined && - obj.opacity > 0 && - obj.opacity <= 1 - ) { - baseObject.opacity = obj.opacity * 100; - } } - return baseObject; + return normalizeCustomShapeObject(baseObject); }); return {