base group
This commit is contained in:
@@ -6,7 +6,7 @@ import type { EditorObject } from "@/pages/editor/store/editorStore";
|
||||
type TableProps = {
|
||||
obj: EditorObject;
|
||||
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;
|
||||
onDragEnd: (tableId: string, x: number, y: number) => void;
|
||||
draggable: boolean;
|
||||
@@ -36,7 +36,7 @@ const Table = ({
|
||||
const handleCellClick = (cellId: string, e: Konva.KonvaEventObject<MouseEvent>) => {
|
||||
e.cancelBubble = true;
|
||||
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 Konva from "konva";
|
||||
import { useEditorStore, type EditorObject } from "../store/editorStore";
|
||||
@@ -18,7 +18,11 @@ const EditorCanvas = () => {
|
||||
objects,
|
||||
updateObject,
|
||||
selectedObjectId,
|
||||
selectedObjectIds,
|
||||
setSelectedObjectId,
|
||||
setSelectedObjectIds,
|
||||
addToSelection,
|
||||
removeFromSelection,
|
||||
setSelectedTableId,
|
||||
selectedCellId,
|
||||
setSelectedCellId,
|
||||
@@ -46,37 +50,68 @@ const EditorCanvas = () => {
|
||||
}
|
||||
}, [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 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") {
|
||||
setSelectedTableId(id);
|
||||
} else {
|
||||
} else if (!isMultiSelect) {
|
||||
setSelectedTableId(null);
|
||||
}
|
||||
setSelectedCellId(null);
|
||||
if (transformerRef.current && layerRef.current) {
|
||||
// اگر object با mask گروه شده، transformer را به Group متصل کن
|
||||
const maskedGroup = layerRef.current.findOne(`#masked-${id}`);
|
||||
const targetNode = maskedGroup || node;
|
||||
transformerRef.current.nodes([targetNode]);
|
||||
transformerRef.current.getLayer()?.batchDraw();
|
||||
|
||||
if (!isMultiSelect) {
|
||||
setSelectedCellId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCellClick = (tableId: string, cellId: string) => {
|
||||
setSelectedTableId(tableId);
|
||||
setSelectedObjectId(tableId);
|
||||
setSelectedCellId(cellId);
|
||||
const obj = objects.find((o) => o.id === tableId);
|
||||
if (obj && transformerRef.current && layerRef.current) {
|
||||
const tableNode = layerRef.current.findOne(`#${tableId}`);
|
||||
if (tableNode) {
|
||||
transformerRef.current.nodes([tableNode]);
|
||||
transformerRef.current.getLayer()?.batchDraw();
|
||||
const handleCellClick = (tableId: string, cellId: string, node?: Konva.Node, e?: Konva.KonvaEventObject<MouseEvent>) => {
|
||||
const isMultiSelect = e?.evt?.metaKey || e?.evt?.ctrlKey || e?.evt?.shiftKey;
|
||||
|
||||
if (isMultiSelect) {
|
||||
// انتخاب چندتایی
|
||||
if (selectedObjectIds.includes(tableId)) {
|
||||
removeFromSelection(tableId);
|
||||
} else {
|
||||
addToSelection(tableId);
|
||||
}
|
||||
} else {
|
||||
setSelectedTableId(tableId);
|
||||
setSelectedObjectId(tableId);
|
||||
}
|
||||
|
||||
setSelectedCellId(cellId);
|
||||
};
|
||||
|
||||
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);
|
||||
if (!obj) return;
|
||||
|
||||
// اگر object در یک گروه است و position تغییر کرد، همه اعضای گروه را جابجا کن
|
||||
if (obj.groupId && (updates.x !== undefined || updates.y !== undefined)) {
|
||||
const groupMembers = objects.filter((o) => o.groupId === obj.groupId && o.id !== id);
|
||||
// اگر group object است و position تغییر کرد، همه objectهای داخل group را جابجا کن
|
||||
if (obj.type === "group" && (updates.x !== undefined || updates.y !== undefined)) {
|
||||
const groupMembers = objects.filter((o) => o.groupId === id);
|
||||
const deltaX = updates.x !== undefined ? updates.x - (obj.x || 0) : 0;
|
||||
const deltaY = updates.y !== undefined ? updates.y - (obj.y || 0) : 0;
|
||||
|
||||
// بهروزرسانی object اصلی
|
||||
// بهروزرسانی group object
|
||||
updateObject(id, updates);
|
||||
|
||||
// بهروزرسانی همه اعضای گروه
|
||||
@@ -114,6 +149,10 @@ const EditorCanvas = () => {
|
||||
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 {
|
||||
updateObject(id, updates);
|
||||
}
|
||||
@@ -127,78 +166,248 @@ const EditorCanvas = () => {
|
||||
ungroupObjects(groupId);
|
||||
};
|
||||
|
||||
// Attach transformer to selected object when it changes
|
||||
// Attach transformer to selected objects when they change
|
||||
useEffect(() => {
|
||||
if (!transformerRef.current || !selectedObjectId || !layerRef.current) return;
|
||||
|
||||
const selectedObject = objects.find((obj) => obj.id === selectedObjectId);
|
||||
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]);
|
||||
if (!transformerRef.current || selectedObjectIds.length === 0 || !layerRef.current) {
|
||||
if (transformerRef.current) {
|
||||
transformerRef.current.nodes([]);
|
||||
transformerRef.current.getLayer()?.batchDraw();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// For other objects, find the node by id and attach transformer
|
||||
// Use a small delay to ensure the node is mounted (especially for images that need to load)
|
||||
// Use a small delay to ensure the nodes are mounted (especially for images that need to load)
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (!layerRef.current || !transformerRef.current) return;
|
||||
|
||||
// اگر object با mask گروه شده، transformer را به Group متصل کن
|
||||
const maskedGroup = layerRef.current.findOne(`#masked-${selectedObjectId}`);
|
||||
const node = maskedGroup || layerRef.current.findOne(`#${selectedObjectId}`);
|
||||
const nodesToTransform: Konva.Node[] = [];
|
||||
|
||||
selectedObjectIds.forEach((objId) => {
|
||||
const selectedObject = objects.find((obj) => obj.id === objId);
|
||||
if (!selectedObject) return;
|
||||
|
||||
if (node && transformerRef.current) {
|
||||
transformerRef.current.nodes([node]);
|
||||
// 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();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [selectedObjectId, objects, transformerRef, layerRef]);
|
||||
}, [selectedObjectIds, objects, transformerRef, layerRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!transformerRef.current || !selectedObjectId) return;
|
||||
|
||||
const selectedObject = objects.find((obj) => obj.id === selectedObjectId);
|
||||
if (!selectedObject) return;
|
||||
if (!transformerRef.current || selectedObjectIds.length === 0) return;
|
||||
|
||||
const transformer = transformerRef.current;
|
||||
|
||||
// Handle grid objects separately
|
||||
if (selectedObject.type === "grid") {
|
||||
const handleTransformEnd = () => {
|
||||
const node = transformer.nodes()[0];
|
||||
if (!node || !selectedObject) return;
|
||||
const handleTransform = () => {
|
||||
const nodes = transformer.nodes();
|
||||
if (nodes.length === 0 || !layerRef.current) 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 scaleY = node.scaleY();
|
||||
const newX = node.x();
|
||||
const newY = node.y();
|
||||
|
||||
node.scaleX(1);
|
||||
node.scaleY(1);
|
||||
const groupMembers = objects.filter((o) => o.groupId === selectedObject.id);
|
||||
const oldGroupX = selectedObject.x || 0;
|
||||
const oldGroupY = selectedObject.y || 0;
|
||||
|
||||
const newWidth = (selectedObject.width || 300) * scaleX;
|
||||
const newHeight = (selectedObject.height || 200) * scaleY;
|
||||
// Update Konva nodes directly for live preview
|
||||
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);
|
||||
updateObject(selectedObject.id, {
|
||||
x: node.x(),
|
||||
y: node.y(),
|
||||
// Find the Konva node for this member
|
||||
const memberNode = layerRef.current?.findOne(`#${member.id}`) ||
|
||||
layerRef.current?.findOne(`#masked-${member.id}`);
|
||||
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 () => {
|
||||
transformer.off("transformend", handleTransformEnd);
|
||||
};
|
||||
}
|
||||
}, [selectedObjectId, objects, transformerRef, applyTableResize, updateObject]);
|
||||
const handleTransformEnd = () => {
|
||||
const nodes = transformer.nodes();
|
||||
if (nodes.length === 0) return;
|
||||
|
||||
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;
|
||||
|
||||
@@ -227,26 +436,76 @@ const EditorCanvas = () => {
|
||||
</Layer>
|
||||
|
||||
<Layer ref={layerRef}>
|
||||
{objects.map((obj) => (
|
||||
<ObjectRenderer
|
||||
key={obj.id}
|
||||
obj={obj}
|
||||
isSelected={selectedObjectId === obj.id}
|
||||
transformerRef={transformerRef}
|
||||
layerRef={layerRef}
|
||||
onSelect={handleSelect}
|
||||
onUpdate={handleObjectUpdate}
|
||||
draggable={tool === "select"}
|
||||
onCellClick={handleCellClick}
|
||||
onCellDblClick={handleCellDblClick}
|
||||
selectedCellId={selectedCellId}
|
||||
allObjects={objects}
|
||||
/>
|
||||
))}
|
||||
{/* Render group objects first */}
|
||||
{objects
|
||||
.filter((obj) => 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 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 && (() => {
|
||||
const selectedObject = objects.find(obj => obj.id === selectedObjectId);
|
||||
const isText = selectedObject?.type === "text";
|
||||
const isGroup = selectedObject?.type === "group";
|
||||
|
||||
// بررسی امکان group/ungroup با mask
|
||||
const hasMask = selectedObject?.maskId;
|
||||
@@ -272,7 +531,7 @@ const EditorCanvas = () => {
|
||||
borderStroke="#3b82f6"
|
||||
borderStrokeWidth={2}
|
||||
anchorSize={8}
|
||||
rotateEnabled={true}
|
||||
rotateEnabled={!isGroup}
|
||||
enabledAnchors={[
|
||||
"top-left",
|
||||
"top-right",
|
||||
|
||||
@@ -21,7 +21,7 @@ type ObjectRendererProps = {
|
||||
isSelected: boolean;
|
||||
transformerRef: React.RefObject<Konva.Transformer | null>;
|
||||
layerRef: React.RefObject<Konva.Layer | null>;
|
||||
onSelect: (id: string, node: Konva.Node) => void;
|
||||
onSelect: (id: string, node: Konva.Node, e?: Konva.KonvaEventObject<MouseEvent>) => void;
|
||||
onUpdate: (id: string, updates: Partial<EditorObject>) => void;
|
||||
draggable: boolean;
|
||||
onCellClick?: (tableId: string, cellId: string) => void;
|
||||
@@ -51,17 +51,18 @@ const ObjectRenderer = ({
|
||||
|
||||
// Apply caching for masked objects - REQUIRED for destination-in to work
|
||||
useEffect(() => {
|
||||
if (!groupRef.current || !shouldApplyMask) return;
|
||||
const groupNode = groupRef.current;
|
||||
if (!groupNode || !shouldApplyMask) return;
|
||||
|
||||
// Use double requestAnimationFrame to ensure all changes (including stroke) are rendered before caching
|
||||
let rafId1: number;
|
||||
const rafId2 = requestAnimationFrame(() => {
|
||||
rafId1 = requestAnimationFrame(() => {
|
||||
if (!groupRef.current) return;
|
||||
|
||||
|
||||
// Clear cache first
|
||||
groupRef.current.clearCache();
|
||||
|
||||
|
||||
// Apply cache - Konva will automatically calculate bounds
|
||||
groupRef.current.cache();
|
||||
groupRef.current.getLayer()?.batchDraw();
|
||||
@@ -71,7 +72,7 @@ const ObjectRenderer = ({
|
||||
return () => {
|
||||
cancelAnimationFrame(rafId2);
|
||||
if (rafId1) cancelAnimationFrame(rafId1);
|
||||
groupRef.current?.clearCache();
|
||||
groupNode?.clearCache();
|
||||
};
|
||||
}, [shouldApplyMask, isSelected, maskShape?.x, maskShape?.y, maskShape?.width, maskShape?.height, maskShape?.rotation, obj.x, obj.y, obj.width, obj.height, obj.rotation, obj.maskInvert]);
|
||||
|
||||
@@ -97,7 +98,7 @@ const ObjectRenderer = ({
|
||||
}
|
||||
|
||||
// برای objectهای با mask، یک obj جدید با موقعیت (0, 0) ایجاد کن
|
||||
const shapeObj = shouldApplyMask
|
||||
const shapeObj = shouldApplyMask
|
||||
? { ...obj, x: 0, y: 0 }
|
||||
: obj;
|
||||
|
||||
@@ -222,8 +223,8 @@ const ObjectRenderer = ({
|
||||
key={obj.id}
|
||||
obj={obj}
|
||||
selectedCellId={selectedCellId || null}
|
||||
onCellClick={(tableId, cellId, node) => {
|
||||
onSelect(tableId, node);
|
||||
onCellClick={(tableId, cellId, node, e) => {
|
||||
onSelect(tableId, node, e);
|
||||
onCellClick?.(tableId, cellId);
|
||||
}}
|
||||
onCellDblClick={(tableId, cellId, x, y, width, height, text) => {
|
||||
@@ -236,6 +237,52 @@ const ObjectRenderer = ({
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case "group":
|
||||
// Render group as a simple selection box
|
||||
// Objects inside group are rendered separately in EditorCanvas
|
||||
shapeElement = (
|
||||
<Group
|
||||
key={obj.id}
|
||||
id={obj.id}
|
||||
name="group-container"
|
||||
x={obj.x}
|
||||
y={obj.y}
|
||||
draggable={draggable}
|
||||
onClick={(e) => {
|
||||
const groupNode = e.currentTarget;
|
||||
if (groupNode) {
|
||||
onSelect(obj.id, groupNode, e);
|
||||
}
|
||||
}}
|
||||
onDragMove={(e) => {
|
||||
const node = e.target;
|
||||
onUpdate(obj.id, {
|
||||
x: node.x(),
|
||||
y: node.y(),
|
||||
});
|
||||
}}
|
||||
onDragEnd={(e) => {
|
||||
const node = e.target;
|
||||
onUpdate(obj.id, {
|
||||
x: node.x(),
|
||||
y: node.y(),
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Rect
|
||||
x={0}
|
||||
y={0}
|
||||
width={obj.width || 100}
|
||||
height={obj.height || 100}
|
||||
fill="transparent"
|
||||
stroke={isSelected ? "#3b82f6" : "#e5e7eb"}
|
||||
strokeWidth={isSelected ? 2 : 1}
|
||||
dash={[5, 5]}
|
||||
listening={true}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -250,7 +297,7 @@ const ObjectRenderer = ({
|
||||
// The mask layer has listening={false} to not interfere with selection
|
||||
// Default: destination-out (overlap مخفی میشود)
|
||||
const compositeOp = obj.maskInvert === false ? "destination-in" : "destination-out";
|
||||
|
||||
|
||||
const handleGroupDragEnd = (e: Konva.KonvaEventObject<DragEvent>) => {
|
||||
const node = e.target;
|
||||
onUpdate(obj.id, {
|
||||
@@ -323,18 +370,18 @@ const ObjectRenderer = ({
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Group
|
||||
<Group
|
||||
ref={groupRef}
|
||||
id={`masked-${obj.id}`}
|
||||
name="masked-group"
|
||||
x={obj.x}
|
||||
y={obj.y}
|
||||
draggable={draggable}
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
if (groupRef.current) {
|
||||
onSelect(obj.id, groupRef.current);
|
||||
onSelect(obj.id, groupRef.current, e);
|
||||
}
|
||||
}}
|
||||
onDragEnd={handleGroupDragEnd}
|
||||
|
||||
@@ -10,7 +10,7 @@ export const useDrawingHandlers = () => {
|
||||
const [tempObject, setTempObject] = useState<EditorObject | null>(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();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -19,7 +19,7 @@ export const useDrawingHandlers = () => {
|
||||
}, [tool]);
|
||||
|
||||
const clearSelection = () => {
|
||||
setSelectedObjectId(null);
|
||||
setSelectedObjectIds([]);
|
||||
if (transformerRef.current) {
|
||||
transformerRef.current.nodes([]);
|
||||
transformerRef.current.getLayer()?.batchDraw();
|
||||
@@ -32,7 +32,7 @@ export const useDrawingHandlers = () => {
|
||||
|
||||
const clickedOnEmpty = e.target === stage;
|
||||
|
||||
if (clickedOnEmpty && selectedObjectId) {
|
||||
if (clickedOnEmpty && selectedObjectIds.length > 0) {
|
||||
clearSelection();
|
||||
return;
|
||||
}
|
||||
@@ -44,7 +44,7 @@ export const useDrawingHandlers = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedObjectId) {
|
||||
if (selectedObjectIds.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,14 @@ import { useEditorStore } from "../../store/editorStore";
|
||||
export const useKeyboardMovement = () => {
|
||||
const {
|
||||
selectedObjectId,
|
||||
selectedObjectIds,
|
||||
objects,
|
||||
updateObject,
|
||||
deleteObject,
|
||||
tool,
|
||||
layerRef,
|
||||
groupObjects,
|
||||
ungroupObjects,
|
||||
} = useEditorStore();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -28,58 +31,84 @@ export const useKeyboardMovement = () => {
|
||||
|
||||
// Handle Delete and Backspace keys
|
||||
if (e.key === "Delete" || e.key === "Backspace") {
|
||||
if (selectedObjectId) {
|
||||
if (selectedObjectIds.length > 0) {
|
||||
e.preventDefault();
|
||||
deleteObject(selectedObjectId);
|
||||
selectedObjectIds.forEach(id => deleteObject(id));
|
||||
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;
|
||||
}
|
||||
|
||||
const selectedObject = objects.find((obj) => obj.id === selectedObjectId);
|
||||
if (!selectedObject) return;
|
||||
|
||||
const step = e.shiftKey ? 10 : 1;
|
||||
let newX = selectedObject.x || 0;
|
||||
let newY = selectedObject.y || 0;
|
||||
let deltaX = 0;
|
||||
let deltaY = 0;
|
||||
|
||||
switch (e.key) {
|
||||
case "ArrowUp":
|
||||
e.preventDefault();
|
||||
newY -= step;
|
||||
deltaY = -step;
|
||||
break;
|
||||
case "ArrowDown":
|
||||
e.preventDefault();
|
||||
newY += step;
|
||||
deltaY = step;
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
e.preventDefault();
|
||||
newX -= step;
|
||||
deltaX = -step;
|
||||
break;
|
||||
case "ArrowRight":
|
||||
e.preventDefault();
|
||||
newX += step;
|
||||
deltaX = step;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
updateObject(selectedObjectId, {
|
||||
x: newX,
|
||||
y: newY,
|
||||
});
|
||||
// Move all selected objects
|
||||
selectedObjectIds.forEach((id) => {
|
||||
const selectedObject = objects.find((obj) => obj.id === id);
|
||||
if (!selectedObject) return;
|
||||
|
||||
if (layerRef?.current) {
|
||||
const node = layerRef.current.findOne(`#${selectedObjectId}`);
|
||||
if (node) {
|
||||
node.x(newX);
|
||||
node.y(newY);
|
||||
layerRef.current.batchDraw();
|
||||
const newX = (selectedObject.x || 0) + deltaX;
|
||||
const newY = (selectedObject.y || 0) + deltaY;
|
||||
|
||||
updateObject(id, {
|
||||
x: newX,
|
||||
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);
|
||||
@@ -87,5 +116,5 @@ export const useKeyboardMovement = () => {
|
||||
return () => {
|
||||
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 shapeRef = useRef<Konva.Star>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSelected && shapeRef.current && transformerRef.current) {
|
||||
transformerRef.current.nodes([shapeRef.current]);
|
||||
transformerRef.current.getLayer()?.batchDraw();
|
||||
}
|
||||
}, [isSelected, transformerRef]);
|
||||
// Transformer is managed in EditorCanvas for multi-select support
|
||||
|
||||
const isMask = obj.isMask || false;
|
||||
const showGuide = obj.showMaskGuide !== false;
|
||||
@@ -44,9 +39,9 @@ const AbstractShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, dr
|
||||
dash={isMask && showGuide ? [10, 5] : undefined}
|
||||
rotation={obj.rotation || 0}
|
||||
draggable={draggable}
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
if (shapeRef.current) {
|
||||
onSelect(obj.id, shapeRef.current);
|
||||
onSelect(obj.id, shapeRef.current, e);
|
||||
}
|
||||
}}
|
||||
onDragMove={(e) => {
|
||||
|
||||
@@ -6,12 +6,7 @@ import type { ShapeProps } from "./types";
|
||||
const ArrowShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, draggable }: ShapeProps) => {
|
||||
const shapeRef = useRef<Konva.Arrow>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSelected && shapeRef.current && transformerRef.current) {
|
||||
transformerRef.current.nodes([shapeRef.current]);
|
||||
transformerRef.current.getLayer()?.batchDraw();
|
||||
}
|
||||
}, [isSelected, transformerRef]);
|
||||
// Transformer is managed in EditorCanvas for multi-select support
|
||||
|
||||
const startX = obj.x;
|
||||
const startY = obj.y;
|
||||
@@ -32,9 +27,9 @@ const ArrowShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, dragg
|
||||
perfectDrawEnabled={false}
|
||||
rotation={obj.rotation || 0}
|
||||
draggable={draggable}
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
if (shapeRef.current) {
|
||||
onSelect(obj.id, shapeRef.current);
|
||||
onSelect(obj.id, shapeRef.current, e);
|
||||
}
|
||||
}}
|
||||
onDragEnd={(e) => {
|
||||
|
||||
@@ -6,12 +6,7 @@ import type { ShapeProps } from "./types";
|
||||
const CircleShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, draggable }: ShapeProps) => {
|
||||
const shapeRef = useRef<Konva.Circle>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSelected && shapeRef.current && transformerRef.current) {
|
||||
transformerRef.current.nodes([shapeRef.current]);
|
||||
transformerRef.current.getLayer()?.batchDraw();
|
||||
}
|
||||
}, [isSelected, transformerRef]);
|
||||
// Transformer is managed in EditorCanvas for multi-select support
|
||||
|
||||
const isMask = obj.isMask || false;
|
||||
const showGuide = obj.showMaskGuide !== false;
|
||||
@@ -38,9 +33,9 @@ const CircleShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, drag
|
||||
dash={isMask && showGuide ? [10, 5] : undefined}
|
||||
rotation={obj.rotation || 0}
|
||||
draggable={draggable}
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
if (shapeRef.current) {
|
||||
onSelect(obj.id, shapeRef.current);
|
||||
onSelect(obj.id, shapeRef.current, e);
|
||||
}
|
||||
}}
|
||||
onDragMove={(e) => {
|
||||
|
||||
@@ -15,16 +15,7 @@ const ImageObject = ({
|
||||
const [image] = useImage(obj.imageUrl || "");
|
||||
const shapeRef = useRef<Konva.Image>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// اگر 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]);
|
||||
// Transformer is managed in EditorCanvas for multi-select support
|
||||
|
||||
if (!image) return null;
|
||||
|
||||
@@ -42,10 +33,10 @@ const ImageObject = ({
|
||||
strokeWidth={isSelected && draggable ? 3 : 0}
|
||||
rotation={obj.rotation || 0}
|
||||
draggable={draggable}
|
||||
onClick={draggable ? () => {
|
||||
onClick={draggable ? (e) => {
|
||||
// فقط وقتی mask اعمال نشده onClick handler را فعال کن
|
||||
if (shapeRef.current) {
|
||||
onSelect(obj.id, shapeRef.current);
|
||||
onSelect(obj.id, shapeRef.current, e);
|
||||
}
|
||||
} : undefined}
|
||||
onDragEnd={(e) => {
|
||||
|
||||
@@ -6,12 +6,7 @@ import type { ShapeProps } from "./types";
|
||||
const LineShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, draggable }: ShapeProps) => {
|
||||
const shapeRef = useRef<Konva.Line>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSelected && shapeRef.current && transformerRef.current) {
|
||||
transformerRef.current.nodes([shapeRef.current]);
|
||||
transformerRef.current.getLayer()?.batchDraw();
|
||||
}
|
||||
}, [isSelected, transformerRef]);
|
||||
// Transformer is managed in EditorCanvas for multi-select support
|
||||
|
||||
const startX = obj.x;
|
||||
const startY = obj.y;
|
||||
@@ -30,9 +25,9 @@ const LineShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, dragga
|
||||
perfectDrawEnabled={false}
|
||||
rotation={obj.rotation || 0}
|
||||
draggable={draggable}
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
if (shapeRef.current) {
|
||||
onSelect(obj.id, shapeRef.current);
|
||||
onSelect(obj.id, shapeRef.current, e);
|
||||
}
|
||||
}}
|
||||
onDragEnd={(e) => {
|
||||
|
||||
@@ -23,12 +23,7 @@ const LinkShape = ({
|
||||
const shapeRef = useRef<Konva.Text>(null);
|
||||
const fontWeight = useMemo(() => getFontWeight(obj.fontWeight), [obj.fontWeight]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSelected && shapeRef.current && transformerRef.current) {
|
||||
transformerRef.current.nodes([shapeRef.current]);
|
||||
transformerRef.current.getLayer()?.batchDraw();
|
||||
}
|
||||
}, [isSelected, transformerRef]);
|
||||
// Transformer is managed in EditorCanvas for multi-select support
|
||||
|
||||
return (
|
||||
<Text
|
||||
@@ -43,11 +38,11 @@ const LinkShape = ({
|
||||
underline={true}
|
||||
rotation={obj.rotation || 0}
|
||||
draggable={draggable}
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
// در حالت editor، لینکها قابل کلیک نیستند
|
||||
// این قابلیت در viewer اضافه خواهد شد
|
||||
if (shapeRef.current) {
|
||||
onSelect(obj.id, shapeRef.current);
|
||||
onSelect(obj.id, shapeRef.current, e);
|
||||
}
|
||||
}}
|
||||
onDragEnd={(e) => {
|
||||
|
||||
@@ -6,12 +6,7 @@ import type { ShapeProps } from "./types";
|
||||
const RectangleShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, draggable }: ShapeProps) => {
|
||||
const shapeRef = useRef<Konva.Rect>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSelected && shapeRef.current && transformerRef.current) {
|
||||
transformerRef.current.nodes([shapeRef.current]);
|
||||
transformerRef.current.getLayer()?.batchDraw();
|
||||
}
|
||||
}, [isSelected, transformerRef]);
|
||||
// Transformer is managed in EditorCanvas for multi-select support
|
||||
|
||||
const isMask = obj.isMask || false;
|
||||
const showGuide = obj.showMaskGuide !== false;
|
||||
@@ -39,9 +34,9 @@ const RectangleShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, d
|
||||
dash={isMask && showGuide ? [10, 5] : undefined}
|
||||
rotation={obj.rotation || 0}
|
||||
draggable={draggable}
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
if (shapeRef.current) {
|
||||
onSelect(obj.id, shapeRef.current);
|
||||
onSelect(obj.id, shapeRef.current, e);
|
||||
}
|
||||
}}
|
||||
onDragMove={(e) => {
|
||||
|
||||
@@ -44,25 +44,14 @@ const TextShape = ({
|
||||
}: TextShapeProps) => {
|
||||
const shapeRef = useRef<Konva.Text>(null);
|
||||
|
||||
// Transformer is managed in EditorCanvas for multi-select support
|
||||
// Set offset for text node
|
||||
useEffect(() => {
|
||||
if (isSelected && shapeRef.current && transformerRef.current) {
|
||||
const textNode = shapeRef.current;
|
||||
|
||||
// تنظیم 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);
|
||||
if (shapeRef.current) {
|
||||
shapeRef.current.offsetX(0);
|
||||
shapeRef.current.offsetY(0);
|
||||
}
|
||||
}, [isSelected, transformerRef, obj.text, obj.width, obj.height, obj.fontSize, obj.fontFamily, obj.fontWeight]);
|
||||
}, []);
|
||||
|
||||
const fontFamily = useMemo(() => getFontFamily(obj.fontFamily), [obj.fontFamily]);
|
||||
const fontWeight = useMemo(() => getFontWeight(obj.fontWeight), [obj.fontWeight]);
|
||||
@@ -84,9 +73,9 @@ const TextShape = ({
|
||||
align="right"
|
||||
rotation={obj.rotation || 0}
|
||||
draggable={draggable}
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
if (shapeRef.current) {
|
||||
onSelect(obj.id, shapeRef.current);
|
||||
onSelect(obj.id, shapeRef.current, e);
|
||||
}
|
||||
}}
|
||||
onDragEnd={(e) => {
|
||||
|
||||
@@ -6,12 +6,7 @@ import type { ShapeProps } from "./types";
|
||||
const TriangleShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, draggable }: ShapeProps) => {
|
||||
const shapeRef = useRef<Konva.RegularPolygon>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSelected && shapeRef.current && transformerRef.current) {
|
||||
transformerRef.current.nodes([shapeRef.current]);
|
||||
transformerRef.current.getLayer()?.batchDraw();
|
||||
}
|
||||
}, [isSelected, transformerRef]);
|
||||
// Transformer is managed in EditorCanvas for multi-select support
|
||||
|
||||
const isMask = obj.isMask || false;
|
||||
const showGuide = obj.showMaskGuide !== false;
|
||||
@@ -40,9 +35,9 @@ const TriangleShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, dr
|
||||
dash={isMask && showGuide ? [10, 5] : undefined}
|
||||
rotation={obj.rotation || 0}
|
||||
draggable={draggable}
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
if (shapeRef.current) {
|
||||
onSelect(obj.id, shapeRef.current);
|
||||
onSelect(obj.id, shapeRef.current, e);
|
||||
}
|
||||
}}
|
||||
onDragMove={(e) => {
|
||||
|
||||
@@ -54,12 +54,7 @@ const VideoShape = ({
|
||||
};
|
||||
}, [obj.videoUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSelected && groupRef.current && transformerRef.current && image) {
|
||||
transformerRef.current.nodes([groupRef.current]);
|
||||
transformerRef.current.getLayer()?.batchDraw();
|
||||
}
|
||||
}, [isSelected, transformerRef, image]);
|
||||
// Transformer is managed in EditorCanvas for multi-select support
|
||||
|
||||
useEffect(() => {
|
||||
if (!showVideoModal) return;
|
||||
@@ -101,7 +96,7 @@ const VideoShape = ({
|
||||
}
|
||||
}
|
||||
if (groupRef.current) {
|
||||
onSelect(obj.id, groupRef.current);
|
||||
onSelect(obj.id, groupRef.current, e);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ export type ShapeProps = {
|
||||
obj: EditorObject;
|
||||
isSelected: boolean;
|
||||
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;
|
||||
draggable: boolean;
|
||||
};
|
||||
|
||||
@@ -13,7 +13,8 @@ export type ToolType =
|
||||
| "document"
|
||||
| "video"
|
||||
| "link"
|
||||
| "grid";
|
||||
| "grid"
|
||||
| "group";
|
||||
|
||||
export type TableCell = {
|
||||
id: string;
|
||||
@@ -83,6 +84,10 @@ type EditorStoreType = {
|
||||
duplicateObject: (id: string) => void;
|
||||
selectedObjectId: string | null;
|
||||
setSelectedObjectId: (id: string | null) => void;
|
||||
selectedObjectIds: string[];
|
||||
setSelectedObjectIds: (ids: string[]) => void;
|
||||
addToSelection: (id: string) => void;
|
||||
removeFromSelection: (id: string) => void;
|
||||
selectedTableId: string | null;
|
||||
setSelectedTableId: (id: string | null) => void;
|
||||
selectedCellId: string | null;
|
||||
@@ -301,7 +306,37 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
|
||||
}));
|
||||
},
|
||||
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,
|
||||
setSelectedTableId: (id) => set({ selectedTableId: id }),
|
||||
selectedCellId: null,
|
||||
@@ -650,6 +685,57 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
|
||||
if (objectIds.length < 2) return;
|
||||
|
||||
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[]) =>
|
||||
objects.map((obj) =>
|
||||
objectIds.includes(obj.id) ? { ...obj, groupId } : obj
|
||||
@@ -657,25 +743,32 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
|
||||
|
||||
if (state.currentPageId) {
|
||||
set((state) => ({
|
||||
objects: updateObjects(state.objects),
|
||||
objects: [...updateObjects(state.objects), groupObject],
|
||||
pages: state.pages.map((p) =>
|
||||
p.id === state.currentPageId
|
||||
? { ...p, objects: updateObjects(p.objects) }
|
||||
? { ...p, objects: [...updateObjects(p.objects), groupObject] }
|
||||
: p
|
||||
),
|
||||
selectedObjectId: groupId,
|
||||
selectedObjectIds: [groupId],
|
||||
}));
|
||||
return;
|
||||
}
|
||||
set((state) => ({
|
||||
objects: updateObjects(state.objects),
|
||||
objects: [...updateObjects(state.objects), groupObject],
|
||||
selectedObjectId: groupId,
|
||||
selectedObjectIds: [groupId],
|
||||
}));
|
||||
},
|
||||
ungroupObjects: (groupId) => {
|
||||
const state = get();
|
||||
// حذف groupId از objectهای داخل گروه و حذف group object
|
||||
const updateObjects = (objects: EditorObject[]) =>
|
||||
objects.map((obj) =>
|
||||
obj.groupId === groupId ? { ...obj, groupId: undefined } : obj
|
||||
);
|
||||
objects
|
||||
.map((obj) =>
|
||||
obj.groupId === groupId ? { ...obj, groupId: undefined } : obj
|
||||
)
|
||||
.filter((obj) => obj.id !== groupId); // حذف group object
|
||||
|
||||
if (state.currentPageId) {
|
||||
set((state) => ({
|
||||
@@ -685,12 +778,20 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
|
||||
? { ...p, objects: updateObjects(p.objects) }
|
||||
: p
|
||||
),
|
||||
selectedObjectId: null,
|
||||
selectedObjectIds: [],
|
||||
}));
|
||||
return;
|
||||
}
|
||||
set((state) => ({
|
||||
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