fix bug custom shape
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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%)`;
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user