guide line and magnet

This commit is contained in:
hamid zarghami
2026-04-26 10:50:22 +03:30
parent e2b7e964cd
commit 4feccbf1f9
5 changed files with 335 additions and 17 deletions
@@ -0,0 +1,81 @@
import { Layer, Line } from "react-konva";
import type { Guide } from "./useSnap";
type GuidesLayerProps = {
guides: Guide[];
stageWidth: number;
stageHeight: number;
activeGuideIds: string[];
selectedGuideId: string | null;
draftGuide: { orientation: Guide["orientation"]; position: number } | null;
onGuideSelect: (id: string) => void;
onGuideDragEnd: (id: string, position: number, shouldRemove: boolean) => void;
};
const GuidesLayer = ({
guides,
stageWidth,
stageHeight,
activeGuideIds,
selectedGuideId,
draftGuide,
onGuideSelect,
onGuideDragEnd,
}: GuidesLayerProps) => {
return (
<Layer>
{guides.map((guide) => {
const isVertical = guide.orientation === "vertical";
const isActive = activeGuideIds.includes(guide.id);
const isSelected = selectedGuideId === guide.id;
return (
<Line
key={guide.id}
id={guide.id}
name="guide-line"
x={isVertical ? guide.position : 0}
y={isVertical ? 0 : guide.position}
points={isVertical ? [0, 0, 0, stageHeight] : [0, 0, stageWidth, 0]}
stroke={isSelected ? "#2563eb" : isActive ? "#1d4ed8" : "#60a5fa"}
strokeWidth={isSelected ? 2.5 : isActive ? 2 : 1}
dash={[6, 6]}
hitStrokeWidth={10}
draggable
dragBoundFunc={(pos) =>
isVertical ? { x: pos.x, y: 0 } : { x: 0, y: pos.y }
}
onMouseDown={() => onGuideSelect(guide.id)}
onDragEnd={(e) => {
const node = e.target;
const nextPosition = isVertical ? node.x() : node.y();
const shouldRemove = isVertical
? nextPosition < 0 || nextPosition > stageWidth
: nextPosition < 0 || nextPosition > stageHeight;
onGuideDragEnd(guide.id, nextPosition, shouldRemove);
}}
/>
);
})}
{draftGuide && (
<Line
name="guide-line-draft"
listening={false}
x={draftGuide.orientation === "vertical" ? draftGuide.position : 0}
y={draftGuide.orientation === "vertical" ? 0 : draftGuide.position}
points={
draftGuide.orientation === "vertical"
? [0, 0, 0, stageHeight]
: [0, 0, stageWidth, 0]
}
stroke="#2563eb"
strokeWidth={1.5}
dash={[6, 6]}
opacity={0.9}
/>
)}
</Layer>
);
};
export default GuidesLayer;
@@ -255,11 +255,8 @@ const ObjectRenderer = ({
}
}}
onDragMove={(e) => {
const node = e.target;
onUpdate(obj.id, {
x: node.x(),
y: node.y(),
});
// Snapping is handled at Stage level for smoother dragging.
e.target.getLayer()?.batchDraw();
}}
onDragEnd={(e) => {
const node = e.target;
@@ -308,10 +305,6 @@ const ObjectRenderer = ({
const handleGroupDragMove = (e: Konva.KonvaEventObject<DragEvent>) => {
const node = e.target;
onUpdate(obj.id, {
x: node.x(),
y: node.y(),
});
// به‌روزرسانی transformer در حین drag
if (transformerRef.current && isSelected) {
transformerRef.current.nodes([node]);
@@ -0,0 +1,44 @@
export type Guide = {
id: string;
orientation: "vertical" | "horizontal";
position: number;
};
const SNAP_THRESHOLD = 5;
export type SnappedPositionResult = {
position: { x: number; y: number };
activeGuideIds: string[];
};
export const getSnappedPosition = (
pos: { x: number; y: number },
guides: Guide[],
): SnappedPositionResult => {
let x = pos.x;
let y = pos.y;
const activeGuideIds: string[] = [];
guides.forEach((guide) => {
if (
guide.orientation === "vertical" &&
Math.abs(pos.x - guide.position) < SNAP_THRESHOLD
) {
x = guide.position;
activeGuideIds.push(guide.id);
}
if (
guide.orientation === "horizontal" &&
Math.abs(pos.y - guide.position) < SNAP_THRESHOLD
) {
y = guide.position;
activeGuideIds.push(guide.id);
}
});
return {
position: { x, y },
activeGuideIds,
};
};