diff --git a/src/components/EditorCanvas/Canvas.jsx b/src/components/EditorCanvas/Canvas.jsx
index d4ceecb..f2b71a5 100644
--- a/src/components/EditorCanvas/Canvas.jsx
+++ b/src/components/EditorCanvas/Canvas.jsx
@@ -1,864 +1,1759 @@
-import { useRef, useState } from "react";
-import {
- Action,
- Cardinality,
- Constraint,
- darkBgTheme,
- ObjectType,
- gridSize,
- gridCircleRadius,
- minAreaSize,
-} from "../../data/constants";
-import { Toast } from "@douyinfe/semi-ui";
-import Table from "./Table";
-import Area from "./Area";
-import Relationship from "./Relationship";
-import Note from "./Note";
-import {
- useCanvas,
- useSettings,
- useTransform,
- useDiagram,
- useUndoRedo,
- useSelect,
- useAreas,
- useNotes,
- useLayout,
- useSaveState,
-} from "../../hooks";
-import { useTranslation } from "react-i18next";
-import { useEventListener } from "usehooks-ts";
-import { areFieldsCompatible, getTableHeight } from "../../utils/utils";
-import { getRectFromEndpoints, isInsideRect } from "../../utils/rect";
-import { State, noteWidth } from "../../data/constants";
-import { nanoid } from "nanoid";
-
-export default function Canvas() {
- const { t } = useTranslation();
-
- const canvasRef = useRef(null);
- const canvasContextValue = useCanvas();
- const {
- canvas: { viewBox },
- pointer,
- } = canvasContextValue;
-
- const { tables, updateTable, relationships, addRelationship, database } =
- useDiagram();
- const { setSaveState } = useSaveState();
- const { areas, updateArea } = useAreas();
- const { notes, updateNote } = useNotes();
- const { layout } = useLayout();
- const { settings } = useSettings();
- const { setUndoStack, setRedoStack } = useUndoRedo();
- const { transform, setTransform } = useTransform();
- const {
- selectedElement,
- setSelectedElement,
- bulkSelectedElements,
- setBulkSelectedElements,
- } = useSelect();
- const notDragging = {
- id: -1,
- type: ObjectType.NONE,
- grabOffset: { x: 0, y: 0 },
- };
- const [dragging, setDragging] = useState(notDragging);
- const [linking, setLinking] = useState(false);
- const [linkingLine, setLinkingLine] = useState({
- startTableId: -1,
- startFieldId: -1,
- endTableId: -1,
- endFieldId: -1,
- startX: 0,
- startY: 0,
- endX: 0,
- endY: 0,
- });
- const [hoveredTable, setHoveredTable] = useState({
- tableId: null,
- fieldId: null,
- });
- const [panning, setPanning] = useState({
- isPanning: false,
- panStart: { x: 0, y: 0 },
- cursorStart: { x: 0, y: 0 },
- });
- const [areaResize, setAreaResize] = useState({ id: -1, dir: "none" });
- const [areaInitDimensions, setAreaInitDimensions] = useState({
- x: 0,
- y: 0,
- width: 0,
- height: 0,
- });
- const [bulkSelectRect, setBulkSelectRect] = useState({
- x1: 0,
- y1: 0,
- x2: 0,
- y2: 0,
- show: false,
- ctrlKey: false,
- metaKey: false,
- });
- // this is used to store the element that is clicked on
- // at the moment, and shouldn't be a part of the state
- let elementPointerDown = null;
-
- const isSameElement = (el1, el2) => {
- return el1.id === el2.id && el1.type === el2.type;
- };
-
- const collectSelectedElements = () => {
- const rect = getRectFromEndpoints(bulkSelectRect);
- const elements = [];
- const shouldAddElement = (elementRect, element) => {
- // if ctrl key is pressed, only add the elements that are not already selected
- // can theoretically be optimized later if the selected elements is
- // a map from id to element (after the ids are made unique)
- return (
- isInsideRect(elementRect, rect) &&
- ((!bulkSelectRect.ctrlKey && !bulkSelectRect.metaKey) ||
- !bulkSelectedElements.some((el) => isSameElement(el, element)))
- );
- };
-
- tables.forEach((table) => {
- if (table.locked) return;
-
- const element = {
- id: table.id,
- type: ObjectType.TABLE,
- currentCoords: { x: table.x, y: table.y },
- initialCoords: { x: table.x, y: table.y },
- };
- const tableRect = {
- x: table.x,
- y: table.y,
- width: settings.tableWidth,
- height: getTableHeight(table),
- };
- if (shouldAddElement(tableRect, element)) {
- elements.push(element);
- }
- });
-
- areas.forEach((area) => {
- if (area.locked) return;
-
- const element = {
- id: area.id,
- type: ObjectType.AREA,
- currentCoords: { x: area.x, y: area.y },
- initialCoords: { x: area.x, y: area.y },
- };
- const areaRect = {
- x: area.x,
- y: area.y,
- width: area.width,
- height: area.height,
- };
- if (shouldAddElement(areaRect, element)) {
- elements.push(element);
- }
- });
-
- notes.forEach((note) => {
- if (note.locked) return;
-
- const element = {
- id: note.id,
- type: ObjectType.NOTE,
- currentCoords: { x: note.x, y: note.y },
- initialCoords: { x: note.x, y: note.y },
- };
- const noteRect = {
- x: note.x,
- y: note.y,
- width: noteWidth,
- height: note.height,
- };
- if (shouldAddElement(noteRect, element)) {
- elements.push(element);
- }
- });
-
- if (bulkSelectRect.ctrlKey || bulkSelectRect.metaKey) {
- setBulkSelectedElements([...bulkSelectedElements, ...elements]);
- } else {
- setBulkSelectedElements(elements);
- }
- };
-
- const handlePointerDownOnElement = (e, { element, type }) => {
- if (selectedElement.open && !layout.sidebar) return;
-
- if (!e.isPrimary) return;
-
- if (!element.locked || !(e.ctrlKey || e.metaKey)) {
- setSelectedElement((prev) => ({
- ...prev,
- element: type,
- id: element.id,
- open: false,
- }));
- }
-
- if (element.locked) {
- if (!(e.ctrlKey || e.metaKey)) {
- setBulkSelectedElements([]);
- }
- return;
- }
-
- setBulkSelectRect((prev) => ({
- ...prev,
- show: false,
- }));
-
- // this is the object that will be added to the bulk selected elements
- // if necessary
- const elementInBulk = {
- id: element.id,
- type,
- currentCoords: { x: element.x, y: element.y },
- initialCoords: { x: element.x, y: element.y },
- };
-
- const isSelected = bulkSelectedElements.some((el) =>
- isSameElement(el, elementInBulk),
- );
-
- if (e.ctrlKey || e.metaKey) {
- if (isSelected) {
- if (bulkSelectedElements.length > 1) {
- setBulkSelectedElements(
- bulkSelectedElements.filter(
- (el) => !isSameElement(el, elementInBulk),
- ),
- );
- setSelectedElement({
- ...selectedElement,
- element: ObjectType.NONE,
- id: -1,
- open: false,
- });
- }
- } else {
- setBulkSelectedElements([...bulkSelectedElements, elementInBulk]);
- }
- setDragging(notDragging);
- return;
- }
-
- if (!isSelected) {
- setBulkSelectedElements([elementInBulk]);
- }
- setDragging({
- id: element.id,
- type,
- grabOffset: {
- x: pointer.spaces.diagram.x - element.x,
- y: pointer.spaces.diagram.y - element.y,
- },
- });
- };
-
- const coordinatesAfterSnappingToGrid = ({ x, y }) => {
- if (settings.snapToGrid) {
- return {
- x: Math.round(x / gridSize) * gridSize,
- y: Math.round(y / gridSize) * gridSize,
- };
- }
- return { x, y };
- };
-
- /**
- * @param {PointerEvent} e
- */
- const handlePointerMove = (e) => {
- if (selectedElement.open && !layout.sidebar) return;
-
- if (!e.isPrimary) return;
-
- if (panning.isPanning) {
- setTransform((prev) => ({
- ...prev,
- pan: {
- x:
- panning.panStart.x +
- (panning.cursorStart.x - pointer.spaces.screen.x) / transform.zoom,
- y:
- panning.panStart.y +
- (panning.cursorStart.y - pointer.spaces.screen.y) / transform.zoom,
- },
- }));
- return;
- }
-
- if (layout.readOnly) return;
-
- if (linking) {
- setLinkingLine({
- ...linkingLine,
- endX: pointer.spaces.diagram.x,
- endY: pointer.spaces.diagram.y,
- });
- return;
- }
-
- if (isDragging()) {
- const { x: mainElementFinalX, y: mainElementFinalY } =
- coordinatesAfterSnappingToGrid({
- x: pointer.spaces.diagram.x - dragging.grabOffset.x,
- y: pointer.spaces.diagram.y - dragging.grabOffset.y,
- });
-
- const { currentCoords } = bulkSelectedElements.find((el) =>
- isSameElement(el, dragging),
- );
-
- const deltaX = mainElementFinalX - currentCoords.x;
- const deltaY = mainElementFinalY - currentCoords.y;
-
- const newBulkSelectedElements = [];
- bulkSelectedElements.forEach((el) => {
- const elementFinalCoords = {
- x: el.currentCoords.x + deltaX,
- y: el.currentCoords.y + deltaY,
- };
- if (el.type === ObjectType.TABLE) {
- updateTable(el.id, { ...elementFinalCoords });
- }
- if (el.type === ObjectType.AREA) {
- updateArea(el.id, { ...elementFinalCoords });
- }
- if (el.type === ObjectType.NOTE) {
- updateNote(el.id, { ...elementFinalCoords });
- }
- newBulkSelectedElements.push({
- ...el,
- currentCoords: elementFinalCoords,
- });
- });
-
- setBulkSelectedElements(newBulkSelectedElements);
- return;
- }
-
- if (areaResize.id !== -1) {
- if (areaResize.dir === "none") return;
- let newDims = { ...areaInitDimensions };
- setPanning((old) => ({ ...old, isPanning: false }));
- const { x, y } = coordinatesAfterSnappingToGrid(pointer.spaces.diagram);
-
- switch (areaResize.dir) {
- case "br":
- newDims.width = x - areaInitDimensions.x;
- newDims.height = y - areaInitDimensions.y;
- break;
- case "tl":
- newDims.x = x;
- newDims.y = y;
- newDims.width = areaInitDimensions.width - (x - areaInitDimensions.x);
- newDims.height =
- areaInitDimensions.height - (y - areaInitDimensions.y);
- break;
- case "tr":
- newDims.y = y;
- newDims.width = x - areaInitDimensions.x;
- newDims.height =
- areaInitDimensions.height - (y - areaInitDimensions.y);
- break;
- case "bl":
- newDims.x = x;
- newDims.width = areaInitDimensions.width - (x - areaInitDimensions.x);
- newDims.height = y - areaInitDimensions.y;
- break;
- }
-
- if (newDims.width <= minAreaSize) {
- newDims.width = minAreaSize;
- if (areaResize.dir === "tl" || areaResize.dir === "bl") {
- newDims.x =
- areaInitDimensions.x + areaInitDimensions.width - minAreaSize;
- }
- }
-
- if (newDims.height <= minAreaSize) {
- newDims.height = minAreaSize;
- if (areaResize.dir === "tl" || areaResize.dir === "tr") {
- newDims.y =
- areaInitDimensions.y + areaInitDimensions.height - minAreaSize;
- }
- }
-
- updateArea(areaResize.id, { ...newDims });
- return;
- }
-
- if (bulkSelectRect.show) {
- setBulkSelectRect((prev) => ({
- ...prev,
- x2: pointer.spaces.diagram.x,
- y2: pointer.spaces.diagram.y,
- }));
- }
- };
-
- /**
- * @param {PointerEvent} e
- */
- const handlePointerDown = (e) => {
- if (!e.isPrimary) return;
-
- // don't pan if the sidesheet for editing a table is open
- if (
- selectedElement.element === ObjectType.TABLE &&
- selectedElement.open &&
- !layout.sidebar
- )
- return;
-
- const isMouseLeftButton = e.button === 0;
- const isMouseMiddleButton = e.button === 1;
-
- if (isMouseLeftButton) {
- setBulkSelectRect({
- x1: pointer.spaces.diagram.x,
- y1: pointer.spaces.diagram.y,
- x2: pointer.spaces.diagram.x,
- y2: pointer.spaces.diagram.y,
- show: elementPointerDown === null || !elementPointerDown.element.locked,
- ctrlKey: e.ctrlKey,
- metaKey: e.metaKey,
- });
- if (elementPointerDown !== null) {
- handlePointerDownOnElement(e, elementPointerDown);
- }
- pointer.setStyle("crosshair");
- } else if (isMouseMiddleButton) {
- setPanning({
- isPanning: true,
- panStart: transform.pan,
- // Diagram space depends on the current panning.
- // Use screen space to avoid circular dependencies and undefined behavior.
- cursorStart: pointer.spaces.screen,
- });
- pointer.setStyle("grabbing");
- }
- };
-
- const isDragging = () => {
- return dragging.type !== ObjectType.NONE && dragging.id !== -1;
- };
-
- const didDrag = () => {
- if (!isDragging()) return false;
- // checking any element is sufficient
- const { currentCoords, initialCoords } = bulkSelectedElements[0];
- return (
- currentCoords.x !== initialCoords.x || currentCoords.y !== initialCoords.y
- );
- };
-
- const didResize = (id) => {
- return !(
- areas[id].x === areaInitDimensions.x &&
- areas[id].y === areaInitDimensions.y &&
- areas[id].width === areaInitDimensions.width &&
- areas[id].height === areaInitDimensions.height
- );
- };
-
- const didPan = () =>
- !(
- transform.pan.x === panning.panStart.x &&
- transform.pan.y === panning.panStart.y
- );
-
- /**
- * @param {PointerEvent} e
- */
- const handlePointerUp = (e) => {
- if (selectedElement.open && !layout.sidebar) return;
-
- if (!e.isPrimary) return;
-
- if (didDrag()) {
- setUndoStack((prev) => [
- ...prev,
- {
- action: Action.MOVE,
- bulk: true,
- message: t("bulk_update"),
- elements: bulkSelectedElements.map((el) => ({
- id: el.id,
- type: el.type,
- undo: el.initialCoords,
- redo: el.currentCoords,
- })),
- },
- ]);
- setRedoStack([]);
- setBulkSelectedElements((prev) =>
- prev.map((el) => ({
- ...el,
- initialCoords: { ...el.currentCoords },
- })),
- );
- }
-
- if (bulkSelectRect.show) {
- setBulkSelectRect((prev) => ({
- ...prev,
- x2: pointer.spaces.diagram.x,
- y2: pointer.spaces.diagram.y,
- show: false,
- }));
- if (!isDragging()) {
- collectSelectedElements();
- }
- }
- setDragging(notDragging);
-
- if (panning.isPanning && didPan()) {
- setSaveState(State.SAVING);
- }
- setPanning((old) => ({ ...old, isPanning: false }));
- pointer.setStyle("default");
-
- if (linking) handleLinking();
- setLinking(false);
-
- if (areaResize.id !== -1 && didResize(areaResize.id)) {
- setUndoStack((prev) => [
- ...prev,
- {
- action: Action.EDIT,
- element: ObjectType.AREA,
- aid: areaResize.id,
- undo: {
- ...areas[areaResize.id],
- x: areaInitDimensions.x,
- y: areaInitDimensions.y,
- width: areaInitDimensions.width,
- height: areaInitDimensions.height,
- },
- redo: areas[areaResize.id],
- message: t("edit_area", {
- areaName: areas[areaResize.id].name,
- extra: "[resize]",
- }),
- },
- ]);
- setRedoStack([]);
- }
- setAreaResize({ id: -1, dir: "none" });
- setAreaInitDimensions({
- x: 0,
- y: 0,
- width: 0,
- height: 0,
- });
- };
-
- const handleGripField = () => {
- setPanning((old) => ({ ...old, isPanning: false }));
- setDragging(notDragging);
- setLinking(true);
- };
-
- const getCardinality = (startField, endField) => {
- const startIsUnique = startField.unique || startField.primary;
- const endIsUnique = endField.unique || endField.primary;
-
- if (startIsUnique && endIsUnique) {
- return Cardinality.ONE_TO_ONE;
- }
-
- if (startIsUnique && !endIsUnique) {
- return Cardinality.ONE_TO_MANY;
- }
-
- if (!startIsUnique && endIsUnique) {
- return Cardinality.MANY_TO_ONE;
- }
-
- return Cardinality.ONE_TO_ONE;
- };
-
- const handleLinking = () => {
- if (hoveredTable.tableId === null) return;
- if (hoveredTable.fieldId === null) return;
-
- const { fields: startTableFields, name: startTableName } = tables.find(
- (t) => t.id === linkingLine.startTableId,
- );
- const startField = startTableFields.find(
- (f) => f.id === linkingLine.startFieldId,
- );
- const { fields: endTableFields, name: endTableName } = tables.find(
- (t) => t.id === hoveredTable.tableId,
- );
- const endField = endTableFields.find((f) => f.id === hoveredTable.fieldId);
-
- if (!areFieldsCompatible(database, startField.type, endField.type)) {
- Toast.info(t("cannot_connect"));
- return;
- }
- if (
- linkingLine.startTableId === hoveredTable.tableId &&
- linkingLine.startFieldId === hoveredTable.fieldId
- )
- return;
-
- const cardinality = getCardinality(startField, endField);
-
- const newRelationship = {
- ...linkingLine,
- cardinality,
- endTableId: hoveredTable.tableId,
- endFieldId: hoveredTable.fieldId,
- updateConstraint: Constraint.NONE,
- deleteConstraint: Constraint.NONE,
- name: `fk_${startTableName}_${startField.name}_${endTableName}`,
- id: nanoid(),
- };
- delete newRelationship.startX;
- delete newRelationship.startY;
- delete newRelationship.endX;
- delete newRelationship.endY;
- addRelationship(newRelationship);
- };
-
- useEventListener(
- "wheel",
- (e) => {
- e.preventDefault();
-
- if (e.ctrlKey || e.metaKey) {
- // How "eager" the viewport is to
- // center the cursor's coordinates
- const eagernessFactor = 0.05;
- setTransform((prev) => ({
- pan: {
- x:
- prev.pan.x -
- (pointer.spaces.diagram.x - prev.pan.x) *
- eagernessFactor *
- Math.sign(e.deltaY),
- y:
- prev.pan.y -
- (pointer.spaces.diagram.y - prev.pan.y) *
- eagernessFactor *
- Math.sign(e.deltaY),
- },
- zoom: e.deltaY <= 0 ? prev.zoom * 1.05 : prev.zoom / 1.05,
- }));
- } else if (e.shiftKey) {
- setTransform((prev) => ({
- ...prev,
- pan: {
- ...prev.pan,
- x: prev.pan.x + e.deltaY / prev.zoom,
- },
- }));
- } else {
- setTransform((prev) => ({
- ...prev,
- pan: {
- x: prev.pan.x + e.deltaX / prev.zoom,
- y: prev.pan.y + e.deltaY / prev.zoom,
- },
- }));
- }
- },
- canvasRef,
- { passive: false },
- );
-
- return (
-
-
-