alignment setting
This commit is contained in:
@@ -56,7 +56,7 @@ const Editor: FC = () => {
|
|||||||
) : null}
|
) : null}
|
||||||
<LayersPanel isOpen={isLayersPanelOpen} setIsOpen={setIsLayersPanelOpen} />
|
<LayersPanel isOpen={isLayersPanelOpen} setIsOpen={setIsLayersPanelOpen} />
|
||||||
<EditorCanvas catalogSize={data?.data?.size} />
|
<EditorCanvas catalogSize={data?.data?.size} />
|
||||||
<EditorSidebar />
|
<EditorSidebar catalogSize={data?.data?.size} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
import Logo from "@/assets/images/logo.svg";
|
import Logo from "@/assets/images/logo.svg";
|
||||||
|
import { getPaperDimensions } from "@/config/paperSizes";
|
||||||
import { clx } from "@/helpers/utils";
|
import { clx } from "@/helpers/utils";
|
||||||
import { useSharedStore } from "@/shared/store/sharedStore";
|
import { useSharedStore } from "@/shared/store/sharedStore";
|
||||||
import { CloseSquare } from "iconsax-react";
|
import { CloseSquare } from "iconsax-react";
|
||||||
import { useEditorStore, type ToolType } from "../store/editorStore";
|
import { useEditorStore, type ToolType } from "../store/editorStore";
|
||||||
|
import { useObjectHandlers } from "./canvas/useObjectHandlers";
|
||||||
import { ToolsBar, ObjectSettings, ToolInstructions } from "./sidebar";
|
import { ToolsBar, ObjectSettings, ToolInstructions } from "./sidebar";
|
||||||
|
|
||||||
const EditorSidebar = () => {
|
type EditorSidebarProps = {
|
||||||
|
catalogSize?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditorSidebar = ({ catalogSize }: EditorSidebarProps) => {
|
||||||
const { openSidebar, setOpenSidebar } = useSharedStore();
|
const { openSidebar, setOpenSidebar } = useSharedStore();
|
||||||
const { tool, setTool, selectedObjectId, objects, updateObject, deleteObject } = useEditorStore();
|
const { tool, setTool, selectedObjectId, objects, deleteObject } = useEditorStore();
|
||||||
|
const { handleObjectUpdate } = useObjectHandlers();
|
||||||
|
|
||||||
const selectedObject = objects.find((obj) => obj.id === selectedObjectId);
|
const selectedObject = objects.find((obj) => obj.id === selectedObjectId);
|
||||||
|
const { width: pageWidth, height: pageHeight } = getPaperDimensions(catalogSize ?? "a4");
|
||||||
|
|
||||||
const handleToolClick = (selectedTool: ToolType) => {
|
const handleToolClick = (selectedTool: ToolType) => {
|
||||||
setTool(selectedTool);
|
setTool(selectedTool);
|
||||||
@@ -66,7 +74,9 @@ const EditorSidebar = () => {
|
|||||||
{selectedObject ? (
|
{selectedObject ? (
|
||||||
<ObjectSettings
|
<ObjectSettings
|
||||||
selectedObject={selectedObject}
|
selectedObject={selectedObject}
|
||||||
onUpdate={updateObject}
|
pageWidth={pageWidth}
|
||||||
|
pageHeight={pageHeight}
|
||||||
|
onUpdate={handleObjectUpdate}
|
||||||
onDelete={deleteObject}
|
onDelete={deleteObject}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
SizeSettings,
|
SizeSettings,
|
||||||
LinkSettings,
|
LinkSettings,
|
||||||
VideoSettings,
|
VideoSettings,
|
||||||
|
AlignmentSettings,
|
||||||
TransformSettings,
|
TransformSettings,
|
||||||
GridSettings,
|
GridSettings,
|
||||||
MaskSettings,
|
MaskSettings,
|
||||||
@@ -14,11 +15,19 @@ import TextInstruction from "./instructions/TextInstruction";
|
|||||||
|
|
||||||
type ObjectSettingsProps = {
|
type ObjectSettingsProps = {
|
||||||
selectedObject: EditorObject;
|
selectedObject: EditorObject;
|
||||||
|
pageWidth: number;
|
||||||
|
pageHeight: number;
|
||||||
onUpdate: (id: string, updates: Partial<EditorObject>) => void;
|
onUpdate: (id: string, updates: Partial<EditorObject>) => void;
|
||||||
onDelete: (id: string) => void;
|
onDelete: (id: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ObjectSettings = ({ selectedObject, onUpdate, onDelete }: ObjectSettingsProps) => {
|
const ObjectSettings = ({
|
||||||
|
selectedObject,
|
||||||
|
pageWidth,
|
||||||
|
pageHeight,
|
||||||
|
onUpdate,
|
||||||
|
onDelete,
|
||||||
|
}: ObjectSettingsProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between pb-4 border-b border-border">
|
<div className="flex items-center justify-between pb-4 border-b border-border">
|
||||||
@@ -69,6 +78,11 @@ const ObjectSettings = ({ selectedObject, onUpdate, onDelete }: ObjectSettingsPr
|
|||||||
<GridSettings selectedObject={selectedObject} onUpdate={onUpdate} />
|
<GridSettings selectedObject={selectedObject} onUpdate={onUpdate} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<AlignmentSettings
|
||||||
|
pageWidth={pageWidth}
|
||||||
|
pageHeight={pageHeight}
|
||||||
|
onUpdate={onUpdate}
|
||||||
|
/>
|
||||||
<TransformSettings selectedObject={selectedObject} onUpdate={onUpdate} />
|
<TransformSettings selectedObject={selectedObject} onUpdate={onUpdate} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
|
||||||
|
import {
|
||||||
|
AlignBottom,
|
||||||
|
AlignHorizontally,
|
||||||
|
AlignLeft,
|
||||||
|
AlignRight,
|
||||||
|
AlignTop,
|
||||||
|
AlignVertically,
|
||||||
|
} from "iconsax-react";
|
||||||
|
import { useEditorStore, type EditorObject } from "../../../store/editorStore";
|
||||||
|
import type { AlignKind } from "./alignmentUtils";
|
||||||
|
import {
|
||||||
|
deltasForAlign,
|
||||||
|
pageBounds,
|
||||||
|
unionBoundsOfAlignTargets,
|
||||||
|
computeGroupFrameUpdate,
|
||||||
|
updatesFromDeltas,
|
||||||
|
} from "./alignmentUtils";
|
||||||
|
|
||||||
|
type AlignmentSettingsProps = {
|
||||||
|
pageWidth: number;
|
||||||
|
pageHeight: number;
|
||||||
|
onUpdate: (id: string, updates: Partial<EditorObject>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const alignIconProps = { size: 24, color: "currentColor" as const, variant: "Linear" as const };
|
||||||
|
|
||||||
|
const AlignmentSettings = ({
|
||||||
|
pageWidth,
|
||||||
|
pageHeight,
|
||||||
|
onUpdate,
|
||||||
|
}: AlignmentSettingsProps) => {
|
||||||
|
// const selectedObjectIds = useEditorStore((s) => s.selectedObjectIds);
|
||||||
|
|
||||||
|
/** دقیقاً دو شیٔ مجزا (هنوز یک گروه نشدهاند) → مرز همان دو شی؛ یک شی یا گروه یا بیش از دو تا → صفحه */
|
||||||
|
// const alignToSelection = selectedObjectIds.length === 2;
|
||||||
|
|
||||||
|
const applyAlign = (kind: AlignKind) => {
|
||||||
|
const { objects: objs, selectedObjectIds: ids, layerRef } = useEditorStore.getState();
|
||||||
|
const layer = layerRef?.current ?? null;
|
||||||
|
|
||||||
|
const initial = new Map<string, EditorObject>();
|
||||||
|
for (const id of ids) {
|
||||||
|
const o = objs.find((x) => x.id === id);
|
||||||
|
if (o && o.visible !== false) initial.set(id, { ...o });
|
||||||
|
}
|
||||||
|
if (initial.size === 0) return;
|
||||||
|
|
||||||
|
const initialList = [...initial.values()];
|
||||||
|
const useSelectionBounds = ids.length === 2;
|
||||||
|
const target =
|
||||||
|
useSelectionBounds && initialList.length === 2
|
||||||
|
? unionBoundsOfAlignTargets(initialList, objs, layer)
|
||||||
|
: pageBounds(pageWidth, pageHeight);
|
||||||
|
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
|
const alignedGroupIds = new Set<string>();
|
||||||
|
|
||||||
|
for (const id of initial.keys()) {
|
||||||
|
const obj = initial.get(id)!;
|
||||||
|
const { dx, dy } = deltasForAlign(obj, kind, target, objs, layer);
|
||||||
|
const updates = updatesFromDeltas(obj, dx, dy);
|
||||||
|
if (Object.keys(updates).length === 0) continue;
|
||||||
|
onUpdate(id, updates);
|
||||||
|
if (obj.type === "group") {
|
||||||
|
alignedGroupIds.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { updateObject } = useEditorStore.getState();
|
||||||
|
const afterObjects = useEditorStore.getState().objects;
|
||||||
|
for (const gid of alignedGroupIds) {
|
||||||
|
const patch = computeGroupFrameUpdate(gid, afterObjects, layer);
|
||||||
|
if (patch) updateObject(gid, patch);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// const hint = alignToSelection ? "نسبت به دو شی انتخابشده" : "نسبت به صفحه";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3 border-t border-border pt-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-bold text-foreground">ابزار جابهجایی</h4>
|
||||||
|
{/* <p className="mt-1 text-xs text-muted-foreground">{hint}</p> */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-xs font-medium text-muted-foreground">افقی</p>
|
||||||
|
<div className="flex gap-12 items-center mt-4">
|
||||||
|
<AlignRight onClick={() => applyAlign("right")} {...alignIconProps} />
|
||||||
|
<AlignHorizontally onClick={() => applyAlign("centerH")} {...alignIconProps} />
|
||||||
|
<AlignLeft onClick={() => applyAlign("left")} {...alignIconProps} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-xs font-medium text-muted-foreground mt-4">عمودی</p>
|
||||||
|
<div className="flex gap-12 items-center mt-4">
|
||||||
|
|
||||||
|
<AlignTop onClick={() => applyAlign("top")} {...alignIconProps} />
|
||||||
|
|
||||||
|
<AlignVertically onClick={() => applyAlign("centerV")} {...alignIconProps} />
|
||||||
|
|
||||||
|
<AlignBottom onClick={() => applyAlign("bottom")} {...alignIconProps} />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AlignmentSettings;
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
import Konva from "konva";
|
||||||
|
import type { EditorObject } from "../../../store/editorStore";
|
||||||
|
import { getObjectBounds } from "../../../store/editorStore.helpers";
|
||||||
|
|
||||||
|
export type AlignKind =
|
||||||
|
| "left"
|
||||||
|
| "centerH"
|
||||||
|
| "right"
|
||||||
|
| "top"
|
||||||
|
| "centerV"
|
||||||
|
| "bottom";
|
||||||
|
|
||||||
|
export type BoundsRect = {
|
||||||
|
minX: number;
|
||||||
|
minY: number;
|
||||||
|
maxX: number;
|
||||||
|
maxY: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** مرز آمار واقعی روی لایه (فونت RTL، چرخش، mask، …)؛ همنس با مختصات ذخیرهشده در استور */
|
||||||
|
export function boundsFromKonvaLayer(
|
||||||
|
objId: string,
|
||||||
|
layer: Konva.Layer | null | undefined,
|
||||||
|
): BoundsRect | null {
|
||||||
|
if (!layer) return null;
|
||||||
|
const node =
|
||||||
|
layer.findOne(`#${objId}`) ?? layer.findOne(`#masked-${objId}`);
|
||||||
|
if (!node) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = node.getClientRect({
|
||||||
|
relativeTo: layer,
|
||||||
|
skipStroke: false,
|
||||||
|
skipShadow: true,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
minX: r.x,
|
||||||
|
minY: r.y,
|
||||||
|
maxX: r.x + r.width,
|
||||||
|
maxY: r.y + r.height,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unionKonvaBoundsOfGroupMembers(
|
||||||
|
groupId: string,
|
||||||
|
objects: EditorObject[],
|
||||||
|
layer: Konva.Layer | null | undefined,
|
||||||
|
): BoundsRect | null {
|
||||||
|
const members = objects.filter(
|
||||||
|
(o) =>
|
||||||
|
o.groupId === groupId &&
|
||||||
|
o.type !== "group" &&
|
||||||
|
o.visible !== false,
|
||||||
|
);
|
||||||
|
if (members.length === 0) return null;
|
||||||
|
|
||||||
|
let minX = Infinity;
|
||||||
|
let minY = Infinity;
|
||||||
|
let maxX = -Infinity;
|
||||||
|
let maxY = -Infinity;
|
||||||
|
let any = false;
|
||||||
|
|
||||||
|
for (const m of members) {
|
||||||
|
const b = boundsFromKonvaLayer(m.id, layer) ?? getObjectBounds(m);
|
||||||
|
any = true;
|
||||||
|
minX = Math.min(minX, b.minX);
|
||||||
|
minY = Math.min(minY, b.minY);
|
||||||
|
maxX = Math.max(maxX, b.maxX);
|
||||||
|
maxY = Math.max(maxY, b.maxY);
|
||||||
|
}
|
||||||
|
|
||||||
|
return any ? { minX, minY, maxX, maxY } : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pageBounds = (width: number, height: number): BoundsRect => ({
|
||||||
|
minX: 0,
|
||||||
|
minY: 0,
|
||||||
|
maxX: width,
|
||||||
|
maxY: height,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** مرز واقعی اعضای یک گروه (نه مستطیل ذخیرهشدهٔ شیء group که ممکن است قدیمی باشد) */
|
||||||
|
export function unionBoundsOfGroupMembers(
|
||||||
|
groupId: string,
|
||||||
|
objects: EditorObject[],
|
||||||
|
): BoundsRect | null {
|
||||||
|
const members = objects.filter(
|
||||||
|
(o) =>
|
||||||
|
o.groupId === groupId &&
|
||||||
|
o.type !== "group" &&
|
||||||
|
o.visible !== false,
|
||||||
|
);
|
||||||
|
return unionBounds(members);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* مرزی که برای همترازی استفاده میشود؛ برای گروه از اتحاد اعضا تا با لبهٔ صفحه همپوشانی دقیق باشد.
|
||||||
|
*/
|
||||||
|
export function boundsForAlign(
|
||||||
|
obj: EditorObject,
|
||||||
|
objects: EditorObject[],
|
||||||
|
layer?: Konva.Layer | null,
|
||||||
|
): BoundsRect {
|
||||||
|
if (layer) {
|
||||||
|
if (obj.type === "group") {
|
||||||
|
const k = unionKonvaBoundsOfGroupMembers(obj.id, objects, layer);
|
||||||
|
if (k) return k;
|
||||||
|
} else {
|
||||||
|
const k = boundsFromKonvaLayer(obj.id, layer);
|
||||||
|
if (k) return k;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.type !== "group") {
|
||||||
|
return getObjectBounds(obj);
|
||||||
|
}
|
||||||
|
const fromMembers = unionBoundsOfGroupMembers(obj.id, objects);
|
||||||
|
return fromMembers ?? getObjectBounds(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unionBounds(objects: EditorObject[]): BoundsRect | null {
|
||||||
|
let minX = Infinity;
|
||||||
|
let minY = Infinity;
|
||||||
|
let maxX = -Infinity;
|
||||||
|
let maxY = -Infinity;
|
||||||
|
let any = false;
|
||||||
|
|
||||||
|
for (const obj of objects) {
|
||||||
|
if (obj.visible === false) continue;
|
||||||
|
const b = getObjectBounds(obj);
|
||||||
|
any = true;
|
||||||
|
minX = Math.min(minX, b.minX);
|
||||||
|
minY = Math.min(minY, b.minY);
|
||||||
|
maxX = Math.max(maxX, b.maxX);
|
||||||
|
maxY = Math.max(maxY, b.maxY);
|
||||||
|
}
|
||||||
|
|
||||||
|
return any ? { minX, minY, maxX, maxY } : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** اتحاد مرز همترازیِ چند هدف (گروهها با اعضایشان) */
|
||||||
|
export function unionBoundsOfAlignTargets(
|
||||||
|
targets: EditorObject[],
|
||||||
|
allObjects: EditorObject[],
|
||||||
|
layer?: Konva.Layer | null,
|
||||||
|
): BoundsRect | null {
|
||||||
|
let minX = Infinity;
|
||||||
|
let minY = Infinity;
|
||||||
|
let maxX = -Infinity;
|
||||||
|
let maxY = -Infinity;
|
||||||
|
let any = false;
|
||||||
|
|
||||||
|
for (const obj of targets) {
|
||||||
|
if (obj.visible === false) continue;
|
||||||
|
const b = boundsForAlign(obj, allObjects, layer);
|
||||||
|
any = true;
|
||||||
|
minX = Math.min(minX, b.minX);
|
||||||
|
minY = Math.min(minY, b.minY);
|
||||||
|
maxX = Math.max(maxX, b.maxX);
|
||||||
|
maxY = Math.max(maxY, b.maxY);
|
||||||
|
}
|
||||||
|
|
||||||
|
return any ? { minX, minY, maxX, maxY } : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deltasForAlign(
|
||||||
|
obj: EditorObject,
|
||||||
|
kind: AlignKind,
|
||||||
|
target: BoundsRect,
|
||||||
|
objects: EditorObject[],
|
||||||
|
layer?: Konva.Layer | null,
|
||||||
|
): { dx: number; dy: number } {
|
||||||
|
const b = boundsForAlign(obj, objects, layer);
|
||||||
|
const midX = (b.minX + b.maxX) / 2;
|
||||||
|
const midY = (b.minY + b.maxY) / 2;
|
||||||
|
const tMidX = (target.minX + target.maxX) / 2;
|
||||||
|
const tMidY = (target.minY + target.maxY) / 2;
|
||||||
|
|
||||||
|
let dx = 0;
|
||||||
|
let dy = 0;
|
||||||
|
|
||||||
|
switch (kind) {
|
||||||
|
case "left":
|
||||||
|
dx = target.minX - b.minX;
|
||||||
|
break;
|
||||||
|
case "centerH":
|
||||||
|
dx = tMidX - midX;
|
||||||
|
break;
|
||||||
|
case "right":
|
||||||
|
dx = target.maxX - b.maxX;
|
||||||
|
break;
|
||||||
|
case "top":
|
||||||
|
dy = target.minY - b.minY;
|
||||||
|
break;
|
||||||
|
case "centerV":
|
||||||
|
dy = tMidY - midY;
|
||||||
|
break;
|
||||||
|
case "bottom":
|
||||||
|
dy = target.maxY - b.maxY;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
dx: kind === "left" || kind === "centerH" || kind === "right" ? Math.round(dx) : 0,
|
||||||
|
dy: kind === "top" || kind === "centerV" || kind === "bottom" ? Math.round(dy) : 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** قاب گروه را مطابق اتحاد اعضا بدون جابجاییٔ اعضا تنظیم میکند */
|
||||||
|
export function computeGroupFrameUpdate(
|
||||||
|
groupId: string,
|
||||||
|
objects: EditorObject[],
|
||||||
|
layer?: Konva.Layer | null,
|
||||||
|
): Partial<EditorObject> | null {
|
||||||
|
const u =
|
||||||
|
(layer && unionKonvaBoundsOfGroupMembers(groupId, objects, layer)) ||
|
||||||
|
unionBoundsOfGroupMembers(groupId, objects);
|
||||||
|
if (!u) return null;
|
||||||
|
return {
|
||||||
|
x: u.minX,
|
||||||
|
y: u.minY,
|
||||||
|
width: u.maxX - u.minX,
|
||||||
|
height: u.maxY - u.minY,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updatesFromDeltas(
|
||||||
|
obj: EditorObject,
|
||||||
|
dx: number,
|
||||||
|
dy: number,
|
||||||
|
): Partial<EditorObject> {
|
||||||
|
if (dx === 0 && dy === 0) return {};
|
||||||
|
|
||||||
|
if (obj.type === "line" || obj.type === "arrow") {
|
||||||
|
return {
|
||||||
|
x: (obj.x ?? 0) + dx,
|
||||||
|
y: (obj.y ?? 0) + dy,
|
||||||
|
width: (obj.width ?? 0) + dx,
|
||||||
|
height: (obj.height ?? 0) + dy,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: (obj.x ?? 0) + dx,
|
||||||
|
y: (obj.y ?? 0) + dy,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ export { default as LineSettings } from "./LineSettings";
|
|||||||
export { default as SizeSettings } from "./SizeSettings";
|
export { default as SizeSettings } from "./SizeSettings";
|
||||||
export { default as LinkSettings } from "./LinkSettings";
|
export { default as LinkSettings } from "./LinkSettings";
|
||||||
export { default as VideoSettings } from "./VideoSettings";
|
export { default as VideoSettings } from "./VideoSettings";
|
||||||
|
export { default as AlignmentSettings } from "./AlignmentSettings";
|
||||||
export { default as TransformSettings } from "./TransformSettings";
|
export { default as TransformSettings } from "./TransformSettings";
|
||||||
export { default as GridSettings } from "./GridSettings";
|
export { default as GridSettings } from "./GridSettings";
|
||||||
export { default as MaskSettings } from "./MaskSettings";
|
export { default as MaskSettings } from "./MaskSettings";
|
||||||
|
|||||||
@@ -71,11 +71,11 @@ export const reorderByIds = (
|
|||||||
return nextItems;
|
return nextItems;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getObjectBounds = (obj: EditorObject) => {
|
export const getObjectBounds = (obj: EditorObject) => {
|
||||||
let w = obj.width || 0;
|
// TextShape: متن با align="right" در Konva؛ گوشهٔ بالا-راست جعبه در (x,y)، نقاط بدنه بهسمت چپ است.
|
||||||
let h = obj.height || 0;
|
|
||||||
|
|
||||||
if (obj.type === "text") {
|
if (obj.type === "text") {
|
||||||
|
let w = obj.width || 0;
|
||||||
|
let h = obj.height || 0;
|
||||||
if (!w) {
|
if (!w) {
|
||||||
const textLength = (obj.text || "").length;
|
const textLength = (obj.text || "").length;
|
||||||
const fontSize = obj.fontSize || 24;
|
const fontSize = obj.fontSize || 24;
|
||||||
@@ -84,8 +84,40 @@ const getObjectBounds = (obj: EditorObject) => {
|
|||||||
if (!h) {
|
if (!h) {
|
||||||
h = obj.fontSize || 24;
|
h = obj.fontSize || 24;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const px = obj.x ?? 0;
|
||||||
|
const py = obj.y ?? 0;
|
||||||
|
const rot = ((obj.rotation || 0) * Math.PI) / 180;
|
||||||
|
|
||||||
|
const cornersLocal = [
|
||||||
|
{ x: -w, y: 0 },
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
{ x: 0, y: h },
|
||||||
|
{ x: -w, y: h },
|
||||||
|
];
|
||||||
|
|
||||||
|
let minX = Infinity;
|
||||||
|
let minY = Infinity;
|
||||||
|
let maxX = -Infinity;
|
||||||
|
let maxY = -Infinity;
|
||||||
|
|
||||||
|
cornersLocal.forEach(({ x: lx, y: ly }) => {
|
||||||
|
const rx = lx * Math.cos(rot) - ly * Math.sin(rot);
|
||||||
|
const ry = lx * Math.sin(rot) + ly * Math.cos(rot);
|
||||||
|
const wx = px + rx;
|
||||||
|
const wy = py + ry;
|
||||||
|
minX = Math.min(minX, wx);
|
||||||
|
minY = Math.min(minY, wy);
|
||||||
|
maxX = Math.max(maxX, wx);
|
||||||
|
maxY = Math.max(maxY, wy);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { minX, minY, maxX, maxY };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let w = obj.width || 0;
|
||||||
|
let h = obj.height || 0;
|
||||||
|
|
||||||
if (obj.type === "rectangle" && obj.shapeType === "circle") {
|
if (obj.type === "rectangle" && obj.shapeType === "circle") {
|
||||||
const radius = w / 2;
|
const radius = w / 2;
|
||||||
const cx = obj.x || 0;
|
const cx = obj.x || 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user