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