drawdb/src/components/canvas.jsx
2023-10-19 11:36:46 +03:00

483 lines
14 KiB
JavaScript

import React, { useContext, useRef, useState, useEffect } from "react";
import Table from "./Table";
import { Action, Cardinality, Constraint, ObjectType } from "../data/data";
import Area from "./Area";
import Relationship from "./Relationship";
import {
AreaContext,
NoteContext,
SettingsContext,
TableContext,
UndoRedoContext,
SelectContext,
} from "../pages/Editor";
import Note from "./Note";
import { Toast } from "@douyinfe/semi-ui";
export default function Canvas(props) {
const { tables, updateTable, relationships, addRelationship } =
useContext(TableContext);
const { areas, updateArea } = useContext(AreaContext);
const { notes, updateNote } = useContext(NoteContext);
const { settings, setSettings } = useContext(SettingsContext);
const { setUndoStack, setRedoStack } = useContext(UndoRedoContext);
const { selectedElement, setSelectedElement } = useContext(SelectContext);
const [dragging, setDragging] = useState({
element: ObjectType.NONE,
id: -1,
prevX: 0,
prevY: 0,
});
const [linking, setLinking] = useState(false);
const [line, setLine] = useState({
startTableId: -1,
startFieldId: -1,
endTableId: -1,
endFieldId: -1,
startX: 0,
startY: 0,
endX: 0,
endY: 0,
name: "",
cardinality: Cardinality.ONE_TO_ONE,
updateConstraint: Constraint.none,
deleteConstraint: Constraint.none,
mandatory: false,
});
const [offset, setOffset] = useState({ x: 0, y: 0 });
const [onRect, setOnRect] = useState({
tableId: -1,
field: -2,
});
const [panning, setPanning] = useState({ state: false, x: 0, y: 0 });
const [panOffset, setPanOffset] = useState({ x: 0, y: 0 });
const [areaResize, setAreaResize] = useState({ id: -1, dir: "none" });
const [initCoords, setInitCoords] = useState({
x: 0,
y: 0,
width: 0,
height: 0,
mouseX: 0,
mouseY: 0,
});
const [cursor, setCursor] = useState("default");
const canvas = useRef(null);
const handleMouseDownRect = (e, id, type) => {
const { clientX, clientY } = e;
if (type === ObjectType.TABLE) {
const table = tables.find((t) => t.id === id);
setOffset({
x: clientX / settings.zoom - table.x,
y: clientY / settings.zoom - table.y,
});
setDragging({
element: ObjectType.TABLE,
id: id,
prevX: table.x,
prevY: table.y,
});
} else if (type === ObjectType.AREA) {
const area = areas.find((t) => t.id === id);
setOffset({
x: clientX / settings.zoom - area.x,
y: clientY / settings.zoom - area.y,
});
setDragging({
element: ObjectType.AREA,
id: id,
prevX: area.x,
prevY: area.y,
});
} else if (type === ObjectType.NOTE) {
const note = notes.find((t) => t.id === id);
setOffset({
x: clientX / settings.zoom - note.x,
y: clientY / settings.zoom - note.y,
});
setDragging({
element: ObjectType.NOTE,
id: id,
prevX: note.x,
prevY: note.y,
});
}
setSelectedElement({
element: type,
id: id,
openDialogue: false,
openCollapse: false,
});
};
const handleMouseMove = (e) => {
if (linking) {
const rect = canvas.current.getBoundingClientRect();
const offsetX = rect.left;
const offsetY = rect.top;
setLine({
...line,
endX: (e.clientX - offsetX - settings.pan.x) / settings.zoom,
endY: (e.clientY - offsetY - settings.pan.y) / settings.zoom,
});
} else if (
panning.state &&
dragging.element === ObjectType.NONE &&
areaResize.id === -1
) {
const dx = e.clientX - panOffset.x;
const dy = e.clientY - panOffset.y;
setSettings((prev) => ({
...prev,
pan: { x: prev.pan.x + dx, y: prev.pan.y + dy },
}));
setPanOffset({ x: e.clientX, y: e.clientY });
} else if (dragging.element === ObjectType.TABLE && dragging.id >= 0) {
const dx = e.clientX / settings.zoom - offset.x;
const dy = e.clientY / settings.zoom - offset.y;
updateTable(dragging.id, { x: dx, y: dy }, true);
} else if (
dragging.element === ObjectType.AREA &&
dragging.id >= 0 &&
areaResize.id === -1
) {
const dx = e.clientX / settings.zoom - offset.x;
const dy = e.clientY / settings.zoom - offset.y;
updateArea(dragging.id, { x: dx, y: dy });
} else if (dragging.element === ObjectType.NOTE && dragging.id >= 0) {
const dx = e.clientX / settings.zoom - offset.x;
const dy = e.clientY / settings.zoom - offset.y;
updateNote(dragging.id, { x: dx, y: dy });
} else if (areaResize.id !== -1) {
if (areaResize.dir === "none") return;
let newX = initCoords.x;
let newY = initCoords.y;
let newWidth = initCoords.width;
let newHeight = initCoords.height;
const mouseX = e.clientX / settings.zoom;
const mouseY = e.clientY / settings.zoom;
setPanning({ state: false, x: 0, y: 0 });
if (areaResize.dir === "br") {
newWidth = initCoords.width + (mouseX - initCoords.mouseX);
newHeight = initCoords.height + (mouseY - initCoords.mouseY);
} else if (areaResize.dir === "tl") {
newX = initCoords.x + (mouseX - initCoords.mouseX);
newY = initCoords.y + (mouseY - initCoords.mouseY);
newWidth = initCoords.width - (mouseX - initCoords.mouseX);
newHeight = initCoords.height - (mouseY - initCoords.mouseY);
} else if (areaResize.dir === "tr") {
newY = initCoords.y + (mouseY - initCoords.mouseY);
newWidth = initCoords.width + (mouseX - initCoords.mouseX);
newHeight = initCoords.height - (mouseY - initCoords.mouseY);
} else if (areaResize.dir === "bl") {
newX = initCoords.x + (mouseX - initCoords.mouseX);
newWidth = initCoords.width - (mouseX - initCoords.mouseX);
newHeight = initCoords.height + (mouseY - initCoords.mouseY);
}
updateArea(areaResize.id, {
x: newX,
y: newY,
width: newWidth,
height: newHeight,
});
}
};
const handleMouseDown = (e) => {
setPanning({ state: true, ...settings.pan });
setPanOffset({ x: e.clientX, y: e.clientY });
setCursor("grabbing");
};
const coordsDidUpdate = (element) => {
switch (element) {
case ObjectType.TABLE:
return !(
dragging.prevX === tables[dragging.id].x &&
dragging.prevY === tables[dragging.id].y
);
case ObjectType.AREA:
return !(
dragging.prevX === areas[dragging.id].x &&
dragging.prevY === areas[dragging.id].y
);
case ObjectType.NOTE:
return !(
dragging.prevX === notes[dragging.id].x &&
dragging.prevY === notes[dragging.id].y
);
default:
return false;
}
};
const didResize = (id) => {
return !(
areas[id].x === initCoords.x &&
areas[id].y === initCoords.y &&
areas[id].width === initCoords.width &&
areas[id].height === initCoords.height
);
};
const didPan = () =>
!(settings.pan.x === panning.x && settings.pan.y === panning.y);
const getMoveInfo = () => {
switch (dragging.element) {
case ObjectType.TABLE:
return {
name: "table",
x: tables[dragging.id].x,
y: tables[dragging.id].y,
};
case ObjectType.AREA:
return {
name: "area",
x: areas[dragging.id].x,
y: areas[dragging.id].y,
};
case ObjectType.NOTE:
return {
name: "note",
x: notes[dragging.id].x,
y: notes[dragging.id].y,
};
default:
return false;
}
};
const handleMouseUp = (e) => {
if (coordsDidUpdate(dragging.element)) {
const info = getMoveInfo();
setUndoStack((prev) => [
...prev,
{
action: Action.MOVE,
element: dragging.element,
x: dragging.prevX,
y: dragging.prevY,
id: dragging.id,
message: `Move ${info.name} to (${info.x}, ${info.y})`,
},
]);
setRedoStack([]);
}
setDragging({ element: ObjectType.NONE, id: -1, prevX: 0, prevY: 0 });
if (panning.state && didPan()) {
setUndoStack((prev) => [
...prev,
{
action: Action.PAN,
undo: { x: panning.x, y: panning.y },
redo: settings.pan,
message: `Move diagram to (${settings.pan.x}, ${settings.pan.y})`,
},
]);
setRedoStack([]);
setSelectedElement({ element: ObjectType.NONE, id: -1 });
}
setPanning({ state: false, x: 0, y: 0 });
setCursor("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: initCoords.x,
y: initCoords.y,
width: initCoords.width,
height: initCoords.height,
},
redo: areas[areaResize.id],
message: `Resize area`,
},
]);
setRedoStack([]);
}
setAreaResize({ id: -1, dir: "none" });
setInitCoords({
x: 0,
y: 0,
width: 0,
height: 0,
mouseX: 0,
mouseY: 0,
});
};
const handleGripField = (id) => {
setPanning(false);
setDragging({ element: ObjectType.NONE, id: -1, prevX: 0, prevY: 0 });
setLinking(true);
};
const handleLinking = () => {
if (onRect.tableId < 0) return;
if (onRect.field < 0) return;
if (
tables[line.startTableId].fields[line.startFieldId].type !==
tables[onRect.tableId].fields[onRect.field].type
) {
Toast.info("Cannot connect");
return;
}
if (
line.startTableId === onRect.tableId &&
line.startFieldId === onRect.field
)
return;
addRelationship(true, {
...line,
endTableId: onRect.tableId,
endFieldId: onRect.field,
endX: tables[onRect.tableId].x + 15,
endY: tables[onRect.tableId].y + onRect.field * 36 + 69,
name: `${tables[line.startTableId].name}_${
tables[line.startTableId].fields[line.startFieldId].name
}_fk`,
id: relationships.length,
});
};
const handleMouseWheel = (e) => {
e.preventDefault();
setSettings((prev) => ({
...prev,
zoom: e.deltaY <= 0 ? prev.zoom * 1.05 : prev.zoom / 1.05,
}));
};
useEffect(() => {
const canvasElement = canvas.current;
canvasElement.addEventListener("wheel", handleMouseWheel, {
passive: false,
});
return () => {
canvasElement.removeEventListener("wheel", handleMouseWheel);
};
});
const theme = localStorage.getItem("theme");
return (
<div className="flex-grow h-full" id="canvas">
<div ref={canvas} className="w-full h-full">
<svg
onMouseMove={handleMouseMove}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
className="w-full h-full"
style={{
cursor: cursor,
backgroundColor: theme === "dark" ? "rgba(22, 22, 26, 1)" : "white",
}}
>
{settings.showGrid && (
<>
<defs>
<pattern
id="pattern-circles"
x="0"
y="0"
width="24"
height="24"
patternUnits="userSpaceOnUse"
patternContentUnits="userSpaceOnUse"
>
<circle
id="pattern-circle"
cx="4"
cy="4"
r="0.85"
fill="rgb(99, 152, 191)"
></circle>
</pattern>
</defs>
<rect
x="0"
y="0"
width="100%"
height="100%"
fill="url(#pattern-circles)"
></rect>
</>
)}
<g
style={{
transform: `translate(${settings.pan.x}px, ${settings.pan.y}px) scale(${settings.zoom})`,
transformOrigin: "top left",
}}
id="diagram"
>
{areas.map((a) => (
<Area
key={a.id}
areaData={a}
onMouseDown={(e) =>
handleMouseDownRect(e, a.id, ObjectType.AREA)
}
setResize={setAreaResize}
initCoords={initCoords}
setInitCoords={setInitCoords}
zoom={settings.zoom}
></Area>
))}
{tables.map((table) => (
<Table
key={table.id}
tableData={table}
setOnRect={setOnRect}
handleGripField={handleGripField}
setLine={setLine}
onMouseDown={(e) =>
handleMouseDownRect(e, table.id, ObjectType.TABLE)
}
active={
selectedElement.element === ObjectType.TABLE &&
selectedElement.id === table.id
}
moving={
dragging.element === ObjectType.TABLE &&
dragging.id === table.id
}
/>
))}
{linking && (
<path
d={`M ${line.startX} ${line.startY} L ${line.endX} ${line.endY}`}
stroke="red"
strokeDasharray="8,8"
/>
)}
{relationships.map((e, i) => (
<Relationship key={i} data={e} />
))}
{notes.map((n) => (
<Note
key={n.id}
data={n}
onMouseDown={(e) =>
handleMouseDownRect(e, n.id, ObjectType.NOTE)
}
></Note>
))}
</g>
</svg>
</div>
</div>
);
}