base group

This commit is contained in:
hamid zarghami
2026-01-05 12:27:58 +03:30
parent e683b30fa1
commit 61d8e5a814
17 changed files with 607 additions and 231 deletions
@@ -21,7 +21,7 @@ type ObjectRendererProps = {
isSelected: boolean;
transformerRef: React.RefObject<Konva.Transformer | null>;
layerRef: React.RefObject<Konva.Layer | null>;
onSelect: (id: string, node: Konva.Node) => void;
onSelect: (id: string, node: Konva.Node, e?: Konva.KonvaEventObject<MouseEvent>) => void;
onUpdate: (id: string, updates: Partial<EditorObject>) => void;
draggable: boolean;
onCellClick?: (tableId: string, cellId: string) => void;
@@ -51,17 +51,18 @@ const ObjectRenderer = ({
// Apply caching for masked objects - REQUIRED for destination-in to work
useEffect(() => {
if (!groupRef.current || !shouldApplyMask) return;
const groupNode = groupRef.current;
if (!groupNode || !shouldApplyMask) return;
// Use double requestAnimationFrame to ensure all changes (including stroke) are rendered before caching
let rafId1: number;
const rafId2 = requestAnimationFrame(() => {
rafId1 = requestAnimationFrame(() => {
if (!groupRef.current) return;
// Clear cache first
groupRef.current.clearCache();
// Apply cache - Konva will automatically calculate bounds
groupRef.current.cache();
groupRef.current.getLayer()?.batchDraw();
@@ -71,7 +72,7 @@ const ObjectRenderer = ({
return () => {
cancelAnimationFrame(rafId2);
if (rafId1) cancelAnimationFrame(rafId1);
groupRef.current?.clearCache();
groupNode?.clearCache();
};
}, [shouldApplyMask, isSelected, maskShape?.x, maskShape?.y, maskShape?.width, maskShape?.height, maskShape?.rotation, obj.x, obj.y, obj.width, obj.height, obj.rotation, obj.maskInvert]);
@@ -97,7 +98,7 @@ const ObjectRenderer = ({
}
// برای objectهای با mask، یک obj جدید با موقعیت (0, 0) ایجاد کن
const shapeObj = shouldApplyMask
const shapeObj = shouldApplyMask
? { ...obj, x: 0, y: 0 }
: obj;
@@ -222,8 +223,8 @@ const ObjectRenderer = ({
key={obj.id}
obj={obj}
selectedCellId={selectedCellId || null}
onCellClick={(tableId, cellId, node) => {
onSelect(tableId, node);
onCellClick={(tableId, cellId, node, e) => {
onSelect(tableId, node, e);
onCellClick?.(tableId, cellId);
}}
onCellDblClick={(tableId, cellId, x, y, width, height, text) => {
@@ -236,6 +237,52 @@ const ObjectRenderer = ({
/>
);
break;
case "group":
// Render group as a simple selection box
// Objects inside group are rendered separately in EditorCanvas
shapeElement = (
<Group
key={obj.id}
id={obj.id}
name="group-container"
x={obj.x}
y={obj.y}
draggable={draggable}
onClick={(e) => {
const groupNode = e.currentTarget;
if (groupNode) {
onSelect(obj.id, groupNode, e);
}
}}
onDragMove={(e) => {
const node = e.target;
onUpdate(obj.id, {
x: node.x(),
y: node.y(),
});
}}
onDragEnd={(e) => {
const node = e.target;
onUpdate(obj.id, {
x: node.x(),
y: node.y(),
});
}}
>
<Rect
x={0}
y={0}
width={obj.width || 100}
height={obj.height || 100}
fill="transparent"
stroke={isSelected ? "#3b82f6" : "#e5e7eb"}
strokeWidth={isSelected ? 2 : 1}
dash={[5, 5]}
listening={true}
/>
</Group>
);
break;
default:
return null;
}
@@ -250,7 +297,7 @@ const ObjectRenderer = ({
// The mask layer has listening={false} to not interfere with selection
// Default: destination-out (overlap مخفی می‌شود)
const compositeOp = obj.maskInvert === false ? "destination-in" : "destination-out";
const handleGroupDragEnd = (e: Konva.KonvaEventObject<DragEvent>) => {
const node = e.target;
onUpdate(obj.id, {
@@ -323,18 +370,18 @@ const ObjectRenderer = ({
});
}
};
return (
<Group
<Group
ref={groupRef}
id={`masked-${obj.id}`}
name="masked-group"
x={obj.x}
y={obj.y}
draggable={draggable}
onClick={() => {
onClick={(e) => {
if (groupRef.current) {
onSelect(obj.id, groupRef.current);
onSelect(obj.id, groupRef.current, e);
}
}}
onDragEnd={handleGroupDragEnd}