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
+2 -2
View File
@@ -6,7 +6,7 @@ import type { EditorObject } from "@/pages/editor/store/editorStore";
type TableProps = { type TableProps = {
obj: EditorObject; obj: EditorObject;
selectedCellId: string | null; selectedCellId: string | null;
onCellClick: (tableId: string, cellId: string, node: Konva.Node) => void; onCellClick: (tableId: string, cellId: string, node: Konva.Node, e?: Konva.KonvaEventObject<MouseEvent>) => void;
onCellDblClick: (tableId: string, cellId: string, x: number, y: number, width: number, height: number, text: string) => void; onCellDblClick: (tableId: string, cellId: string, x: number, y: number, width: number, height: number, text: string) => void;
onDragEnd: (tableId: string, x: number, y: number) => void; onDragEnd: (tableId: string, x: number, y: number) => void;
draggable: boolean; draggable: boolean;
@@ -36,7 +36,7 @@ const Table = ({
const handleCellClick = (cellId: string, e: Konva.KonvaEventObject<MouseEvent>) => { const handleCellClick = (cellId: string, e: Konva.KonvaEventObject<MouseEvent>) => {
e.cancelBubble = true; e.cancelBubble = true;
if (groupRef.current) { if (groupRef.current) {
onCellClick(obj.id, cellId, groupRef.current); onCellClick(obj.id, cellId, groupRef.current, e);
} }
}; };
+344 -85
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef, useState } from "react";
import { Stage, Layer, Transformer, Rect, Group, Text } from "react-konva"; import { Stage, Layer, Transformer, Rect, Group, Text } from "react-konva";
import Konva from "konva"; import Konva from "konva";
import { useEditorStore, type EditorObject } from "../store/editorStore"; import { useEditorStore, type EditorObject } from "../store/editorStore";
@@ -18,7 +18,11 @@ const EditorCanvas = () => {
objects, objects,
updateObject, updateObject,
selectedObjectId, selectedObjectId,
selectedObjectIds,
setSelectedObjectId, setSelectedObjectId,
setSelectedObjectIds,
addToSelection,
removeFromSelection,
setSelectedTableId, setSelectedTableId,
selectedCellId, selectedCellId,
setSelectedCellId, setSelectedCellId,
@@ -46,37 +50,68 @@ const EditorCanvas = () => {
} }
}, [setStageRef, setLayerRef]); }, [setStageRef, setLayerRef]);
const handleSelect = (id: string, node: Konva.Node) => { const handleSelect = (id: string, node: Konva.Node, e?: Konva.KonvaEventObject<MouseEvent>) => {
const obj = objects.find((o) => o.id === id); const obj = objects.find((o) => o.id === id);
const isMultiSelect = e?.evt?.metaKey || e?.evt?.ctrlKey || e?.evt?.shiftKey;
// اگر object در یک group است، group را انتخاب کن نه object
if (obj?.groupId && obj.type !== "group") {
const groupObject = objects.find((o) => o.id === obj.groupId);
if (groupObject) {
if (isMultiSelect) {
if (selectedObjectIds.includes(groupObject.id)) {
removeFromSelection(groupObject.id);
} else {
addToSelection(groupObject.id);
}
} else {
setSelectedObjectId(groupObject.id);
}
return;
}
}
if (isMultiSelect) {
// انتخاب چندتایی
if (selectedObjectIds.includes(id)) {
// اگر قبلاً انتخاب شده بود، از انتخاب خارج کن
removeFromSelection(id);
} else {
// اگر انتخاب نشده بود، به انتخاب اضافه کن
addToSelection(id);
}
} else {
// انتخاب تک‌تایی
setSelectedObjectId(id);
}
setSelectedObjectId(id);
if (obj?.type === "grid") { if (obj?.type === "grid") {
setSelectedTableId(id); setSelectedTableId(id);
} else { } else if (!isMultiSelect) {
setSelectedTableId(null); setSelectedTableId(null);
} }
setSelectedCellId(null);
if (transformerRef.current && layerRef.current) { if (!isMultiSelect) {
// اگر object با mask گروه شده، transformer را به Group متصل کن setSelectedCellId(null);
const maskedGroup = layerRef.current.findOne(`#masked-${id}`);
const targetNode = maskedGroup || node;
transformerRef.current.nodes([targetNode]);
transformerRef.current.getLayer()?.batchDraw();
} }
}; };
const handleCellClick = (tableId: string, cellId: string) => { const handleCellClick = (tableId: string, cellId: string, node?: Konva.Node, e?: Konva.KonvaEventObject<MouseEvent>) => {
setSelectedTableId(tableId); const isMultiSelect = e?.evt?.metaKey || e?.evt?.ctrlKey || e?.evt?.shiftKey;
setSelectedObjectId(tableId);
setSelectedCellId(cellId); if (isMultiSelect) {
const obj = objects.find((o) => o.id === tableId); // انتخاب چندتایی
if (obj && transformerRef.current && layerRef.current) { if (selectedObjectIds.includes(tableId)) {
const tableNode = layerRef.current.findOne(`#${tableId}`); removeFromSelection(tableId);
if (tableNode) { } else {
transformerRef.current.nodes([tableNode]); addToSelection(tableId);
transformerRef.current.getLayer()?.batchDraw();
} }
} else {
setSelectedTableId(tableId);
setSelectedObjectId(tableId);
} }
setSelectedCellId(cellId);
}; };
const handleCellDblClick = (tableId: string, cellId: string, x: number, y: number, width: number, height: number, text: string) => { const handleCellDblClick = (tableId: string, cellId: string, x: number, y: number, width: number, height: number, text: string) => {
@@ -98,13 +133,13 @@ const EditorCanvas = () => {
const obj = objects.find((o) => o.id === id); const obj = objects.find((o) => o.id === id);
if (!obj) return; if (!obj) return;
// اگر object در یک گروه است و position تغییر کرد، همه اعضای گروه را جابجا کن // اگر group object است و position تغییر کرد، همه objectهای داخل group را جابجا کن
if (obj.groupId && (updates.x !== undefined || updates.y !== undefined)) { if (obj.type === "group" && (updates.x !== undefined || updates.y !== undefined)) {
const groupMembers = objects.filter((o) => o.groupId === obj.groupId && o.id !== id); const groupMembers = objects.filter((o) => o.groupId === id);
const deltaX = updates.x !== undefined ? updates.x - (obj.x || 0) : 0; const deltaX = updates.x !== undefined ? updates.x - (obj.x || 0) : 0;
const deltaY = updates.y !== undefined ? updates.y - (obj.y || 0) : 0; const deltaY = updates.y !== undefined ? updates.y - (obj.y || 0) : 0;
// به‌روزرسانی object اصلی // به‌روزرسانی group object
updateObject(id, updates); updateObject(id, updates);
// به‌روزرسانی همه اعضای گروه // به‌روزرسانی همه اعضای گروه
@@ -114,6 +149,10 @@ const EditorCanvas = () => {
y: (member.y || 0) + deltaY, y: (member.y || 0) + deltaY,
}); });
}); });
} else if (obj.groupId && (updates.x !== undefined || updates.y !== undefined)) {
// اگر object در یک گروه است و position تغییر کرد، فقط object را به‌روزرسانی کن
// bounds group فقط موقع transform به‌روزرسانی می‌شود
updateObject(id, updates);
} else { } else {
updateObject(id, updates); updateObject(id, updates);
} }
@@ -127,78 +166,248 @@ const EditorCanvas = () => {
ungroupObjects(groupId); ungroupObjects(groupId);
}; };
// Attach transformer to selected object when it changes // Attach transformer to selected objects when they change
useEffect(() => { useEffect(() => {
if (!transformerRef.current || !selectedObjectId || !layerRef.current) return; if (!transformerRef.current || selectedObjectIds.length === 0 || !layerRef.current) {
if (transformerRef.current) {
const selectedObject = objects.find((obj) => obj.id === selectedObjectId); transformerRef.current.nodes([]);
if (!selectedObject) return;
// For grid objects, handle separately
if (selectedObject.type === "grid") {
const tableNode = layerRef.current.findOne(`#${selectedObjectId}`);
if (tableNode && transformerRef.current) {
transformerRef.current.nodes([tableNode]);
transformerRef.current.getLayer()?.batchDraw(); transformerRef.current.getLayer()?.batchDraw();
} }
return; return;
} }
// For other objects, find the node by id and attach transformer // Use a small delay to ensure the nodes are mounted (especially for images that need to load)
// Use a small delay to ensure the node is mounted (especially for images that need to load)
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
if (!layerRef.current || !transformerRef.current) return; if (!layerRef.current || !transformerRef.current) return;
// اگر object با mask گروه شده، transformer را به Group متصل کن const nodesToTransform: Konva.Node[] = [];
const maskedGroup = layerRef.current.findOne(`#masked-${selectedObjectId}`);
const node = maskedGroup || layerRef.current.findOne(`#${selectedObjectId}`);
if (node && transformerRef.current) { selectedObjectIds.forEach((objId) => {
transformerRef.current.nodes([node]); const selectedObject = objects.find((obj) => obj.id === objId);
if (!selectedObject) return;
// For all objects, find by id (group, grid, or regular)
if (selectedObject.type === "group" || selectedObject.type === "grid") {
const node = layerRef.current?.findOne(`#${objId}`);
if (node) {
nodesToTransform.push(node);
}
} else {
// اگر object با mask گروه شده، transformer را به Group متصل کن
const maskedGroup = layerRef.current?.findOne(`#masked-${objId}`);
const regularNode = layerRef.current?.findOne(`#${objId}`);
const targetNode = maskedGroup || regularNode;
if (targetNode) {
nodesToTransform.push(targetNode);
}
}
});
if (nodesToTransform.length > 0) {
transformerRef.current.nodes(nodesToTransform);
transformerRef.current.getLayer()?.batchDraw(); transformerRef.current.getLayer()?.batchDraw();
} }
}, 100); }, 100);
return () => clearTimeout(timeoutId); return () => clearTimeout(timeoutId);
}, [selectedObjectId, objects, transformerRef, layerRef]); }, [selectedObjectIds, objects, transformerRef, layerRef]);
useEffect(() => { useEffect(() => {
if (!transformerRef.current || !selectedObjectId) return; if (!transformerRef.current || selectedObjectIds.length === 0) return;
const selectedObject = objects.find((obj) => obj.id === selectedObjectId);
if (!selectedObject) return;
const transformer = transformerRef.current; const transformer = transformerRef.current;
// Handle grid objects separately const handleTransform = () => {
if (selectedObject.type === "grid") { const nodes = transformer.nodes();
const handleTransformEnd = () => { if (nodes.length === 0 || !layerRef.current) return;
const node = transformer.nodes()[0];
if (!node || !selectedObject) return; nodes.forEach((node) => {
const nodeId = node.id();
const selectedObject = objects.find((obj) => obj.id === nodeId);
if (!selectedObject || selectedObject.type !== "group") return;
const scaleX = node.scaleX(); const scaleX = node.scaleX();
const scaleY = node.scaleY(); const scaleY = node.scaleY();
const newX = node.x();
const newY = node.y();
node.scaleX(1); const groupMembers = objects.filter((o) => o.groupId === selectedObject.id);
node.scaleY(1); const oldGroupX = selectedObject.x || 0;
const oldGroupY = selectedObject.y || 0;
const newWidth = (selectedObject.width || 300) * scaleX; // Update Konva nodes directly for live preview
const newHeight = (selectedObject.height || 200) * scaleY; groupMembers.forEach((member) => {
const relativeX = (member.x || 0) - oldGroupX;
const relativeY = (member.y || 0) - oldGroupY;
const scaledX = relativeX * scaleX;
const scaledY = relativeY * scaleY;
applyTableResize(selectedObject.id, newWidth, newHeight); // Find the Konva node for this member
updateObject(selectedObject.id, { const memberNode = layerRef.current?.findOne(`#${member.id}`) ||
x: node.x(), layerRef.current?.findOne(`#masked-${member.id}`);
y: node.y(), if (memberNode) {
memberNode.x(newX + scaledX);
memberNode.y(newY + scaledY);
memberNode.width((member.width || 100) * scaleX);
memberNode.height((member.height || 100) * scaleY);
memberNode.scaleX(1);
memberNode.scaleY(1);
}
}); });
};
transformer.on("transformend", handleTransformEnd); layerRef.current?.batchDraw();
});
};
return () => { const handleTransformEnd = () => {
transformer.off("transformend", handleTransformEnd); const nodes = transformer.nodes();
}; if (nodes.length === 0) return;
}
}, [selectedObjectId, objects, transformerRef, applyTableResize, updateObject]); nodes.forEach((node) => {
const nodeId = node.id();
const selectedObject = objects.find((obj) => obj.id === nodeId);
if (!selectedObject) return;
// Handle group objects
if (selectedObject.type === "group") {
const scaleX = node.scaleX();
const scaleY = node.scaleY();
const rotation = node.rotation();
const newX = node.x();
const newY = node.y();
node.scaleX(1);
node.scaleY(1);
node.rotation(0);
const groupMembers = objects.filter((o) => o.groupId === selectedObject.id);
const oldGroupX = selectedObject.x || 0;
const oldGroupY = selectedObject.y || 0;
const oldGroupWidth = selectedObject.width || 100;
const oldGroupHeight = selectedObject.height || 100;
// محاسبه bounds جدید بر اساا member های update شده
const updatedMembers = groupMembers.map((member) => {
const relativeX = (member.x || 0) - oldGroupX;
const relativeY = (member.y || 0) - oldGroupY;
// Apply rotation
const angle = (rotation * Math.PI) / 180;
const rotatedX = relativeX * Math.cos(angle) - relativeY * Math.sin(angle);
const rotatedY = relativeX * Math.sin(angle) + relativeY * Math.cos(angle);
// Apply scale
const scaledX = rotatedX * scaleX;
const scaledY = rotatedY * scaleY;
return {
id: member.id,
x: newX + scaledX,
y: newY + scaledY,
rotation: (member.rotation || 0) + rotation,
width: (member.width || 100) * scaleX,
height: (member.height || 100) * scaleY,
};
});
// Update all members
updatedMembers.forEach((member) => {
updateObject(member.id, {
x: member.x,
y: member.y,
rotation: member.rotation,
width: member.width,
height: member.height,
});
});
// محاسبه bounds گروه بر اساس member های update شده با در نظر گرفتن rotation
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
updatedMembers.forEach((m) => {
const cx = (m.x || 0) + (m.width || 0) / 2;
const cy = (m.y || 0) + (m.height || 0) / 2;
const w = m.width || 0;
const h = m.height || 0;
const rot = ((m.rotation || 0) * Math.PI) / 180;
// محاسبه چهار گوشه rectangle با rotation
const corners = [
{ x: -w / 2, y: -h / 2 },
{ x: w / 2, y: -h / 2 },
{ x: w / 2, y: h / 2 },
{ x: -w / 2, y: h / 2 },
];
corners.forEach((corner) => {
const rotatedX = corner.x * Math.cos(rot) - corner.y * Math.sin(rot);
const rotatedY = corner.x * Math.sin(rot) + corner.y * Math.cos(rot);
const finalX = cx + rotatedX;
const finalY = cy + rotatedY;
minX = Math.min(minX, finalX);
minY = Math.min(minY, finalY);
maxX = Math.max(maxX, finalX);
maxY = Math.max(maxY, finalY);
});
});
const groupWidth = maxX - minX;
const groupHeight = maxY - minY;
// Update group object with new bounds (rotation should remain 0)
updateObject(selectedObject.id, {
x: minX,
y: minY,
width: groupWidth,
height: groupHeight,
rotation: 0,
});
} else if (selectedObject.type === "grid") {
// Handle grid objects separately
const scaleX = node.scaleX();
const scaleY = node.scaleY();
node.scaleX(1);
node.scaleY(1);
const newWidth = (selectedObject.width || 300) * scaleX;
const newHeight = (selectedObject.height || 200) * scaleY;
applyTableResize(selectedObject.id, newWidth, newHeight);
updateObject(selectedObject.id, {
x: node.x(),
y: node.y(),
});
} else {
// Handle other objects
const scaleX = node.scaleX();
const scaleY = node.scaleY();
node.scaleX(1);
node.scaleY(1);
updateObject(selectedObject.id, {
x: node.x(),
y: node.y(),
rotation: node.rotation(),
width: Math.max(5, (selectedObject.width || 100) * scaleX),
height: Math.max(5, (selectedObject.height || 100) * scaleY),
});
}
});
};
transformer.on("transform", handleTransform);
transformer.on("transformend", handleTransformEnd);
return () => {
transformer.off("transform", handleTransform);
transformer.off("transformend", handleTransformEnd);
};
}, [selectedObjectIds, objects, transformerRef, applyTableResize, updateObject]);
const finalScale = stageSize.scale * zoom; const finalScale = stageSize.scale * zoom;
@@ -227,26 +436,76 @@ const EditorCanvas = () => {
</Layer> </Layer>
<Layer ref={layerRef}> <Layer ref={layerRef}>
{objects.map((obj) => ( {/* Render group objects first */}
<ObjectRenderer {objects
key={obj.id} .filter((obj) => obj.type === "group")
obj={obj} .map((obj) => (
isSelected={selectedObjectId === obj.id} <ObjectRenderer
transformerRef={transformerRef} key={obj.id}
layerRef={layerRef} obj={obj}
onSelect={handleSelect} isSelected={selectedObjectIds.includes(obj.id)}
onUpdate={handleObjectUpdate} transformerRef={transformerRef}
draggable={tool === "select"} layerRef={layerRef}
onCellClick={handleCellClick} onSelect={handleSelect}
onCellDblClick={handleCellDblClick} onUpdate={handleObjectUpdate}
selectedCellId={selectedCellId} draggable={tool === "select"}
allObjects={objects} onCellClick={handleCellClick}
/> onCellDblClick={handleCellDblClick}
))} selectedCellId={selectedCellId}
allObjects={objects}
/>
))}
{/* Render non-grouped objects */}
{objects
.filter((obj) => !obj.groupId && obj.type !== "group")
.map((obj) => (
<ObjectRenderer
key={obj.id}
obj={obj}
isSelected={selectedObjectIds.includes(obj.id)}
transformerRef={transformerRef}
layerRef={layerRef}
onSelect={handleSelect}
onUpdate={handleObjectUpdate}
draggable={tool === "select"}
onCellClick={handleCellClick}
onCellDblClick={handleCellDblClick}
selectedCellId={selectedCellId}
allObjects={objects}
/>
))}
{/* Render objects inside groups - they should not be interactive */}
{objects
.filter((obj) => obj.groupId && obj.type !== "group")
.map((obj) => {
const groupObject = objects.find((o) => o.id === obj.groupId);
if (!groupObject) return null;
// Render with absolute position but wrapped in non-listening group
return (
<Group key={`group-member-${obj.id}`} listening={false}>
<ObjectRenderer
key={obj.id}
obj={obj}
isSelected={false}
transformerRef={transformerRef}
layerRef={layerRef}
onSelect={handleSelect}
onUpdate={handleObjectUpdate}
draggable={false}
onCellClick={handleCellClick}
onCellDblClick={handleCellDblClick}
selectedCellId={selectedCellId}
allObjects={objects}
/>
</Group>
);
})}
{selectedObjectId && (() => { {selectedObjectId && (() => {
const selectedObject = objects.find(obj => obj.id === selectedObjectId); const selectedObject = objects.find(obj => obj.id === selectedObjectId);
const isText = selectedObject?.type === "text"; const isText = selectedObject?.type === "text";
const isGroup = selectedObject?.type === "group";
// بررسی امکان group/ungroup با mask // بررسی امکان group/ungroup با mask
const hasMask = selectedObject?.maskId; const hasMask = selectedObject?.maskId;
@@ -272,7 +531,7 @@ const EditorCanvas = () => {
borderStroke="#3b82f6" borderStroke="#3b82f6"
borderStrokeWidth={2} borderStrokeWidth={2}
anchorSize={8} anchorSize={8}
rotateEnabled={true} rotateEnabled={!isGroup}
enabledAnchors={[ enabledAnchors={[
"top-left", "top-left",
"top-right", "top-right",
@@ -21,7 +21,7 @@ type ObjectRendererProps = {
isSelected: boolean; isSelected: boolean;
transformerRef: React.RefObject<Konva.Transformer | null>; transformerRef: React.RefObject<Konva.Transformer | null>;
layerRef: React.RefObject<Konva.Layer | 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; onUpdate: (id: string, updates: Partial<EditorObject>) => void;
draggable: boolean; draggable: boolean;
onCellClick?: (tableId: string, cellId: string) => void; onCellClick?: (tableId: string, cellId: string) => void;
@@ -51,7 +51,8 @@ const ObjectRenderer = ({
// Apply caching for masked objects - REQUIRED for destination-in to work // Apply caching for masked objects - REQUIRED for destination-in to work
useEffect(() => { 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 // Use double requestAnimationFrame to ensure all changes (including stroke) are rendered before caching
let rafId1: number; let rafId1: number;
@@ -71,7 +72,7 @@ const ObjectRenderer = ({
return () => { return () => {
cancelAnimationFrame(rafId2); cancelAnimationFrame(rafId2);
if (rafId1) cancelAnimationFrame(rafId1); 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]); }, [shouldApplyMask, isSelected, maskShape?.x, maskShape?.y, maskShape?.width, maskShape?.height, maskShape?.rotation, obj.x, obj.y, obj.width, obj.height, obj.rotation, obj.maskInvert]);
@@ -222,8 +223,8 @@ const ObjectRenderer = ({
key={obj.id} key={obj.id}
obj={obj} obj={obj}
selectedCellId={selectedCellId || null} selectedCellId={selectedCellId || null}
onCellClick={(tableId, cellId, node) => { onCellClick={(tableId, cellId, node, e) => {
onSelect(tableId, node); onSelect(tableId, node, e);
onCellClick?.(tableId, cellId); onCellClick?.(tableId, cellId);
}} }}
onCellDblClick={(tableId, cellId, x, y, width, height, text) => { onCellDblClick={(tableId, cellId, x, y, width, height, text) => {
@@ -236,6 +237,52 @@ const ObjectRenderer = ({
/> />
); );
break; 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: default:
return null; return null;
} }
@@ -332,9 +379,9 @@ const ObjectRenderer = ({
x={obj.x} x={obj.x}
y={obj.y} y={obj.y}
draggable={draggable} draggable={draggable}
onClick={() => { onClick={(e) => {
if (groupRef.current) { if (groupRef.current) {
onSelect(obj.id, groupRef.current); onSelect(obj.id, groupRef.current, e);
} }
}} }}
onDragEnd={handleGroupDragEnd} onDragEnd={handleGroupDragEnd}
@@ -10,7 +10,7 @@ export const useDrawingHandlers = () => {
const [tempObject, setTempObject] = useState<EditorObject | null>(null); const [tempObject, setTempObject] = useState<EditorObject | null>(null);
const transformerRef = useRef<Konva.Transformer>(null); const transformerRef = useRef<Konva.Transformer>(null);
const { tool, addObject, updateObject, setSelectedObjectId, selectedObjectId, setTool, addTable, setSelectedTableId } = useEditorStore(); const { tool, addObject, updateObject, setSelectedObjectId, setSelectedObjectIds, selectedObjectId, selectedObjectIds, setTool, addTable, setSelectedTableId } = useEditorStore();
const { defaults } = useTextDefaultsStore(); const { defaults } = useTextDefaultsStore();
useEffect(() => { useEffect(() => {
@@ -19,7 +19,7 @@ export const useDrawingHandlers = () => {
}, [tool]); }, [tool]);
const clearSelection = () => { const clearSelection = () => {
setSelectedObjectId(null); setSelectedObjectIds([]);
if (transformerRef.current) { if (transformerRef.current) {
transformerRef.current.nodes([]); transformerRef.current.nodes([]);
transformerRef.current.getLayer()?.batchDraw(); transformerRef.current.getLayer()?.batchDraw();
@@ -32,7 +32,7 @@ export const useDrawingHandlers = () => {
const clickedOnEmpty = e.target === stage; const clickedOnEmpty = e.target === stage;
if (clickedOnEmpty && selectedObjectId) { if (clickedOnEmpty && selectedObjectIds.length > 0) {
clearSelection(); clearSelection();
return; return;
} }
@@ -44,7 +44,7 @@ export const useDrawingHandlers = () => {
return; return;
} }
if (selectedObjectId) { if (selectedObjectIds.length > 0) {
return; return;
} }
@@ -4,11 +4,14 @@ import { useEditorStore } from "../../store/editorStore";
export const useKeyboardMovement = () => { export const useKeyboardMovement = () => {
const { const {
selectedObjectId, selectedObjectId,
selectedObjectIds,
objects, objects,
updateObject, updateObject,
deleteObject, deleteObject,
tool, tool,
layerRef, layerRef,
groupObjects,
ungroupObjects,
} = useEditorStore(); } = useEditorStore();
useEffect(() => { useEffect(() => {
@@ -28,58 +31,84 @@ export const useKeyboardMovement = () => {
// Handle Delete and Backspace keys // Handle Delete and Backspace keys
if (e.key === "Delete" || e.key === "Backspace") { if (e.key === "Delete" || e.key === "Backspace") {
if (selectedObjectId) { if (selectedObjectIds.length > 0) {
e.preventDefault(); e.preventDefault();
deleteObject(selectedObjectId); selectedObjectIds.forEach(id => deleteObject(id));
return; return;
} }
} }
if (!selectedObjectId) { // Handle Command/Ctrl + G for grouping/ungrouping
if ((e.metaKey || e.ctrlKey) && e.key === "g") {
e.preventDefault();
if (selectedObjectIds.length === 1) {
const selectedObject = objects.find((obj) => obj.id === selectedObjectIds[0]);
// اگر group انتخاب شده، ungroup کن
if (selectedObject?.type === "group") {
ungroupObjects(selectedObject.id);
return;
}
}
// اگر چند object انتخاب شده، group کن
if (selectedObjectIds.length >= 2) {
groupObjects(selectedObjectIds);
return;
}
}
if (selectedObjectIds.length === 0) {
return; return;
} }
const selectedObject = objects.find((obj) => obj.id === selectedObjectId);
if (!selectedObject) return;
const step = e.shiftKey ? 10 : 1; const step = e.shiftKey ? 10 : 1;
let newX = selectedObject.x || 0; let deltaX = 0;
let newY = selectedObject.y || 0; let deltaY = 0;
switch (e.key) { switch (e.key) {
case "ArrowUp": case "ArrowUp":
e.preventDefault(); e.preventDefault();
newY -= step; deltaY = -step;
break; break;
case "ArrowDown": case "ArrowDown":
e.preventDefault(); e.preventDefault();
newY += step; deltaY = step;
break; break;
case "ArrowLeft": case "ArrowLeft":
e.preventDefault(); e.preventDefault();
newX -= step; deltaX = -step;
break; break;
case "ArrowRight": case "ArrowRight":
e.preventDefault(); e.preventDefault();
newX += step; deltaX = step;
break; break;
default: default:
return; return;
} }
updateObject(selectedObjectId, { // Move all selected objects
x: newX, selectedObjectIds.forEach((id) => {
y: newY, const selectedObject = objects.find((obj) => obj.id === id);
}); if (!selectedObject) return;
if (layerRef?.current) { const newX = (selectedObject.x || 0) + deltaX;
const node = layerRef.current.findOne(`#${selectedObjectId}`); const newY = (selectedObject.y || 0) + deltaY;
if (node) {
node.x(newX); updateObject(id, {
node.y(newY); x: newX,
layerRef.current.batchDraw(); y: newY,
});
if (layerRef?.current) {
const maskedGroup = layerRef.current.findOne(`#masked-${id}`);
const regularNode = layerRef.current.findOne(`#${id}`);
const node = maskedGroup || regularNode;
if (node) {
node.x(newX);
node.y(newY);
layerRef.current.batchDraw();
}
} }
} });
}; };
window.addEventListener("keydown", handleKeyDown); window.addEventListener("keydown", handleKeyDown);
@@ -87,5 +116,5 @@ export const useKeyboardMovement = () => {
return () => { return () => {
window.removeEventListener("keydown", handleKeyDown); window.removeEventListener("keydown", handleKeyDown);
}; };
}, [selectedObjectId, objects, updateObject, deleteObject, tool, layerRef]); }, [selectedObjectIds, objects, updateObject, deleteObject, tool, layerRef, groupObjects, ungroupObjects]);
}; };
@@ -6,12 +6,7 @@ import type { ShapeProps } from "./types";
const AbstractShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, draggable }: ShapeProps) => { const AbstractShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, draggable }: ShapeProps) => {
const shapeRef = useRef<Konva.Star>(null); const shapeRef = useRef<Konva.Star>(null);
useEffect(() => { // Transformer is managed in EditorCanvas for multi-select support
if (isSelected && shapeRef.current && transformerRef.current) {
transformerRef.current.nodes([shapeRef.current]);
transformerRef.current.getLayer()?.batchDraw();
}
}, [isSelected, transformerRef]);
const isMask = obj.isMask || false; const isMask = obj.isMask || false;
const showGuide = obj.showMaskGuide !== false; const showGuide = obj.showMaskGuide !== false;
@@ -44,9 +39,9 @@ const AbstractShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, dr
dash={isMask && showGuide ? [10, 5] : undefined} dash={isMask && showGuide ? [10, 5] : undefined}
rotation={obj.rotation || 0} rotation={obj.rotation || 0}
draggable={draggable} draggable={draggable}
onClick={() => { onClick={(e) => {
if (shapeRef.current) { if (shapeRef.current) {
onSelect(obj.id, shapeRef.current); onSelect(obj.id, shapeRef.current, e);
} }
}} }}
onDragMove={(e) => { onDragMove={(e) => {
@@ -6,12 +6,7 @@ import type { ShapeProps } from "./types";
const ArrowShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, draggable }: ShapeProps) => { const ArrowShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, draggable }: ShapeProps) => {
const shapeRef = useRef<Konva.Arrow>(null); const shapeRef = useRef<Konva.Arrow>(null);
useEffect(() => { // Transformer is managed in EditorCanvas for multi-select support
if (isSelected && shapeRef.current && transformerRef.current) {
transformerRef.current.nodes([shapeRef.current]);
transformerRef.current.getLayer()?.batchDraw();
}
}, [isSelected, transformerRef]);
const startX = obj.x; const startX = obj.x;
const startY = obj.y; const startY = obj.y;
@@ -32,9 +27,9 @@ const ArrowShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, dragg
perfectDrawEnabled={false} perfectDrawEnabled={false}
rotation={obj.rotation || 0} rotation={obj.rotation || 0}
draggable={draggable} draggable={draggable}
onClick={() => { onClick={(e) => {
if (shapeRef.current) { if (shapeRef.current) {
onSelect(obj.id, shapeRef.current); onSelect(obj.id, shapeRef.current, e);
} }
}} }}
onDragEnd={(e) => { onDragEnd={(e) => {
@@ -6,12 +6,7 @@ import type { ShapeProps } from "./types";
const CircleShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, draggable }: ShapeProps) => { const CircleShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, draggable }: ShapeProps) => {
const shapeRef = useRef<Konva.Circle>(null); const shapeRef = useRef<Konva.Circle>(null);
useEffect(() => { // Transformer is managed in EditorCanvas for multi-select support
if (isSelected && shapeRef.current && transformerRef.current) {
transformerRef.current.nodes([shapeRef.current]);
transformerRef.current.getLayer()?.batchDraw();
}
}, [isSelected, transformerRef]);
const isMask = obj.isMask || false; const isMask = obj.isMask || false;
const showGuide = obj.showMaskGuide !== false; const showGuide = obj.showMaskGuide !== false;
@@ -38,9 +33,9 @@ const CircleShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, drag
dash={isMask && showGuide ? [10, 5] : undefined} dash={isMask && showGuide ? [10, 5] : undefined}
rotation={obj.rotation || 0} rotation={obj.rotation || 0}
draggable={draggable} draggable={draggable}
onClick={() => { onClick={(e) => {
if (shapeRef.current) { if (shapeRef.current) {
onSelect(obj.id, shapeRef.current); onSelect(obj.id, shapeRef.current, e);
} }
}} }}
onDragMove={(e) => { onDragMove={(e) => {
@@ -15,16 +15,7 @@ const ImageObject = ({
const [image] = useImage(obj.imageUrl || ""); const [image] = useImage(obj.imageUrl || "");
const shapeRef = useRef<Konva.Image>(null); const shapeRef = useRef<Konva.Image>(null);
useEffect(() => { // Transformer is managed in EditorCanvas for multi-select support
// اگر draggable=false باشد، یعنی mask اعمال شده و transformer باید به Group متصل شود (نه Image)
// پس این useEffect را اجرا نکن
if (!draggable) return;
if (isSelected && shapeRef.current && transformerRef.current && image) {
transformerRef.current.nodes([shapeRef.current]);
transformerRef.current.getLayer()?.batchDraw();
}
}, [isSelected, transformerRef, image, draggable]);
if (!image) return null; if (!image) return null;
@@ -42,10 +33,10 @@ const ImageObject = ({
strokeWidth={isSelected && draggable ? 3 : 0} strokeWidth={isSelected && draggable ? 3 : 0}
rotation={obj.rotation || 0} rotation={obj.rotation || 0}
draggable={draggable} draggable={draggable}
onClick={draggable ? () => { onClick={draggable ? (e) => {
// فقط وقتی mask اعمال نشده onClick handler را فعال کن // فقط وقتی mask اعمال نشده onClick handler را فعال کن
if (shapeRef.current) { if (shapeRef.current) {
onSelect(obj.id, shapeRef.current); onSelect(obj.id, shapeRef.current, e);
} }
} : undefined} } : undefined}
onDragEnd={(e) => { onDragEnd={(e) => {
@@ -6,12 +6,7 @@ import type { ShapeProps } from "./types";
const LineShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, draggable }: ShapeProps) => { const LineShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, draggable }: ShapeProps) => {
const shapeRef = useRef<Konva.Line>(null); const shapeRef = useRef<Konva.Line>(null);
useEffect(() => { // Transformer is managed in EditorCanvas for multi-select support
if (isSelected && shapeRef.current && transformerRef.current) {
transformerRef.current.nodes([shapeRef.current]);
transformerRef.current.getLayer()?.batchDraw();
}
}, [isSelected, transformerRef]);
const startX = obj.x; const startX = obj.x;
const startY = obj.y; const startY = obj.y;
@@ -30,9 +25,9 @@ const LineShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, dragga
perfectDrawEnabled={false} perfectDrawEnabled={false}
rotation={obj.rotation || 0} rotation={obj.rotation || 0}
draggable={draggable} draggable={draggable}
onClick={() => { onClick={(e) => {
if (shapeRef.current) { if (shapeRef.current) {
onSelect(obj.id, shapeRef.current); onSelect(obj.id, shapeRef.current, e);
} }
}} }}
onDragEnd={(e) => { onDragEnd={(e) => {
@@ -23,12 +23,7 @@ const LinkShape = ({
const shapeRef = useRef<Konva.Text>(null); const shapeRef = useRef<Konva.Text>(null);
const fontWeight = useMemo(() => getFontWeight(obj.fontWeight), [obj.fontWeight]); const fontWeight = useMemo(() => getFontWeight(obj.fontWeight), [obj.fontWeight]);
useEffect(() => { // Transformer is managed in EditorCanvas for multi-select support
if (isSelected && shapeRef.current && transformerRef.current) {
transformerRef.current.nodes([shapeRef.current]);
transformerRef.current.getLayer()?.batchDraw();
}
}, [isSelected, transformerRef]);
return ( return (
<Text <Text
@@ -43,11 +38,11 @@ const LinkShape = ({
underline={true} underline={true}
rotation={obj.rotation || 0} rotation={obj.rotation || 0}
draggable={draggable} draggable={draggable}
onClick={() => { onClick={(e) => {
// در حالت editor، لینک‌ها قابل کلیک نیستند // در حالت editor، لینک‌ها قابل کلیک نیستند
// این قابلیت در viewer اضافه خواهد شد // این قابلیت در viewer اضافه خواهد شد
if (shapeRef.current) { if (shapeRef.current) {
onSelect(obj.id, shapeRef.current); onSelect(obj.id, shapeRef.current, e);
} }
}} }}
onDragEnd={(e) => { onDragEnd={(e) => {
@@ -6,12 +6,7 @@ import type { ShapeProps } from "./types";
const RectangleShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, draggable }: ShapeProps) => { const RectangleShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, draggable }: ShapeProps) => {
const shapeRef = useRef<Konva.Rect>(null); const shapeRef = useRef<Konva.Rect>(null);
useEffect(() => { // Transformer is managed in EditorCanvas for multi-select support
if (isSelected && shapeRef.current && transformerRef.current) {
transformerRef.current.nodes([shapeRef.current]);
transformerRef.current.getLayer()?.batchDraw();
}
}, [isSelected, transformerRef]);
const isMask = obj.isMask || false; const isMask = obj.isMask || false;
const showGuide = obj.showMaskGuide !== false; const showGuide = obj.showMaskGuide !== false;
@@ -39,9 +34,9 @@ const RectangleShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, d
dash={isMask && showGuide ? [10, 5] : undefined} dash={isMask && showGuide ? [10, 5] : undefined}
rotation={obj.rotation || 0} rotation={obj.rotation || 0}
draggable={draggable} draggable={draggable}
onClick={() => { onClick={(e) => {
if (shapeRef.current) { if (shapeRef.current) {
onSelect(obj.id, shapeRef.current); onSelect(obj.id, shapeRef.current, e);
} }
}} }}
onDragMove={(e) => { onDragMove={(e) => {
@@ -44,25 +44,14 @@ const TextShape = ({
}: TextShapeProps) => { }: TextShapeProps) => {
const shapeRef = useRef<Konva.Text>(null); const shapeRef = useRef<Konva.Text>(null);
// Transformer is managed in EditorCanvas for multi-select support
// Set offset for text node
useEffect(() => { useEffect(() => {
if (isSelected && shapeRef.current && transformerRef.current) { if (shapeRef.current) {
const textNode = shapeRef.current; shapeRef.current.offsetX(0);
shapeRef.current.offsetY(0);
// تنظیم offset برای حذف فضای خالی
textNode.offsetX(0);
textNode.offsetY(0);
transformerRef.current.nodes([textNode]);
// به‌روزرسانی transformer بعد از یک تیک برای محاسبه دقیق bounds
setTimeout(() => {
if (transformerRef.current && shapeRef.current) {
transformerRef.current.forceUpdate();
transformerRef.current.getLayer()?.batchDraw();
}
}, 0);
} }
}, [isSelected, transformerRef, obj.text, obj.width, obj.height, obj.fontSize, obj.fontFamily, obj.fontWeight]); }, []);
const fontFamily = useMemo(() => getFontFamily(obj.fontFamily), [obj.fontFamily]); const fontFamily = useMemo(() => getFontFamily(obj.fontFamily), [obj.fontFamily]);
const fontWeight = useMemo(() => getFontWeight(obj.fontWeight), [obj.fontWeight]); const fontWeight = useMemo(() => getFontWeight(obj.fontWeight), [obj.fontWeight]);
@@ -84,9 +73,9 @@ const TextShape = ({
align="right" align="right"
rotation={obj.rotation || 0} rotation={obj.rotation || 0}
draggable={draggable} draggable={draggable}
onClick={() => { onClick={(e) => {
if (shapeRef.current) { if (shapeRef.current) {
onSelect(obj.id, shapeRef.current); onSelect(obj.id, shapeRef.current, e);
} }
}} }}
onDragEnd={(e) => { onDragEnd={(e) => {
@@ -6,12 +6,7 @@ import type { ShapeProps } from "./types";
const TriangleShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, draggable }: ShapeProps) => { const TriangleShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, draggable }: ShapeProps) => {
const shapeRef = useRef<Konva.RegularPolygon>(null); const shapeRef = useRef<Konva.RegularPolygon>(null);
useEffect(() => { // Transformer is managed in EditorCanvas for multi-select support
if (isSelected && shapeRef.current && transformerRef.current) {
transformerRef.current.nodes([shapeRef.current]);
transformerRef.current.getLayer()?.batchDraw();
}
}, [isSelected, transformerRef]);
const isMask = obj.isMask || false; const isMask = obj.isMask || false;
const showGuide = obj.showMaskGuide !== false; const showGuide = obj.showMaskGuide !== false;
@@ -40,9 +35,9 @@ const TriangleShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, dr
dash={isMask && showGuide ? [10, 5] : undefined} dash={isMask && showGuide ? [10, 5] : undefined}
rotation={obj.rotation || 0} rotation={obj.rotation || 0}
draggable={draggable} draggable={draggable}
onClick={() => { onClick={(e) => {
if (shapeRef.current) { if (shapeRef.current) {
onSelect(obj.id, shapeRef.current); onSelect(obj.id, shapeRef.current, e);
} }
}} }}
onDragMove={(e) => { onDragMove={(e) => {
@@ -54,12 +54,7 @@ const VideoShape = ({
}; };
}, [obj.videoUrl]); }, [obj.videoUrl]);
useEffect(() => { // Transformer is managed in EditorCanvas for multi-select support
if (isSelected && groupRef.current && transformerRef.current && image) {
transformerRef.current.nodes([groupRef.current]);
transformerRef.current.getLayer()?.batchDraw();
}
}, [isSelected, transformerRef, image]);
useEffect(() => { useEffect(() => {
if (!showVideoModal) return; if (!showVideoModal) return;
@@ -101,7 +96,7 @@ const VideoShape = ({
} }
} }
if (groupRef.current) { if (groupRef.current) {
onSelect(obj.id, groupRef.current); onSelect(obj.id, groupRef.current, e);
} }
}; };
+1 -1
View File
@@ -5,7 +5,7 @@ export type ShapeProps = {
obj: EditorObject; obj: EditorObject;
isSelected: boolean; isSelected: boolean;
transformerRef: React.RefObject<Konva.Transformer | null>; transformerRef: React.RefObject<Konva.Transformer | 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; onUpdate: (id: string, updates: Partial<EditorObject>) => void;
draggable: boolean; draggable: boolean;
}; };
+109 -8
View File
@@ -13,7 +13,8 @@ export type ToolType =
| "document" | "document"
| "video" | "video"
| "link" | "link"
| "grid"; | "grid"
| "group";
export type TableCell = { export type TableCell = {
id: string; id: string;
@@ -83,6 +84,10 @@ type EditorStoreType = {
duplicateObject: (id: string) => void; duplicateObject: (id: string) => void;
selectedObjectId: string | null; selectedObjectId: string | null;
setSelectedObjectId: (id: string | null) => void; setSelectedObjectId: (id: string | null) => void;
selectedObjectIds: string[];
setSelectedObjectIds: (ids: string[]) => void;
addToSelection: (id: string) => void;
removeFromSelection: (id: string) => void;
selectedTableId: string | null; selectedTableId: string | null;
setSelectedTableId: (id: string | null) => void; setSelectedTableId: (id: string | null) => void;
selectedCellId: string | null; selectedCellId: string | null;
@@ -301,7 +306,37 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
})); }));
}, },
selectedObjectId: null, selectedObjectId: null,
setSelectedObjectId: (id) => set({ selectedObjectId: id }), setSelectedObjectId: (id) => {
set({
selectedObjectId: id,
selectedObjectIds: id ? [id] : []
});
},
selectedObjectIds: [],
setSelectedObjectIds: (ids) => {
set({
selectedObjectIds: ids,
selectedObjectId: ids.length > 0 ? ids[0] : null
});
},
addToSelection: (id) => {
const state = get();
if (!state.selectedObjectIds.includes(id)) {
const newIds = [...state.selectedObjectIds, id];
set({
selectedObjectIds: newIds,
selectedObjectId: newIds[0]
});
}
},
removeFromSelection: (id) => {
const state = get();
const newIds = state.selectedObjectIds.filter(selectedId => selectedId !== id);
set({
selectedObjectIds: newIds,
selectedObjectId: newIds.length > 0 ? newIds[0] : null
});
},
selectedTableId: null, selectedTableId: null,
setSelectedTableId: (id) => set({ selectedTableId: id }), setSelectedTableId: (id) => set({ selectedTableId: id }),
selectedCellId: null, selectedCellId: null,
@@ -650,6 +685,57 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
if (objectIds.length < 2) return; if (objectIds.length < 2) return;
const groupId = `group-${crypto.randomUUID()}`; const groupId = `group-${crypto.randomUUID()}`;
const objectsToGroup = state.objects.filter((obj) => objectIds.includes(obj.id));
if (objectsToGroup.length === 0) return;
// محاسبه bounds گروه با در نظر گرفتن rotation
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
objectsToGroup.forEach((obj) => {
const cx = (obj.x || 0) + (obj.width || 0) / 2;
const cy = (obj.y || 0) + (obj.height || 0) / 2;
const w = obj.width || 0;
const h = obj.height || 0;
const rot = ((obj.rotation || 0) * Math.PI) / 180;
// محاسبه چهار گوشه rectangle با rotation
const corners = [
{ x: -w / 2, y: -h / 2 },
{ x: w / 2, y: -h / 2 },
{ x: w / 2, y: h / 2 },
{ x: -w / 2, y: h / 2 },
];
corners.forEach((corner) => {
const rotatedX = corner.x * Math.cos(rot) - corner.y * Math.sin(rot);
const rotatedY = corner.x * Math.sin(rot) + corner.y * Math.cos(rot);
const finalX = cx + rotatedX;
const finalY = cy + rotatedY;
minX = Math.min(minX, finalX);
minY = Math.min(minY, finalY);
maxX = Math.max(maxX, finalX);
maxY = Math.max(maxY, finalY);
});
});
const groupWidth = maxX - minX;
const groupHeight = maxY - minY;
// ایجاد group object
const groupObject: EditorObject = {
id: groupId,
type: "group",
x: minX,
y: minY,
width: groupWidth,
height: groupHeight,
};
// اضافه کردن groupId به objectهای داخل گروه
const updateObjects = (objects: EditorObject[]) => const updateObjects = (objects: EditorObject[]) =>
objects.map((obj) => objects.map((obj) =>
objectIds.includes(obj.id) ? { ...obj, groupId } : obj objectIds.includes(obj.id) ? { ...obj, groupId } : obj
@@ -657,25 +743,32 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
if (state.currentPageId) { if (state.currentPageId) {
set((state) => ({ set((state) => ({
objects: updateObjects(state.objects), objects: [...updateObjects(state.objects), groupObject],
pages: state.pages.map((p) => pages: state.pages.map((p) =>
p.id === state.currentPageId p.id === state.currentPageId
? { ...p, objects: updateObjects(p.objects) } ? { ...p, objects: [...updateObjects(p.objects), groupObject] }
: p : p
), ),
selectedObjectId: groupId,
selectedObjectIds: [groupId],
})); }));
return; return;
} }
set((state) => ({ set((state) => ({
objects: updateObjects(state.objects), objects: [...updateObjects(state.objects), groupObject],
selectedObjectId: groupId,
selectedObjectIds: [groupId],
})); }));
}, },
ungroupObjects: (groupId) => { ungroupObjects: (groupId) => {
const state = get(); const state = get();
// حذف groupId از objectهای داخل گروه و حذف group object
const updateObjects = (objects: EditorObject[]) => const updateObjects = (objects: EditorObject[]) =>
objects.map((obj) => objects
obj.groupId === groupId ? { ...obj, groupId: undefined } : obj .map((obj) =>
); obj.groupId === groupId ? { ...obj, groupId: undefined } : obj
)
.filter((obj) => obj.id !== groupId); // حذف group object
if (state.currentPageId) { if (state.currentPageId) {
set((state) => ({ set((state) => ({
@@ -685,12 +778,20 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
? { ...p, objects: updateObjects(p.objects) } ? { ...p, objects: updateObjects(p.objects) }
: p : p
), ),
selectedObjectId: null,
selectedObjectIds: [],
})); }));
return; return;
} }
set((state) => ({ set((state) => ({
objects: updateObjects(state.objects), objects: updateObjects(state.objects),
selectedObjectId: null,
selectedObjectIds: [],
})); }));
}, },
getGroupObjects: (groupId) => {
const state = get();
return state.objects.filter((obj) => obj.groupId === groupId);
},
}; };
}); });