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 { Line } from "react-konva";
|
||||||
import Konva from "konva";
|
import Konva from "konva";
|
||||||
import type { ShapeProps } from "./types";
|
import type { ShapeProps } from "./types";
|
||||||
import { getKonvaGradientProps } from "../../utils/gradient";
|
import { getKonvaGradientProps } from "../../utils/gradient";
|
||||||
|
import { getCustomShapePointBounds } from "../../utils/customShape";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a completed custom polygon shape using Konva.Line with closed=true.
|
* 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)
|
? (hasStroke ? actualStrokeWidth : 2)
|
||||||
: actualStrokeWidth;
|
: actualStrokeWidth;
|
||||||
|
|
||||||
// Derive bounding box width/height from points for gradient calculation
|
const { minX, minY, width: bboxW, height: bboxH } =
|
||||||
const xs = points.filter((_, i) => i % 2 === 0);
|
getCustomShapePointBounds(points);
|
||||||
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 gradientProps = getKonvaGradientProps(
|
const gradientProps = getKonvaGradientProps(
|
||||||
obj.fillType,
|
obj.fillType,
|
||||||
@@ -42,8 +40,46 @@ const CustomShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapePr
|
|||||||
bboxW,
|
bboxW,
|
||||||
bboxH,
|
bboxH,
|
||||||
"rect",
|
"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 (
|
return (
|
||||||
<Line
|
<Line
|
||||||
ref={shapeRef}
|
ref={shapeRef}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { EditorObject, Page, TableCell } from "./editorStore.types";
|
import type { EditorObject, Page, TableCell } from "./editorStore.types";
|
||||||
|
import { normalizeCustomShapeObject } from "../utils/customShape";
|
||||||
|
|
||||||
export const createTableCellId = (row: number, col: number) =>
|
export const createTableCellId = (row: number, col: number) =>
|
||||||
`cell-${row}-${col}`;
|
`cell-${row}-${col}`;
|
||||||
@@ -378,3 +379,6 @@ export const ensureMissingGroupObjects = (
|
|||||||
|
|
||||||
return toAppend.length > 0 ? [...next, ...toAppend] : next;
|
return toAppend.length > 0 ? [...next, ...toAppend] : next;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const normalizePageObjects = (objects: EditorObject[]): EditorObject[] =>
|
||||||
|
ensureMissingGroupObjects(objects).map(normalizeCustomShapeObject);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
isInvalidPageId,
|
isInvalidPageId,
|
||||||
reorderByIds,
|
reorderByIds,
|
||||||
ungroupById,
|
ungroupById,
|
||||||
ensureMissingGroupObjects,
|
normalizePageObjects,
|
||||||
} from "./editorStore.helpers";
|
} from "./editorStore.helpers";
|
||||||
import type {
|
import type {
|
||||||
EditorObject,
|
EditorObject,
|
||||||
@@ -1024,7 +1024,7 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
|
|||||||
...page,
|
...page,
|
||||||
id: isInvalidPageId(page.id) ? createScopedId("page") : page.id,
|
id: isInvalidPageId(page.id) ? createScopedId("page") : page.id,
|
||||||
guides: page.guides || [],
|
guides: page.guides || [],
|
||||||
objects: ensureMissingGroupObjects(page.objects || []),
|
objects: normalizePageObjects(page.objects || []),
|
||||||
backgroundType:
|
backgroundType:
|
||||||
page.backgroundType ?? fallbackSettings.backgroundType ?? "color",
|
page.backgroundType ?? fallbackSettings.backgroundType ?? "color",
|
||||||
backgroundColor:
|
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,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
mode: GradientPointMode,
|
mode: GradientPointMode,
|
||||||
|
offset: Point = { x: 0, y: 0 },
|
||||||
) => {
|
) => {
|
||||||
if (fillType !== "gradient" || !gradient) return {};
|
if (fillType !== "gradient" || !gradient) return {};
|
||||||
const points = getGradientPoints(width, height, gradient.angle, mode);
|
const points = getGradientPoints(width, height, gradient.angle, mode);
|
||||||
return {
|
return {
|
||||||
fillPriority: "linear-gradient" as const,
|
fillPriority: "linear-gradient" as const,
|
||||||
fillLinearGradientStartPoint: points.start,
|
fillLinearGradientStartPoint: {
|
||||||
fillLinearGradientEndPoint: points.end,
|
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 [
|
fillLinearGradientColorStops: [0, gradient.from, 1, gradient.to] as [
|
||||||
number,
|
number,
|
||||||
string,
|
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) => {
|
export const toCssLinearGradient = (gradient: LinearGradient | undefined) => {
|
||||||
if (!gradient) return undefined;
|
if (!gradient) return undefined;
|
||||||
return `linear-gradient(${normalizeAngle(gradient.angle)}deg, ${gradient.from} 0%, ${gradient.to} 100%)`;
|
return `linear-gradient(${normalizeAngle(gradient.angle)}deg, ${gradient.from} 0%, ${gradient.to} 100%)`;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
import { type PageData } from '../types';
|
import { type PageData } from '../types';
|
||||||
import type { EditorObject } from '@/pages/editor/store/editorStore';
|
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 { getFontFamily } from '@/pages/editor/utils/fontFamily';
|
||||||
import {
|
import {
|
||||||
getCssFontWeight,
|
getCssFontWeight,
|
||||||
@@ -628,17 +629,24 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
const minLength = isClosed ? 6 : 4;
|
const minLength = isClosed ? 6 : 4;
|
||||||
if (rawPoints.length < minLength) return null;
|
if (rawPoints.length < minLength) return null;
|
||||||
|
|
||||||
const width = (obj.width || 100) * scale;
|
const { minX, minY, width: bboxW, height: bboxH } =
|
||||||
const height = (obj.height || 100) * scale;
|
getCustomShapePointBounds(rawPoints);
|
||||||
|
const width = bboxW * scale;
|
||||||
|
const height = bboxH * scale;
|
||||||
const pointsStr = Array.from(
|
const pointsStr = Array.from(
|
||||||
{ length: rawPoints.length / 2 },
|
{ 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(' ');
|
).join(' ');
|
||||||
|
|
||||||
const strokeWidth = hasStroke && obj.stroke ? actualStrokeWidth * scale : 0;
|
const strokeWidth = hasStroke && obj.stroke ? actualStrokeWidth * scale : 0;
|
||||||
const fillColor = obj.fill || '#3b82f6';
|
const fillColor = obj.fill || '#3b82f6';
|
||||||
const strokeColor = obj.stroke || 'transparent';
|
const strokeColor = obj.stroke || 'transparent';
|
||||||
const gradientId = `custom-shape-grad-${obj.id}-${index}`;
|
const gradientId = `custom-shape-grad-${obj.id}-${index}`;
|
||||||
|
const gradientEndpoints =
|
||||||
|
obj.fillType === 'gradient' && obj.gradient
|
||||||
|
? getSvgGradientEndpoints(obj.gradient, width, height, 'rect')
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
@@ -646,8 +654,8 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
style={applyStyle(
|
style={applyStyle(
|
||||||
{
|
{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: `${(obj.x || 0) * scale}px`,
|
left: `${((obj.x || 0) + minX) * scale}px`,
|
||||||
top: `${(obj.y || 0) * scale}px`,
|
top: `${((obj.y || 0) + minY) * scale}px`,
|
||||||
width: `${width}px`,
|
width: `${width}px`,
|
||||||
height: `${height}px`,
|
height: `${height}px`,
|
||||||
opacity: (obj.opacity ?? 100) / 100,
|
opacity: (obj.opacity ?? 100) / 100,
|
||||||
@@ -661,26 +669,25 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
)}
|
)}
|
||||||
viewBox={`0 0 ${width} ${height}`}
|
viewBox={`0 0 ${width} ${height}`}
|
||||||
>
|
>
|
||||||
{obj.fillType === 'gradient' && obj.gradient ? (
|
{gradientEndpoints ? (
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
id={gradientId}
|
id={gradientId}
|
||||||
gradientUnits="userSpaceOnUse"
|
gradientUnits="userSpaceOnUse"
|
||||||
x1="0"
|
x1={gradientEndpoints.x1}
|
||||||
y1="0"
|
y1={gradientEndpoints.y1}
|
||||||
x2={width}
|
x2={gradientEndpoints.x2}
|
||||||
y2={height}
|
y2={gradientEndpoints.y2}
|
||||||
gradientTransform={`rotate(${obj.gradient.angle}, ${width / 2}, ${height / 2})`}
|
|
||||||
>
|
>
|
||||||
<stop offset="0%" stopColor={obj.gradient.from} />
|
<stop offset="0%" stopColor={obj.gradient!.from} />
|
||||||
<stop offset="100%" stopColor={obj.gradient.to} />
|
<stop offset="100%" stopColor={obj.gradient!.to} />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
) : null}
|
) : null}
|
||||||
{isClosed ? (
|
{isClosed ? (
|
||||||
<polygon
|
<polygon
|
||||||
points={pointsStr}
|
points={pointsStr}
|
||||||
fill={obj.fillType === 'gradient' && obj.gradient ? `url(#${gradientId})` : fillColor}
|
fill={gradientEndpoints ? `url(#${gradientId})` : fillColor}
|
||||||
stroke={strokeColor}
|
stroke={strokeColor}
|
||||||
strokeWidth={strokeWidth}
|
strokeWidth={strokeWidth}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { EditorObject, TableData } from "@/pages/editor/store/editorStore";
|
import type { EditorObject, TableData } from "@/pages/editor/store/editorStore";
|
||||||
|
import { normalizeCustomShapeObject } from "@/pages/editor/utils/customShape";
|
||||||
import type { PageData } from "../types";
|
import type { PageData } from "../types";
|
||||||
|
|
||||||
type ViewerDataPage = {
|
type ViewerDataPage = {
|
||||||
@@ -170,17 +171,9 @@ export function transformViewerDataToPages(data: ViewerData): PageData[] {
|
|||||||
if (obj.type === "custom-shape") {
|
if (obj.type === "custom-shape") {
|
||||||
if (obj.points !== undefined) baseObject.points = obj.points;
|
if (obj.points !== undefined) baseObject.points = obj.points;
|
||||||
if (obj.closed !== undefined) baseObject.closed = obj.closed;
|
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 {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user