cleaned editorstore
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
import type { EditorObject, Page, TableCell } from "./editorStore.types";
|
||||
|
||||
export const createTableCellId = (row: number, col: number) => `cell-${row}-${col}`;
|
||||
|
||||
const createUuid = (): string => {
|
||||
if (typeof globalThis.crypto?.randomUUID === "function") {
|
||||
return globalThis.crypto.randomUUID();
|
||||
}
|
||||
|
||||
if (typeof globalThis.crypto?.getRandomValues === "function") {
|
||||
const bytes = new Uint8Array(16);
|
||||
globalThis.crypto.getRandomValues(bytes);
|
||||
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
||||
|
||||
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0"));
|
||||
return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
|
||||
}
|
||||
|
||||
return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
};
|
||||
|
||||
export const createScopedId = (prefix: string) => `${prefix}-${createUuid()}`;
|
||||
|
||||
export const isInvalidPageId = (id?: string) =>
|
||||
!id || id === "page-undefined" || id.endsWith("-undefined");
|
||||
|
||||
export const createInitialCells = (
|
||||
rows: number,
|
||||
cols: number,
|
||||
cellWidth: number,
|
||||
cellHeight: number,
|
||||
): Record<string, TableCell> => {
|
||||
const cells: Record<string, TableCell> = {};
|
||||
for (let row = 0; row < rows; row++) {
|
||||
for (let col = 0; col < cols; col++) {
|
||||
const cellId = createTableCellId(row, col);
|
||||
cells[cellId] = {
|
||||
id: cellId,
|
||||
text: "",
|
||||
background: "#f5f5f5",
|
||||
width: cellWidth,
|
||||
height: cellHeight,
|
||||
};
|
||||
}
|
||||
}
|
||||
return cells;
|
||||
};
|
||||
|
||||
export const createInitialPage = (name: string): Page => ({
|
||||
id: createScopedId("page"),
|
||||
name,
|
||||
objects: [],
|
||||
guides: [],
|
||||
});
|
||||
|
||||
export const reorderByIds = (
|
||||
items: EditorObject[],
|
||||
draggedId: string,
|
||||
targetId: string,
|
||||
) => {
|
||||
const fromIndex = items.findIndex((item) => item.id === draggedId);
|
||||
const toIndex = items.findIndex((item) => item.id === targetId);
|
||||
|
||||
if (fromIndex === -1 || toIndex === -1) return items;
|
||||
|
||||
const nextItems = [...items];
|
||||
const [movedItem] = nextItems.splice(fromIndex, 1);
|
||||
nextItems.splice(toIndex, 0, movedItem);
|
||||
return nextItems;
|
||||
};
|
||||
|
||||
const getObjectBounds = (obj: EditorObject) => {
|
||||
let w = obj.width || 0;
|
||||
let h = obj.height || 0;
|
||||
|
||||
if (obj.type === "text") {
|
||||
if (!w) {
|
||||
const textLength = (obj.text || "").length;
|
||||
const fontSize = obj.fontSize || 24;
|
||||
w = Math.max(textLength * fontSize * 0.6, 50);
|
||||
}
|
||||
if (!h) {
|
||||
h = obj.fontSize || 24;
|
||||
}
|
||||
}
|
||||
|
||||
if (obj.type === "rectangle" && obj.shapeType === "circle") {
|
||||
const radius = w / 2;
|
||||
const cx = obj.x || 0;
|
||||
const cy = obj.y || 0;
|
||||
return {
|
||||
minX: cx - radius,
|
||||
minY: cy - radius,
|
||||
maxX: cx + radius,
|
||||
maxY: cy + radius,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
obj.type === "rectangle" &&
|
||||
(obj.shapeType === "triangle" || obj.shapeType === "abstract")
|
||||
) {
|
||||
const cx = (obj.x || 0) + w / 2;
|
||||
const cy = (obj.y || 0) + h / 2;
|
||||
const rot = ((obj.rotation || 0) * Math.PI) / 180;
|
||||
|
||||
const baseRadius = Math.min(w / 2, h / 2);
|
||||
const actualRadius =
|
||||
obj.shapeType === "abstract" ? baseRadius * 0.85 : baseRadius;
|
||||
|
||||
const corners = [
|
||||
{ x: -actualRadius, y: -actualRadius },
|
||||
{ x: actualRadius, y: -actualRadius },
|
||||
{ x: actualRadius, y: actualRadius },
|
||||
{ x: -actualRadius, y: actualRadius },
|
||||
];
|
||||
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
return { minX, minY, maxX, maxY };
|
||||
}
|
||||
|
||||
if (obj.type === "arrow" || obj.type === "line") {
|
||||
const startX = obj.x || 0;
|
||||
const startY = obj.y || 0;
|
||||
const endX = obj.width || 0;
|
||||
const endY = obj.height || 0;
|
||||
return {
|
||||
minX: Math.min(startX, endX),
|
||||
minY: Math.min(startY, endY),
|
||||
maxX: Math.max(startX, endX),
|
||||
maxY: Math.max(startY, endY),
|
||||
};
|
||||
}
|
||||
|
||||
const cx = (obj.x || 0) + w / 2;
|
||||
const cy = (obj.y || 0) + h / 2;
|
||||
const rot = ((obj.rotation || 0) * Math.PI) / 180;
|
||||
|
||||
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 },
|
||||
];
|
||||
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
return { minX, minY, maxX, maxY };
|
||||
};
|
||||
|
||||
export const buildGroupResult = (
|
||||
objects: EditorObject[],
|
||||
objectIds: string[],
|
||||
nextGroupId?: string,
|
||||
) => {
|
||||
if (objectIds.length < 2) return null;
|
||||
|
||||
const allObjectIdsToGroup = new Set<string>(objectIds);
|
||||
const oldGroupIdsToRemove = new Set<string>();
|
||||
|
||||
objectIds.forEach((id) => {
|
||||
const obj = objects.find((o) => o.id === id);
|
||||
if (obj?.type === "group") {
|
||||
const oldGroupMembers = objects.filter((o) => o.groupId === id);
|
||||
oldGroupMembers.forEach((member) => {
|
||||
if (member.type !== "group") {
|
||||
allObjectIdsToGroup.add(member.id);
|
||||
}
|
||||
});
|
||||
oldGroupIdsToRemove.add(id);
|
||||
} else if (obj?.groupId) {
|
||||
const oldGroupMembers = objects.filter((o) => o.groupId === obj.groupId);
|
||||
oldGroupMembers.forEach((member) => {
|
||||
if (member.type !== "group") {
|
||||
allObjectIdsToGroup.add(member.id);
|
||||
}
|
||||
});
|
||||
oldGroupIdsToRemove.add(obj.groupId);
|
||||
}
|
||||
});
|
||||
|
||||
if (allObjectIdsToGroup.size < 2) return null;
|
||||
|
||||
const groupId = nextGroupId ?? `group-${crypto.randomUUID()}`;
|
||||
const objectsToGroup = objects.filter(
|
||||
(obj) => allObjectIdsToGroup.has(obj.id) && obj.type !== "group",
|
||||
);
|
||||
if (objectsToGroup.length === 0) return null;
|
||||
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
|
||||
objectsToGroup.forEach((obj) => {
|
||||
const bounds = getObjectBounds(obj);
|
||||
minX = Math.min(minX, bounds.minX);
|
||||
minY = Math.min(minY, bounds.minY);
|
||||
maxX = Math.max(maxX, bounds.maxX);
|
||||
maxY = Math.max(maxY, bounds.maxY);
|
||||
});
|
||||
|
||||
const groupObject: EditorObject = {
|
||||
id: groupId,
|
||||
type: "group",
|
||||
x: minX,
|
||||
y: minY,
|
||||
width: maxX - minX,
|
||||
height: maxY - minY,
|
||||
};
|
||||
|
||||
const nextObjects = objects
|
||||
.map((obj) => {
|
||||
if (allObjectIdsToGroup.has(obj.id) && obj.type !== "group") {
|
||||
return { ...obj, groupId };
|
||||
}
|
||||
if (obj.groupId && oldGroupIdsToRemove.has(obj.groupId)) {
|
||||
return { ...obj, groupId: undefined };
|
||||
}
|
||||
return obj;
|
||||
})
|
||||
.filter((obj) => !oldGroupIdsToRemove.has(obj.id));
|
||||
|
||||
return {
|
||||
groupId,
|
||||
nextObjects: [...nextObjects, groupObject],
|
||||
};
|
||||
};
|
||||
|
||||
export const ungroupById = (objects: EditorObject[], groupId: string) =>
|
||||
objects
|
||||
.map((obj) => (obj.groupId === groupId ? { ...obj, groupId: undefined } : obj))
|
||||
.filter((obj) => obj.id !== groupId);
|
||||
@@ -1,85 +1,32 @@
|
||||
import { create } from "zustand";
|
||||
import Konva from "konva";
|
||||
import type { ShapeType } from "./shapeStore";
|
||||
import {
|
||||
buildGroupResult,
|
||||
createInitialCells,
|
||||
createInitialPage,
|
||||
createScopedId,
|
||||
createTableCellId,
|
||||
isInvalidPageId,
|
||||
reorderByIds,
|
||||
ungroupById,
|
||||
} from "./editorStore.helpers";
|
||||
import type {
|
||||
EditorObject,
|
||||
Page,
|
||||
PageGuide,
|
||||
TableCell,
|
||||
TableData,
|
||||
ToolType,
|
||||
} from "./editorStore.types";
|
||||
|
||||
export type ToolType =
|
||||
| "select"
|
||||
| "rectangle"
|
||||
| "text"
|
||||
| "image"
|
||||
| "line"
|
||||
| "arrow"
|
||||
| "sticker"
|
||||
| "document"
|
||||
| "video"
|
||||
| "link"
|
||||
| "grid"
|
||||
| "group";
|
||||
|
||||
export type TableCell = {
|
||||
id: string;
|
||||
text: string;
|
||||
background: string;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export type TableData = {
|
||||
id: string;
|
||||
rows: number;
|
||||
cols: number;
|
||||
cellWidth: number;
|
||||
cellHeight: number;
|
||||
cells: Record<string, TableCell>;
|
||||
stroke?: string;
|
||||
strokeWidth?: number;
|
||||
};
|
||||
|
||||
export type EditorObject = {
|
||||
id: string;
|
||||
type: ToolType;
|
||||
x: number;
|
||||
y: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
fill?: string;
|
||||
stroke?: string;
|
||||
strokeWidth?: number;
|
||||
text?: string;
|
||||
fontSize?: number;
|
||||
fontFamily?: string;
|
||||
fontWeight?: string;
|
||||
letterSpacing?: number;
|
||||
wordSpacing?: number;
|
||||
opacity?: number;
|
||||
imageUrl?: string;
|
||||
videoUrl?: string;
|
||||
linkUrl?: string;
|
||||
rotation?: number;
|
||||
scaleX?: number;
|
||||
scaleY?: number;
|
||||
shapeType?: ShapeType;
|
||||
tableData?: TableData;
|
||||
visible?: boolean;
|
||||
isMask?: boolean;
|
||||
showMaskGuide?: boolean;
|
||||
maskId?: string; // Reference to mask shape (DATA only, NO rendering logic)
|
||||
maskInvert?: boolean; // اگر true: overlap مخفی میشود، اگر false: overlap نمایش داده میشود
|
||||
groupId?: string;
|
||||
};
|
||||
|
||||
export type Page = {
|
||||
id: string;
|
||||
name: string;
|
||||
objects: EditorObject[];
|
||||
guides: PageGuide[];
|
||||
};
|
||||
|
||||
export type PageGuide = {
|
||||
id: string;
|
||||
orientation: "vertical" | "horizontal";
|
||||
position: number;
|
||||
};
|
||||
export type {
|
||||
EditorObject,
|
||||
Page,
|
||||
PageGuide,
|
||||
TableCell,
|
||||
TableData,
|
||||
ToolType,
|
||||
} from "./editorStore.types";
|
||||
|
||||
type EditorStoreType = {
|
||||
tool: ToolType;
|
||||
@@ -159,61 +106,6 @@ type EditorStoreType = {
|
||||
loadPages: (pages: Page[]) => void;
|
||||
};
|
||||
|
||||
const createTableCellId = (row: number, col: number) => `cell-${row}-${col}`;
|
||||
|
||||
const createUuid = (): string => {
|
||||
if (typeof globalThis.crypto?.randomUUID === "function") {
|
||||
return globalThis.crypto.randomUUID();
|
||||
}
|
||||
|
||||
if (typeof globalThis.crypto?.getRandomValues === "function") {
|
||||
const bytes = new Uint8Array(16);
|
||||
globalThis.crypto.getRandomValues(bytes);
|
||||
|
||||
// RFC4122 v4 bits
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
||||
|
||||
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0"));
|
||||
return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
|
||||
}
|
||||
|
||||
return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
};
|
||||
|
||||
const createScopedId = (prefix: string) => `${prefix}-${createUuid()}`;
|
||||
const isInvalidPageId = (id?: string) =>
|
||||
!id || id === "page-undefined" || id.endsWith("-undefined");
|
||||
|
||||
const createInitialCells = (
|
||||
rows: number,
|
||||
cols: number,
|
||||
cellWidth: number,
|
||||
cellHeight: number,
|
||||
): Record<string, TableCell> => {
|
||||
const cells: Record<string, TableCell> = {};
|
||||
for (let row = 0; row < rows; row++) {
|
||||
for (let col = 0; col < cols; col++) {
|
||||
const cellId = createTableCellId(row, col);
|
||||
cells[cellId] = {
|
||||
id: cellId,
|
||||
text: "",
|
||||
background: "#f5f5f5",
|
||||
width: cellWidth,
|
||||
height: cellHeight,
|
||||
};
|
||||
}
|
||||
}
|
||||
return cells;
|
||||
};
|
||||
|
||||
const createInitialPage = (name: string): Page => ({
|
||||
id: createScopedId("page"),
|
||||
name,
|
||||
objects: [],
|
||||
guides: [],
|
||||
});
|
||||
|
||||
export const useEditorStore = create<EditorStoreType>((set, get) => {
|
||||
const initialPage = createInitialPage("جلد");
|
||||
return {
|
||||
@@ -358,25 +250,16 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
|
||||
reorderObjects: (draggedId, targetId) => {
|
||||
if (draggedId === targetId) return;
|
||||
|
||||
const reorder = (items: EditorObject[]) => {
|
||||
const fromIndex = items.findIndex((item) => item.id === draggedId);
|
||||
const toIndex = items.findIndex((item) => item.id === targetId);
|
||||
|
||||
if (fromIndex === -1 || toIndex === -1) return items;
|
||||
|
||||
const nextItems = [...items];
|
||||
const [movedItem] = nextItems.splice(fromIndex, 1);
|
||||
nextItems.splice(toIndex, 0, movedItem);
|
||||
return nextItems;
|
||||
};
|
||||
|
||||
const state = get();
|
||||
if (state.currentPageId) {
|
||||
set((state) => ({
|
||||
objects: reorder(state.objects),
|
||||
objects: reorderByIds(state.objects, draggedId, targetId),
|
||||
pages: state.pages.map((page) =>
|
||||
page.id === state.currentPageId
|
||||
? { ...page, objects: reorder(page.objects) }
|
||||
? {
|
||||
...page,
|
||||
objects: reorderByIds(page.objects, draggedId, targetId),
|
||||
}
|
||||
: page,
|
||||
),
|
||||
}));
|
||||
@@ -384,7 +267,7 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
|
||||
}
|
||||
|
||||
set((state) => ({
|
||||
objects: reorder(state.objects),
|
||||
objects: reorderByIds(state.objects, draggedId, targetId),
|
||||
}));
|
||||
},
|
||||
selectedObjectId: null,
|
||||
@@ -774,239 +657,46 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
|
||||
},
|
||||
groupObjects: (objectIds) => {
|
||||
const state = get();
|
||||
if (objectIds.length < 2) return;
|
||||
|
||||
// پیدا کردن همه آبجکتهایی که باید در گروه جدید باشند
|
||||
// شامل آبجکتهای انتخاب شده + آبجکتهای داخل گروههای قبلی آنها
|
||||
const allObjectIdsToGroup = new Set<string>(objectIds);
|
||||
const oldGroupIdsToRemove = new Set<string>();
|
||||
|
||||
// بررسی اینکه آیا یکی از آبجکتهای انتخاب شده در یک گروه قبلی است
|
||||
objectIds.forEach((id) => {
|
||||
const obj = state.objects.find((o) => o.id === id);
|
||||
if (obj?.type === "group") {
|
||||
// اگر این خود یک گروه است، همه آبجکتهای آن را اضافه کن
|
||||
const oldGroupMembers = state.objects.filter((o) => o.groupId === id);
|
||||
oldGroupMembers.forEach((member) => {
|
||||
if (member.type !== "group") {
|
||||
allObjectIdsToGroup.add(member.id);
|
||||
}
|
||||
});
|
||||
// گروه قدیمی را برای حذف علامت بزن
|
||||
oldGroupIdsToRemove.add(id);
|
||||
} else if (obj?.groupId) {
|
||||
// اگر این آبجکت در یک گروه است، همه آبجکتهای آن گروه را اضافه کن
|
||||
const oldGroupMembers = state.objects.filter(
|
||||
(o) => o.groupId === obj.groupId,
|
||||
);
|
||||
oldGroupMembers.forEach((member) => {
|
||||
if (member.type !== "group") {
|
||||
allObjectIdsToGroup.add(member.id);
|
||||
}
|
||||
});
|
||||
// گروه قدیمی را برای حذف علامت بزن
|
||||
if (obj.groupId) {
|
||||
oldGroupIdsToRemove.add(obj.groupId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// اگر فقط یک آبجکت باقی ماند، گروهبندی انجام نده
|
||||
if (allObjectIdsToGroup.size < 2) return;
|
||||
|
||||
const groupId = `group-${crypto.randomUUID()}`;
|
||||
const objectsToGroup = state.objects.filter(
|
||||
(obj) => allObjectIdsToGroup.has(obj.id) && obj.type !== "group",
|
||||
);
|
||||
|
||||
if (objectsToGroup.length === 0) return;
|
||||
|
||||
// محاسبه bounds گروه با در نظر گرفتن rotation
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
|
||||
objectsToGroup.forEach((obj) => {
|
||||
// برای متنهایی که width و height ندارند، از مقادیر پیشفرض استفاده کن
|
||||
let w = obj.width || 0;
|
||||
let h = obj.height || 0;
|
||||
|
||||
if (obj.type === "text") {
|
||||
// اگر متن width نداره، از fontSize و طول متن برای تخمین استفاده کن
|
||||
if (!w) {
|
||||
const textLength = (obj.text || "").length;
|
||||
const fontSize = obj.fontSize || 24;
|
||||
w = Math.max(textLength * fontSize * 0.6, 50);
|
||||
}
|
||||
// اگر height نداره، از fontSize استفاده کن
|
||||
if (!h) {
|
||||
h = obj.fontSize || 24;
|
||||
}
|
||||
}
|
||||
|
||||
// برای دایره، x و y مرکز دایره است و bounding box همیشه یک مربع است
|
||||
if (obj.type === "rectangle" && obj.shapeType === "circle") {
|
||||
const radius = w / 2;
|
||||
const cx = obj.x || 0;
|
||||
const cy = obj.y || 0;
|
||||
// برای دایره، bounding box همیشه یک مربع با ضلع برابر با قطر است
|
||||
minX = Math.min(minX, cx - radius);
|
||||
minY = Math.min(minY, cy - radius);
|
||||
maxX = Math.max(maxX, cx + radius);
|
||||
maxY = Math.max(maxY, cy + radius);
|
||||
return;
|
||||
}
|
||||
|
||||
// برای مثلث و abstract، x و y در store گوشه بالا-چپ است اما در render مرکز استفاده میشود
|
||||
if (
|
||||
obj.type === "rectangle" &&
|
||||
(obj.shapeType === "triangle" || obj.shapeType === "abstract")
|
||||
) {
|
||||
// محاسبه مرکز از گوشه بالا-چپ
|
||||
const cx = (obj.x || 0) + w / 2;
|
||||
const cy = (obj.y || 0) + h / 2;
|
||||
const rot = ((obj.rotation || 0) * Math.PI) / 180;
|
||||
|
||||
// برای abstract، از radius واقعی استفاده کن (0.85 * baseRadius)
|
||||
// برای triangle، از radius واقعی استفاده کن (min(width, height) / 2)
|
||||
let actualRadius: number;
|
||||
if (obj.shapeType === "abstract") {
|
||||
const baseRadius = Math.min(w / 2, h / 2);
|
||||
actualRadius = baseRadius * 0.85;
|
||||
} else {
|
||||
// triangle
|
||||
actualRadius = Math.min(w / 2, h / 2);
|
||||
}
|
||||
|
||||
// محاسبه چهار گوشه با استفاده از radius واقعی
|
||||
const corners = [
|
||||
{ x: -actualRadius, y: -actualRadius },
|
||||
{ x: actualRadius, y: -actualRadius },
|
||||
{ x: actualRadius, y: actualRadius },
|
||||
{ x: -actualRadius, y: actualRadius },
|
||||
];
|
||||
|
||||
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);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// برای arrow، x و y نقطه شروع است و width/height نقطه پایان است
|
||||
if (obj.type === "arrow" || obj.type === "line") {
|
||||
const startX = obj.x || 0;
|
||||
const startY = obj.y || 0;
|
||||
const endX = obj.width || 0;
|
||||
const endY = obj.height || 0;
|
||||
|
||||
// برای arrow و line، bounding box از نقطه شروع تا نقطه پایان است
|
||||
minX = Math.min(minX, startX, endX);
|
||||
minY = Math.min(minY, startY, endY);
|
||||
maxX = Math.max(maxX, startX, endX);
|
||||
maxY = Math.max(maxY, startY, endY);
|
||||
return;
|
||||
}
|
||||
|
||||
// برای سایر اشکال، x و y گوشه بالا-چپ است
|
||||
const cx = (obj.x || 0) + w / 2;
|
||||
const cy = (obj.y || 0) + h / 2;
|
||||
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) => {
|
||||
// اگر این آبجکت باید در گروه جدید باشد
|
||||
if (allObjectIdsToGroup.has(obj.id) && obj.type !== "group") {
|
||||
return { ...obj, groupId };
|
||||
}
|
||||
// اگر این آبجکت در یک گروه قدیمی است که باید حذف شود
|
||||
if (obj.groupId && oldGroupIdsToRemove.has(obj.groupId)) {
|
||||
return { ...obj, groupId: undefined };
|
||||
}
|
||||
return obj;
|
||||
})
|
||||
// حذف گروههای قدیمی
|
||||
.filter((obj) => !oldGroupIdsToRemove.has(obj.id));
|
||||
const result = buildGroupResult(state.objects, objectIds);
|
||||
if (!result) return;
|
||||
|
||||
if (state.currentPageId) {
|
||||
const pageToGroup = state.pages.find((p) => p.id === state.currentPageId);
|
||||
if (!pageToGroup) return;
|
||||
const pageResult = buildGroupResult(
|
||||
pageToGroup.objects,
|
||||
objectIds,
|
||||
result.groupId,
|
||||
);
|
||||
if (!pageResult) return;
|
||||
|
||||
set((state) => ({
|
||||
objects: [...updateObjects(state.objects), groupObject],
|
||||
objects: result.nextObjects,
|
||||
pages: state.pages.map((p) =>
|
||||
p.id === state.currentPageId
|
||||
? { ...p, objects: [...updateObjects(p.objects), groupObject] }
|
||||
? { ...p, objects: pageResult.nextObjects }
|
||||
: p,
|
||||
),
|
||||
selectedObjectId: groupId,
|
||||
selectedObjectIds: [groupId],
|
||||
selectedObjectId: result.groupId,
|
||||
selectedObjectIds: [result.groupId],
|
||||
}));
|
||||
return;
|
||||
}
|
||||
set((state) => ({
|
||||
objects: [...updateObjects(state.objects), groupObject],
|
||||
selectedObjectId: groupId,
|
||||
selectedObjectIds: [groupId],
|
||||
set(() => ({
|
||||
objects: result.nextObjects,
|
||||
selectedObjectId: result.groupId,
|
||||
selectedObjectIds: [result.groupId],
|
||||
}));
|
||||
},
|
||||
ungroupObjects: (groupId) => {
|
||||
const state = get();
|
||||
// حذف groupId از objectهای داخل گروه و حذف group object
|
||||
const updateObjects = (objects: EditorObject[]) =>
|
||||
objects
|
||||
.map((obj) =>
|
||||
obj.groupId === groupId ? { ...obj, groupId: undefined } : obj,
|
||||
)
|
||||
.filter((obj) => obj.id !== groupId); // حذف group object
|
||||
|
||||
if (state.currentPageId) {
|
||||
set((state) => ({
|
||||
objects: updateObjects(state.objects),
|
||||
objects: ungroupById(state.objects, groupId),
|
||||
pages: state.pages.map((p) =>
|
||||
p.id === state.currentPageId
|
||||
? { ...p, objects: updateObjects(p.objects) }
|
||||
? { ...p, objects: ungroupById(p.objects, groupId) }
|
||||
: p,
|
||||
),
|
||||
selectedObjectId: null,
|
||||
@@ -1015,7 +705,7 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
|
||||
return;
|
||||
}
|
||||
set((state) => ({
|
||||
objects: updateObjects(state.objects),
|
||||
objects: ungroupById(state.objects, groupId),
|
||||
selectedObjectId: null,
|
||||
selectedObjectIds: [],
|
||||
}));
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { ShapeType } from "./shapeStore";
|
||||
|
||||
export type ToolType =
|
||||
| "select"
|
||||
| "rectangle"
|
||||
| "text"
|
||||
| "image"
|
||||
| "line"
|
||||
| "arrow"
|
||||
| "sticker"
|
||||
| "document"
|
||||
| "video"
|
||||
| "link"
|
||||
| "grid"
|
||||
| "group";
|
||||
|
||||
export type TableCell = {
|
||||
id: string;
|
||||
text: string;
|
||||
background: string;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export type TableData = {
|
||||
id: string;
|
||||
rows: number;
|
||||
cols: number;
|
||||
cellWidth: number;
|
||||
cellHeight: number;
|
||||
cells: Record<string, TableCell>;
|
||||
stroke?: string;
|
||||
strokeWidth?: number;
|
||||
};
|
||||
|
||||
export type EditorObject = {
|
||||
id: string;
|
||||
type: ToolType;
|
||||
x: number;
|
||||
y: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
fill?: string;
|
||||
stroke?: string;
|
||||
strokeWidth?: number;
|
||||
text?: string;
|
||||
fontSize?: number;
|
||||
fontFamily?: string;
|
||||
fontWeight?: string;
|
||||
letterSpacing?: number;
|
||||
wordSpacing?: number;
|
||||
opacity?: number;
|
||||
imageUrl?: string;
|
||||
videoUrl?: string;
|
||||
linkUrl?: string;
|
||||
rotation?: number;
|
||||
scaleX?: number;
|
||||
scaleY?: number;
|
||||
shapeType?: ShapeType;
|
||||
tableData?: TableData;
|
||||
visible?: boolean;
|
||||
isMask?: boolean;
|
||||
showMaskGuide?: boolean;
|
||||
maskId?: string;
|
||||
maskInvert?: boolean;
|
||||
groupId?: string;
|
||||
};
|
||||
|
||||
export type PageGuide = {
|
||||
id: string;
|
||||
orientation: "vertical" | "horizontal";
|
||||
position: number;
|
||||
};
|
||||
|
||||
export type Page = {
|
||||
id: string;
|
||||
name: string;
|
||||
objects: EditorObject[];
|
||||
guides: PageGuide[];
|
||||
};
|
||||
Reference in New Issue
Block a user