This commit is contained in:
hamid zarghami
2026-01-04 11:27:53 +03:30
parent aa6153d289
commit 0c6bf2b52b
12 changed files with 758 additions and 291 deletions
@@ -1,6 +1,6 @@
import Konva from "konva";
import { Group } from "react-konva";
import { useEffect, useRef } from "react";
import { Group, Rect, Circle, RegularPolygon, Star } from "react-konva";
import Konva from "konva";
import type { EditorObject } from "../../store/editorStore";
import {
RectangleShape,
@@ -15,7 +15,6 @@ import {
VideoShape,
} from "../tools";
import Table from "@/components/Table";
import MaskShapeRenderer from "./MaskShapeRenderer";
type ObjectRendererProps = {
obj: EditorObject;
@@ -28,7 +27,7 @@ type ObjectRendererProps = {
onCellClick?: (tableId: string, cellId: string) => void;
onCellDblClick?: (tableId: string, cellId: string, x: number, y: number, width: number, height: number, text: string) => void;
selectedCellId?: string | null;
maskShape?: EditorObject | null;
allObjects?: EditorObject[];
};
const ObjectRenderer = ({
@@ -42,39 +41,35 @@ const ObjectRenderer = ({
onCellClick,
onCellDblClick,
selectedCellId,
maskShape,
allObjects = [],
}: ObjectRendererProps) => {
const groupRef = useRef<Konva.Group>(null);
useEffect(() => {
if (!groupRef.current) return;
// Find mask for this object
const maskShape = obj.maskId ? allObjects.find((m) => m.id === obj.maskId) : null;
const shouldApplyMask = maskShape && !obj.isMask;
const isImage = obj.type === "image" || obj.type === "document";
if (maskShape && !isImage) {
// فقط برای non-image objects از cache استفاده کن
groupRef.current.cache({
pixelRatio: 1,
});
} else {
// Apply caching for masked objects - REQUIRED for destination-in to work
useEffect(() => {
if (!groupRef.current || !shouldApplyMask) return;
// Use requestAnimationFrame to ensure shapes are rendered before caching
const rafId = requestAnimationFrame(() => {
if (!groupRef.current) return;
// Clear cache first
groupRef.current.clearCache();
}
groupRef.current.getLayer()?.batchDraw();
}, [
maskShape,
maskShape?.x,
maskShape?.y,
maskShape?.width,
maskShape?.height,
maskShape?.rotation,
obj.type,
obj.x,
obj.y,
obj.width,
obj.height,
obj.rotation
]);
// Apply cache - Konva will automatically calculate bounds
groupRef.current.cache();
groupRef.current.getLayer()?.batchDraw();
});
return () => {
cancelAnimationFrame(rafId);
groupRef.current?.clearCache();
};
}, [shouldApplyMask, maskShape?.x, maskShape?.y, maskShape?.width, maskShape?.height, maskShape?.rotation, obj.x, obj.y, obj.width, obj.height, obj.rotation]);
if (obj.visible === false) {
return null;
@@ -89,6 +84,75 @@ const ObjectRenderer = ({
draggable,
};
const renderMaskShape = (mask: EditorObject) => {
const maskProps = {
listening: false,
perfectDrawEnabled: false,
fill: "black",
};
// موقعیت mask نسبت به object - برای overlap صحیح
// mask در موقعیت مطلق است، پس باید آن را relative به object کنیم
const relativeX = (mask.x || 0);
const relativeY = (mask.y || 0);
if (mask.type === "rectangle" && mask.shapeType === "circle") {
return (
<Circle
{...maskProps}
x={relativeX}
y={relativeY}
radius={(mask.width || 50) / 2}
rotation={mask.rotation || 0}
/>
);
}
if (mask.type === "rectangle" && mask.shapeType === "triangle") {
const radius = Math.min((mask.width || 100) / 2, (mask.height || 100) / 2);
return (
<RegularPolygon
{...maskProps}
x={relativeX + (mask.width || 100) / 2}
y={relativeY + (mask.height || 100) / 2}
sides={3}
radius={radius}
rotation={mask.rotation || 0}
/>
);
}
if (mask.type === "rectangle" && mask.shapeType === "abstract") {
const baseRadius = Math.min((mask.width || 100) / 2, (mask.height || 100) / 2);
const abstractScale = 0.85;
const radius = baseRadius * abstractScale;
const innerRadius = radius * 0.5;
return (
<Star
{...maskProps}
x={relativeX + (mask.width || 100) / 2}
y={relativeY + (mask.height || 100) / 2}
numPoints={5}
innerRadius={innerRadius}
outerRadius={radius}
rotation={mask.rotation || 0}
/>
);
}
return (
<Rect
{...maskProps}
x={relativeX}
y={relativeY}
width={mask.width || 100}
height={mask.height || 100}
rotation={mask.rotation || 0}
/>
);
};
let shapeElement: React.ReactNode = null;
switch (obj.type) {
@@ -148,15 +212,26 @@ const ObjectRenderer = ({
return null;
}
if (!maskShape) {
// If no mask, render normally
if (!shouldApplyMask || !maskShape) {
return shapeElement;
}
// Apply masking with Group + destination-in or destination-out
// IMPORTANT: The shape inside keeps its normal event handlers
// The mask layer has listening={false} to not interfere with selection
// Default: destination-out (overlap مخفی می‌شود)
const compositeOp = obj.maskInvert === false ? "destination-in" : "destination-out";
return (
<Group ref={groupRef} name={`masked-group-${obj.id}`}>
<Group
ref={groupRef}
id={`masked-${obj.id}`}
name="masked-group"
>
{shapeElement}
<Group globalCompositeOperation="destination-out" listening={false}>
<MaskShapeRenderer obj={maskShape} />
<Group globalCompositeOperation={compositeOp} listening={false}>
{renderMaskShape(maskShape)}
</Group>
</Group>
);