fix bug custom shape

This commit is contained in:
hamid zarghami
2026-06-13 16:46:37 +03:30
parent 9447500725
commit ebb2092ec1
7 changed files with 171 additions and 34 deletions
@@ -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 (
<Line
ref={shapeRef}
@@ -1,4 +1,5 @@
import type { EditorObject, Page, TableCell } from "./editorStore.types";
import { normalizeCustomShapeObject } from "../utils/customShape";
export const createTableCellId = (row: number, col: number) =>
`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);
+2 -2
View File
@@ -9,7 +9,7 @@ import {
isInvalidPageId,
reorderByIds,
ungroupById,
ensureMissingGroupObjects,
normalizePageObjects,
} from "./editorStore.helpers";
import type {
EditorObject,
@@ -1024,7 +1024,7 @@ export const useEditorStore = create<EditorStoreType>((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:
+73
View File
@@ -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 0100 scale (not Konva's 01).
*/
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;
};
+26 -2
View File
@@ -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%)`;
+22 -15
View File
@@ -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<HTMLDivElement, BookPageProps>(
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 (
<svg
@@ -646,8 +654,8 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
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<HTMLDivElement, BookPageProps>(
)}
viewBox={`0 0 ${width} ${height}`}
>
{obj.fillType === 'gradient' && obj.gradient ? (
{gradientEndpoints ? (
<defs>
<linearGradient
id={gradientId}
gradientUnits="userSpaceOnUse"
x1="0"
y1="0"
x2={width}
y2={height}
gradientTransform={`rotate(${obj.gradient.angle}, ${width / 2}, ${height / 2})`}
x1={gradientEndpoints.x1}
y1={gradientEndpoints.y1}
x2={gradientEndpoints.x2}
y2={gradientEndpoints.y2}
>
<stop offset="0%" stopColor={obj.gradient.from} />
<stop offset="100%" stopColor={obj.gradient.to} />
<stop offset="0%" stopColor={obj.gradient!.from} />
<stop offset="100%" stopColor={obj.gradient!.to} />
</linearGradient>
</defs>
) : null}
{isClosed ? (
<polygon
points={pointsStr}
fill={obj.fillType === 'gradient' && obj.gradient ? `url(#${gradientId})` : fillColor}
fill={gradientEndpoints ? `url(#${gradientId})` : fillColor}
stroke={strokeColor}
strokeWidth={strokeWidth}
/>
+2 -9
View File
@@ -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 (01) instead of 0100
if (
obj.opacity !== undefined &&
obj.opacity > 0 &&
obj.opacity <= 1
) {
baseObject.opacity = obj.opacity * 100;
}
}
return baseObject;
return normalizeCustomShapeObject(baseObject);
});
return {