base group
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user