Merge pull request #148 from FelixZY/fzy/mobile/1
Basic touchscreen support (#147) and rewrite of coordinate management
This commit is contained in:
commit
4d0983b3f7
23
package-lock.json
generated
23
package-lock.json
generated
@ -17,6 +17,7 @@
|
||||
"@uiw/react-codemirror": "^4.21.25",
|
||||
"@vercel/analytics": "^1.2.2",
|
||||
"axios": "^1.6.2",
|
||||
"classnames": "^2.5.1",
|
||||
"dexie": "^3.2.4",
|
||||
"dexie-react-hooks": "^1.1.7",
|
||||
"file-saver": "^2.0.5",
|
||||
@ -33,7 +34,8 @@
|
||||
"react-hotkeys-hook": "^4.4.1",
|
||||
"react-i18next": "^14.1.1",
|
||||
"react-router-dom": "^6.21.0",
|
||||
"url": "^0.11.1"
|
||||
"url": "^0.11.1",
|
||||
"usehooks-ts": "^3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.43",
|
||||
@ -4283,6 +4285,11 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/lodash.debounce": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
@ -5882,6 +5889,20 @@
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
||||
"integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ=="
|
||||
},
|
||||
"node_modules/usehooks-ts": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-3.1.0.tgz",
|
||||
"integrity": "sha512-bBIa7yUyPhE1BCc0GmR96VU/15l/9gP1Ch5mYdLcFBaFGQsdmXkvjV0TtOqW1yUd6VjIwDunm+flSciCQXujiw==",
|
||||
"dependencies": {
|
||||
"lodash.debounce": "^4.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.15.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17 || ^18"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
@ -19,6 +19,7 @@
|
||||
"@uiw/react-codemirror": "^4.21.25",
|
||||
"@vercel/analytics": "^1.2.2",
|
||||
"axios": "^1.6.2",
|
||||
"classnames": "^2.5.1",
|
||||
"dexie": "^3.2.4",
|
||||
"dexie-react-hooks": "^1.1.7",
|
||||
"file-saver": "^2.0.5",
|
||||
@ -35,7 +36,8 @@
|
||||
"react-hotkeys-hook": "^4.4.1",
|
||||
"react-i18next": "^14.1.1",
|
||||
"react-router-dom": "^6.21.0",
|
||||
"url": "^0.11.1"
|
||||
"url": "^0.11.1",
|
||||
"usehooks-ts": "^3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.43",
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { Button, Popover, Input } from "@douyinfe/semi-ui";
|
||||
import { IconEdit, IconDeleteStroked } from "@douyinfe/semi-icons";
|
||||
import {
|
||||
@ -9,22 +9,33 @@ import {
|
||||
State,
|
||||
} from "../../data/constants";
|
||||
import {
|
||||
useCanvas,
|
||||
useLayout,
|
||||
useSettings,
|
||||
useUndoRedo,
|
||||
useSelect,
|
||||
useAreas,
|
||||
useSaveState,
|
||||
useTransform,
|
||||
} from "../../hooks";
|
||||
import ColorPalette from "../ColorPicker";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHover } from "usehooks-ts";
|
||||
|
||||
export default function Area({ data, onMouseDown, setResize, setInitCoords }) {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
export default function Area({
|
||||
data,
|
||||
onPointerDown,
|
||||
setResize,
|
||||
setInitCoords,
|
||||
}) {
|
||||
const ref = useRef(null);
|
||||
const isHovered = useHover(ref);
|
||||
const {
|
||||
pointer: {
|
||||
spaces: { diagram: pointer },
|
||||
},
|
||||
} = useCanvas();
|
||||
const { layout } = useLayout();
|
||||
const { settings } = useSettings();
|
||||
const { transform } = useTransform();
|
||||
const { setSaveState } = useSaveState();
|
||||
const { selectedElement, setSelectedElement } = useSelect();
|
||||
|
||||
@ -35,8 +46,8 @@ export default function Area({ data, onMouseDown, setResize, setInitCoords }) {
|
||||
y: data.y,
|
||||
width: data.width,
|
||||
height: data.height,
|
||||
mouseX: e.clientX / transform.zoom,
|
||||
mouseY: e.clientY / transform.zoom,
|
||||
pointerX: pointer.x,
|
||||
pointerY: pointer.y,
|
||||
});
|
||||
};
|
||||
|
||||
@ -84,21 +95,18 @@ export default function Area({ data, onMouseDown, setResize, setInitCoords }) {
|
||||
selectedElement.open;
|
||||
|
||||
return (
|
||||
<g
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
>
|
||||
<g ref={ref}>
|
||||
<foreignObject
|
||||
key={data.id}
|
||||
x={data.x}
|
||||
y={data.y}
|
||||
width={data.width > 0 ? data.width : 0}
|
||||
height={data.height > 0 ? data.height : 0}
|
||||
onMouseDown={onMouseDown}
|
||||
onPointerDown={onPointerDown}
|
||||
>
|
||||
<div
|
||||
className={`border-2 ${
|
||||
hovered
|
||||
isHovered
|
||||
? "border-dashed border-blue-500"
|
||||
: selectedElement.element === ObjectType.AREA &&
|
||||
selectedElement.id === data.id
|
||||
@ -114,7 +122,7 @@ export default function Area({ data, onMouseDown, setResize, setInitCoords }) {
|
||||
<div className="text-color select-none overflow-hidden text-ellipsis">
|
||||
{data.name}
|
||||
</div>
|
||||
{(hovered || (areaIsSelected() && !layout.sidebar)) && (
|
||||
{(isHovered || (areaIsSelected() && !layout.sidebar)) && (
|
||||
<Popover
|
||||
visible={areaIsSelected() && !layout.sidebar}
|
||||
onClickOutSide={onClickOutSide}
|
||||
@ -139,7 +147,7 @@ export default function Area({ data, onMouseDown, setResize, setInitCoords }) {
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
{hovered && (
|
||||
{isHovered && (
|
||||
<>
|
||||
<circle
|
||||
cx={data.x}
|
||||
@ -149,7 +157,7 @@ export default function Area({ data, onMouseDown, setResize, setInitCoords }) {
|
||||
stroke="#5891db"
|
||||
strokeWidth={2}
|
||||
cursor="nwse-resize"
|
||||
onMouseDown={(e) => handleResize(e, "tl")}
|
||||
onPointerDown={(e) => e.isPrimary && handleResize(e, "tl")}
|
||||
/>
|
||||
<circle
|
||||
cx={data.x + data.width}
|
||||
@ -159,7 +167,7 @@ export default function Area({ data, onMouseDown, setResize, setInitCoords }) {
|
||||
stroke="#5891db"
|
||||
strokeWidth={2}
|
||||
cursor="nesw-resize"
|
||||
onMouseDown={(e) => handleResize(e, "tr")}
|
||||
onPointerDown={(e) => e.isPrimary && handleResize(e, "tr")}
|
||||
/>
|
||||
<circle
|
||||
cx={data.x}
|
||||
@ -169,7 +177,7 @@ export default function Area({ data, onMouseDown, setResize, setInitCoords }) {
|
||||
stroke="#5891db"
|
||||
strokeWidth={2}
|
||||
cursor="nesw-resize"
|
||||
onMouseDown={(e) => handleResize(e, "bl")}
|
||||
onPointerDown={(e) => e.isPrimary && handleResize(e, "bl")}
|
||||
/>
|
||||
<circle
|
||||
cx={data.x + data.width}
|
||||
@ -179,7 +187,7 @@ export default function Area({ data, onMouseDown, setResize, setInitCoords }) {
|
||||
stroke="#5891db"
|
||||
strokeWidth={2}
|
||||
cursor="nwse-resize"
|
||||
onMouseDown={(e) => handleResize(e, "br")}
|
||||
onPointerDown={(e) => e.isPrimary && handleResize(e, "br")}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useRef, useState, useEffect } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import {
|
||||
Action,
|
||||
Cardinality,
|
||||
@ -11,6 +11,7 @@ import Area from "./Area";
|
||||
import Relationship from "./Relationship";
|
||||
import Note from "./Note";
|
||||
import {
|
||||
useCanvas,
|
||||
useSettings,
|
||||
useTransform,
|
||||
useDiagram,
|
||||
@ -22,9 +23,18 @@ import {
|
||||
} from "../../hooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { diagram } from "../../data/heroDiagram";
|
||||
import { useEventListener } from "usehooks-ts";
|
||||
|
||||
export default function Canvas() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const canvasRef = useRef(null);
|
||||
const canvasContextValue = useCanvas();
|
||||
const {
|
||||
canvas: { viewBox },
|
||||
pointer,
|
||||
} = canvasContextValue;
|
||||
|
||||
const { tables, updateTable, relationships, addRelationship } = useDiagram();
|
||||
const { areas, updateArea } = useAreas();
|
||||
const { notes, updateNote } = useNotes();
|
||||
@ -50,17 +60,15 @@ export default function Canvas() {
|
||||
endX: 0,
|
||||
endY: 0,
|
||||
});
|
||||
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
||||
const [grabOffset, setGrabOffset] = useState({ x: 0, y: 0 });
|
||||
const [hoveredTable, setHoveredTable] = useState({
|
||||
tableId: -1,
|
||||
field: -2,
|
||||
});
|
||||
const [panning, setPanning] = useState({
|
||||
isPanning: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
dx: 0,
|
||||
dy: 0,
|
||||
panStart: { x: 0, y: 0 },
|
||||
cursorStart: { x: 0, y: 0 },
|
||||
});
|
||||
const [areaResize, setAreaResize] = useState({ id: -1, dir: "none" });
|
||||
const [initCoords, setInitCoords] = useState({
|
||||
@ -68,20 +76,23 @@ export default function Canvas() {
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
mouseX: 0,
|
||||
mouseY: 0,
|
||||
pointerX: 0,
|
||||
pointerY: 0,
|
||||
});
|
||||
const [cursor, setCursor] = useState("default");
|
||||
|
||||
const canvas = useRef(null);
|
||||
/**
|
||||
* @param {PointerEvent} e
|
||||
* @param {*} id
|
||||
* @param {ObjectType[keyof ObjectType]} type
|
||||
*/
|
||||
const handlePointerDownOnElement = (e, id, type) => {
|
||||
if (!e.isPrimary) return;
|
||||
|
||||
const handleMouseDownOnElement = (e, id, type) => {
|
||||
const { clientX, clientY } = e;
|
||||
if (type === ObjectType.TABLE) {
|
||||
const table = tables.find((t) => t.id === id);
|
||||
setOffset({
|
||||
x: clientX / transform.zoom - table.x,
|
||||
y: clientY / transform.zoom - table.y,
|
||||
setGrabOffset({
|
||||
x: table.x - pointer.spaces.diagram.x,
|
||||
y: table.y - pointer.spaces.diagram.y,
|
||||
});
|
||||
setDragging({
|
||||
element: type,
|
||||
@ -91,9 +102,9 @@ export default function Canvas() {
|
||||
});
|
||||
} else if (type === ObjectType.AREA) {
|
||||
const area = areas.find((t) => t.id === id);
|
||||
setOffset({
|
||||
x: clientX / transform.zoom - area.x,
|
||||
y: clientY / transform.zoom - area.y,
|
||||
setGrabOffset({
|
||||
x: area.x - pointer.spaces.diagram.x,
|
||||
y: area.y - pointer.spaces.diagram.y,
|
||||
});
|
||||
setDragging({
|
||||
element: type,
|
||||
@ -103,9 +114,9 @@ export default function Canvas() {
|
||||
});
|
||||
} else if (type === ObjectType.NOTE) {
|
||||
const note = notes.find((t) => t.id === id);
|
||||
setOffset({
|
||||
x: clientX / transform.zoom - note.x,
|
||||
y: clientY / transform.zoom - note.y,
|
||||
setGrabOffset({
|
||||
x: note.x - pointer.spaces.diagram.x,
|
||||
y: note.y - pointer.spaces.diagram.y,
|
||||
});
|
||||
setDragging({
|
||||
element: type,
|
||||
@ -122,13 +133,17 @@ export default function Canvas() {
|
||||
}));
|
||||
};
|
||||
|
||||
const handleMouseMove = (e) => {
|
||||
/**
|
||||
* @param {PointerEvent} e
|
||||
*/
|
||||
const handlePointerMove = (e) => {
|
||||
if (!e.isPrimary) return;
|
||||
|
||||
if (linking) {
|
||||
const rect = canvas.current.getBoundingClientRect();
|
||||
setLinkingLine({
|
||||
...linkingLine,
|
||||
endX: (e.clientX - rect.left - transform.pan?.x) / transform.zoom,
|
||||
endY: (e.clientY - rect.top - transform.pan?.y) / transform.zoom,
|
||||
endX: pointer.spaces.diagram.x,
|
||||
endY: pointer.spaces.diagram.y,
|
||||
});
|
||||
} else if (
|
||||
panning.isPanning &&
|
||||
@ -138,60 +153,80 @@ export default function Canvas() {
|
||||
if (!settings.panning) {
|
||||
return;
|
||||
}
|
||||
const dx = e.clientX - panning.dx;
|
||||
const dy = e.clientY - panning.dy;
|
||||
setTransform((prev) => ({
|
||||
...prev,
|
||||
pan: { x: prev.pan?.x + dx, y: prev.pan?.y + dy },
|
||||
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,
|
||||
},
|
||||
}));
|
||||
setPanning((prev) => ({ ...prev, dx: e.clientX, dy: e.clientY }));
|
||||
} else if (dragging.element === ObjectType.TABLE && dragging.id >= 0) {
|
||||
const dx = e.clientX / transform.zoom - offset.x;
|
||||
const dy = e.clientY / transform.zoom - offset.y;
|
||||
updateTable(dragging.id, { x: dx, y: dy });
|
||||
updateTable(dragging.id, {
|
||||
x: pointer.spaces.diagram.x + grabOffset.x,
|
||||
y: pointer.spaces.diagram.y + grabOffset.y,
|
||||
});
|
||||
} else if (
|
||||
dragging.element === ObjectType.AREA &&
|
||||
dragging.id >= 0 &&
|
||||
areaResize.id === -1
|
||||
) {
|
||||
const dx = e.clientX / transform.zoom - offset.x;
|
||||
const dy = e.clientY / transform.zoom - offset.y;
|
||||
updateArea(dragging.id, { x: dx, y: dy });
|
||||
updateArea(dragging.id, {
|
||||
x: pointer.spaces.diagram.x + grabOffset.x,
|
||||
y: pointer.spaces.diagram.y + grabOffset.y,
|
||||
});
|
||||
} else if (dragging.element === ObjectType.NOTE && dragging.id >= 0) {
|
||||
const dx = e.clientX / transform.zoom - offset.x;
|
||||
const dy = e.clientY / transform.zoom - offset.y;
|
||||
updateNote(dragging.id, { x: dx, y: dy });
|
||||
updateNote(dragging.id, {
|
||||
x: pointer.spaces.diagram.x + grabOffset.x,
|
||||
y: pointer.spaces.diagram.y + grabOffset.y,
|
||||
});
|
||||
} else if (areaResize.id !== -1) {
|
||||
if (areaResize.dir === "none") return;
|
||||
let newDims = { ...initCoords };
|
||||
delete newDims.mouseX;
|
||||
delete newDims.mouseY;
|
||||
const mouseX = e.clientX / transform.zoom;
|
||||
const mouseY = e.clientY / transform.zoom;
|
||||
setPanning({ isPanning: false, x: 0, y: 0 });
|
||||
if (areaResize.dir === "br") {
|
||||
newDims.width = initCoords.width + (mouseX - initCoords.mouseX);
|
||||
newDims.height = initCoords.height + (mouseY - initCoords.mouseY);
|
||||
} else if (areaResize.dir === "tl") {
|
||||
newDims.x = initCoords.x + (mouseX - initCoords.mouseX);
|
||||
newDims.y = initCoords.y + (mouseY - initCoords.mouseY);
|
||||
newDims.width = initCoords.width - (mouseX - initCoords.mouseX);
|
||||
newDims.height = initCoords.height - (mouseY - initCoords.mouseY);
|
||||
} else if (areaResize.dir === "tr") {
|
||||
newDims.y = initCoords.y + (mouseY - initCoords.mouseY);
|
||||
newDims.width = initCoords.width + (mouseX - initCoords.mouseX);
|
||||
newDims.height = initCoords.height - (mouseY - initCoords.mouseY);
|
||||
} else if (areaResize.dir === "bl") {
|
||||
newDims.x = initCoords.x + (mouseX - initCoords.mouseX);
|
||||
newDims.width = initCoords.width - (mouseX - initCoords.mouseX);
|
||||
newDims.height = initCoords.height + (mouseY - initCoords.mouseY);
|
||||
delete newDims.pointerX;
|
||||
delete newDims.pointerY;
|
||||
setPanning((old) => ({ ...old, isPanning: false }));
|
||||
|
||||
switch (areaResize.dir) {
|
||||
case "br":
|
||||
newDims.width = pointer.spaces.diagram.x - initCoords.x;
|
||||
newDims.height = pointer.spaces.diagram.y - initCoords.y;
|
||||
break;
|
||||
case "tl":
|
||||
newDims.x = pointer.spaces.diagram.x;
|
||||
newDims.y = pointer.spaces.diagram.y;
|
||||
newDims.width =
|
||||
initCoords.x + initCoords.width - pointer.spaces.diagram.x;
|
||||
newDims.height =
|
||||
initCoords.y + initCoords.height - pointer.spaces.diagram.y;
|
||||
break;
|
||||
case "tr":
|
||||
newDims.y = pointer.spaces.diagram.y;
|
||||
newDims.width = pointer.spaces.diagram.x - initCoords.x;
|
||||
newDims.height =
|
||||
initCoords.y + initCoords.height - pointer.spaces.diagram.y;
|
||||
break;
|
||||
case "bl":
|
||||
newDims.x = pointer.spaces.diagram.x;
|
||||
newDims.width =
|
||||
initCoords.x + initCoords.width - pointer.spaces.diagram.x;
|
||||
newDims.height = pointer.spaces.diagram.y - initCoords.y;
|
||||
break;
|
||||
}
|
||||
|
||||
updateArea(areaResize.id, { ...newDims });
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseDown = (e) => {
|
||||
/**
|
||||
* @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 &&
|
||||
@ -202,11 +237,12 @@ export default function Canvas() {
|
||||
|
||||
setPanning({
|
||||
isPanning: true,
|
||||
...transform.pan,
|
||||
dx: e.clientX,
|
||||
dy: e.clientY,
|
||||
panStart: transform.pan,
|
||||
// Diagram space depends on the current panning.
|
||||
// Use screen space to avoid circular dependencies and undefined behavior.
|
||||
cursorStart: pointer.spaces.screen,
|
||||
});
|
||||
setCursor("grabbing");
|
||||
pointer.setStyle("grabbing");
|
||||
};
|
||||
|
||||
const coordsDidUpdate = (element) => {
|
||||
@ -241,7 +277,7 @@ export default function Canvas() {
|
||||
};
|
||||
|
||||
const didPan = () =>
|
||||
!(transform.pan?.x === panning.x && transform.pan?.y === panning.y);
|
||||
!(transform.pan.x === panning.x && transform.pan.y === panning.y);
|
||||
|
||||
const getMovedElementDetails = () => {
|
||||
switch (dragging.element) {
|
||||
@ -268,7 +304,12 @@ export default function Canvas() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
/**
|
||||
* @param {PointerEvent} e
|
||||
*/
|
||||
const handlePointerUp = (e) => {
|
||||
if (!e.isPrimary) return;
|
||||
|
||||
if (coordsDidUpdate(dragging.element)) {
|
||||
const info = getMovedElementDetails();
|
||||
setUndoStack((prev) => [
|
||||
@ -311,8 +352,8 @@ export default function Canvas() {
|
||||
open: false,
|
||||
}));
|
||||
}
|
||||
setPanning({ isPanning: false, x: 0, y: 0 });
|
||||
setCursor("default");
|
||||
setPanning((old) => ({ ...old, isPanning: false }));
|
||||
pointer.setStyle("default");
|
||||
if (linking) handleLinking();
|
||||
setLinking(false);
|
||||
if (areaResize.id !== -1 && didResize(areaResize.id)) {
|
||||
@ -344,13 +385,13 @@ export default function Canvas() {
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
mouseX: 0,
|
||||
mouseY: 0,
|
||||
pointerX: 0,
|
||||
pointerY: 0,
|
||||
});
|
||||
};
|
||||
|
||||
const handleGripField = () => {
|
||||
setPanning(false);
|
||||
setPanning((old) => ({ ...old, isPanning: false }));
|
||||
setDragging({ element: ObjectType.NONE, id: -1, prevX: 0, prevY: 0 });
|
||||
setLinking(true);
|
||||
};
|
||||
@ -390,121 +431,217 @@ export default function Canvas() {
|
||||
addRelationship(newRelationship);
|
||||
};
|
||||
|
||||
const handleMouseWheel = (e) => {
|
||||
e.preventDefault();
|
||||
setTransform((prev) => ({
|
||||
...prev,
|
||||
zoom: e.deltaY <= 0 ? prev.zoom * 1.05 : prev.zoom / 1.05,
|
||||
}));
|
||||
};
|
||||
// Handle mouse wheel scrolling
|
||||
useEventListener(
|
||||
"wheel",
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
useEffect(() => {
|
||||
const canvasElement = canvas.current;
|
||||
canvasElement.addEventListener("wheel", handleMouseWheel, {
|
||||
passive: false,
|
||||
});
|
||||
return () => {
|
||||
canvasElement.removeEventListener("wheel", handleMouseWheel);
|
||||
};
|
||||
});
|
||||
if (e.ctrlKey) {
|
||||
// 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: {
|
||||
...prev.pan,
|
||||
y: prev.pan.y + e.deltaY / prev.zoom,
|
||||
},
|
||||
}));
|
||||
}
|
||||
},
|
||||
canvasRef,
|
||||
{ passive: false },
|
||||
);
|
||||
|
||||
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
|
||||
<div className="flex-grow h-full touch-none" id="canvas">
|
||||
<div
|
||||
className="w-full h-full"
|
||||
style={{
|
||||
cursor: pointer.style,
|
||||
backgroundColor: theme === "dark" ? "rgba(22, 22, 26, 1)" : "white",
|
||||
}}
|
||||
>
|
||||
{settings.showGrid && (
|
||||
<svg className="absolute w-full h-full">
|
||||
<defs>
|
||||
<pattern
|
||||
id="pattern-circles"
|
||||
x="0"
|
||||
y="0"
|
||||
width="100%"
|
||||
height="100%"
|
||||
fill="url(#pattern-circles)"
|
||||
></rect>
|
||||
</>
|
||||
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>
|
||||
</svg>
|
||||
)}
|
||||
<svg
|
||||
ref={canvasRef}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerUp={handlePointerUp}
|
||||
className="absolute w-full h-full touch-none"
|
||||
viewBox={`${viewBox.left} ${viewBox.top} ${viewBox.width} ${viewBox.height}`}
|
||||
>
|
||||
{areas.map((a) => (
|
||||
<Area
|
||||
key={a.id}
|
||||
data={a}
|
||||
onPointerDown={(e) =>
|
||||
handlePointerDownOnElement(e, a.id, ObjectType.AREA)
|
||||
}
|
||||
setResize={setAreaResize}
|
||||
setInitCoords={setInitCoords}
|
||||
/>
|
||||
))}
|
||||
{relationships.map((e, i) => (
|
||||
<Relationship key={i} data={e} />
|
||||
))}
|
||||
{tables.map((table) => (
|
||||
<Table
|
||||
key={table.id}
|
||||
tableData={table}
|
||||
setHoveredTable={setHoveredTable}
|
||||
handleGripField={handleGripField}
|
||||
setLinkingLine={setLinkingLine}
|
||||
onPointerDown={(e) =>
|
||||
handlePointerDownOnElement(e, table.id, ObjectType.TABLE)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{linking && (
|
||||
<path
|
||||
d={`M ${linkingLine.startX} ${linkingLine.startY} L ${linkingLine.endX} ${linkingLine.endY}`}
|
||||
stroke="red"
|
||||
strokeDasharray="8,8"
|
||||
className="pointer-events-none touch-none"
|
||||
/>
|
||||
)}
|
||||
<g
|
||||
style={{
|
||||
transform: `translate(${transform.pan?.x}px, ${transform.pan?.y}px) scale(${transform.zoom})`,
|
||||
transformOrigin: "top left",
|
||||
}}
|
||||
id="diagram"
|
||||
>
|
||||
{areas.map((a) => (
|
||||
<Area
|
||||
key={a.id}
|
||||
data={a}
|
||||
onMouseDown={(e) =>
|
||||
handleMouseDownOnElement(e, a.id, ObjectType.AREA)
|
||||
}
|
||||
setResize={setAreaResize}
|
||||
setInitCoords={setInitCoords}
|
||||
/>
|
||||
))}
|
||||
{relationships.map((e, i) => (
|
||||
<Relationship key={i} data={e} />
|
||||
))}
|
||||
{tables.map((table) => (
|
||||
<Table
|
||||
key={table.id}
|
||||
tableData={table}
|
||||
setHoveredTable={setHoveredTable}
|
||||
handleGripField={handleGripField}
|
||||
setLinkingLine={setLinkingLine}
|
||||
onMouseDown={(e) =>
|
||||
handleMouseDownOnElement(e, table.id, ObjectType.TABLE)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{linking && (
|
||||
<path
|
||||
d={`M ${linkingLine.startX} ${linkingLine.startY} L ${linkingLine.endX} ${linkingLine.endY}`}
|
||||
stroke="red"
|
||||
strokeDasharray="8,8"
|
||||
/>
|
||||
)}
|
||||
{notes.map((n) => (
|
||||
<Note
|
||||
key={n.id}
|
||||
data={n}
|
||||
onMouseDown={(e) =>
|
||||
handleMouseDownOnElement(e, n.id, ObjectType.NOTE)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</g>
|
||||
{notes.map((n) => (
|
||||
<Note
|
||||
key={n.id}
|
||||
data={n}
|
||||
onPointerDown={(e) =>
|
||||
handlePointerDownOnElement(e, n.id, ObjectType.NOTE)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
</div>
|
||||
{settings.showDebugCoordinates && (
|
||||
<div className="fixed flex flex-col flex-wrap gap-6 bg-[rgba(var(--semi-grey-1),var(--tw-bg-opacity))]/40 border border-color bottom-4 right-4 p-4 rounded-xl backdrop-blur-sm pointer-events-none select-none">
|
||||
<table className="table-auto grow">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left" colSpan={3}>
|
||||
{t("transform")}
|
||||
</th>
|
||||
</tr>
|
||||
<tr className="italic [&_th]:font-normal [&_th]:text-right">
|
||||
<th>pan x</th>
|
||||
<th>pan y</th>
|
||||
<th>scale</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="[&_td]:text-right [&_td]:min-w-[8ch]">
|
||||
<tr>
|
||||
<td>{transform.pan.x.toFixed(2)}</td>
|
||||
<td>{transform.pan.y.toFixed(2)}</td>
|
||||
<td>{transform.zoom.toFixed(4)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table className="table-auto grow [&_th]:text-left [&_th:not(:first-of-type)]:text-right [&_td:not(:first-of-type)]:text-right [&_td]:min-w-[8ch]">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colSpan={4}>{t("viewbox")}</th>
|
||||
</tr>
|
||||
<tr className="italic [&_th]:font-normal">
|
||||
<th>left</th>
|
||||
<th>top</th>
|
||||
<th>width</th>
|
||||
<th>height</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{viewBox.left.toFixed(2)}</td>
|
||||
<td>{viewBox.top.toFixed(2)}</td>
|
||||
<td>{viewBox.width.toFixed(2)}</td>
|
||||
<td>{viewBox.height.toFixed(2)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table className="table-auto grow [&_th]:text-left [&_th:not(:first-of-type)]:text-right [&_td:not(:first-of-type)]:text-right [&_td]:min-w-[8ch]">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colSpan={3}>{t("cursor_coordinates")}</th>
|
||||
</tr>
|
||||
<tr className="italic [&_th]:font-normal">
|
||||
<th>{t("coordinate_space")}</th>
|
||||
<th>x</th>
|
||||
<th>y</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{t("coordinate_space_screen")}</td>
|
||||
<td>{pointer.spaces.screen.x.toFixed(2)}</td>
|
||||
<td>{pointer.spaces.screen.y.toFixed(2)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{t("coordinate_space_diagram")}</td>
|
||||
<td>{pointer.spaces.diagram.x.toFixed(2)}</td>
|
||||
<td>{pointer.spaces.diagram.y.toFixed(2)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ import {
|
||||
} from "../../hooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function Note({ data, onMouseDown }) {
|
||||
export default function Note({ data, onPointerDown }) {
|
||||
const w = 180;
|
||||
const r = 3;
|
||||
const fold = 24;
|
||||
@ -83,8 +83,13 @@ export default function Note({ data, onMouseDown }) {
|
||||
|
||||
return (
|
||||
<g
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
onPointerEnter={(e) => e.isPrimary && setHovered(true)}
|
||||
onPointerLeave={(e) => e.isPrimary && setHovered(false)}
|
||||
onPointerDown={(e) => {
|
||||
// Required for onPointerLeave to trigger when a touch pointer leaves
|
||||
// https://stackoverflow.com/a/70976017/1137077
|
||||
e.target.releasePointerCapture(e.pointerId);
|
||||
}}
|
||||
>
|
||||
<path
|
||||
d={`M${data.x + fold} ${data.y} L${data.x + w - r} ${
|
||||
@ -133,7 +138,7 @@ export default function Note({ data, onMouseDown }) {
|
||||
y={data.y}
|
||||
width={w}
|
||||
height={data.height}
|
||||
onMouseDown={onMouseDown}
|
||||
onPointerDown={onPointerDown}
|
||||
>
|
||||
<div className="text-gray-900 select-none w-full h-full cursor-move px-3 py-2">
|
||||
<div className="flex justify-between gap-1 w-full">
|
||||
|
@ -22,7 +22,7 @@ export default function Table(props) {
|
||||
const [hoveredField, setHoveredField] = useState(-1);
|
||||
const {
|
||||
tableData,
|
||||
onMouseDown,
|
||||
onPointerDown,
|
||||
setHoveredTable,
|
||||
handleGripField,
|
||||
setLinkingLine,
|
||||
@ -67,7 +67,7 @@ export default function Table(props) {
|
||||
width={settings.tableWidth}
|
||||
height={height}
|
||||
className="group drop-shadow-lg rounded-md cursor-move"
|
||||
onMouseDown={onMouseDown}
|
||||
onPointerDown={onPointerDown}
|
||||
>
|
||||
<div
|
||||
onDoubleClick={openEditor}
|
||||
@ -266,16 +266,25 @@ export default function Table(props) {
|
||||
? ""
|
||||
: "border-b border-gray-400"
|
||||
} group h-[36px] px-2 py-1 flex justify-between items-center gap-1 w-full overflow-hidden`}
|
||||
onMouseEnter={() => {
|
||||
onPointerEnter={(e) => {
|
||||
if (!e.isPrimary) return;
|
||||
|
||||
setHoveredField(index);
|
||||
setHoveredTable({
|
||||
tableId: tableData.id,
|
||||
field: index,
|
||||
});
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
onPointerLeave={(e) => {
|
||||
if (!e.isPrimary) return;
|
||||
|
||||
setHoveredField(-1);
|
||||
}}
|
||||
onPointerDown={(e) => {
|
||||
// Required for onPointerLeave to trigger when a touch pointer leaves
|
||||
// https://stackoverflow.com/a/70976017/1137077
|
||||
e.target.releasePointerCapture(e.pointerId);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`${
|
||||
@ -284,7 +293,9 @@ export default function Table(props) {
|
||||
>
|
||||
<button
|
||||
className="flex-shrink-0 w-[10px] h-[10px] bg-[#2f68adcc] rounded-full"
|
||||
onMouseDown={() => {
|
||||
onPointerDown={(e) => {
|
||||
if (!e.isPrimary) return;
|
||||
|
||||
handleGripField(index);
|
||||
setLinkingLine((prev) => ({
|
||||
...prev,
|
||||
|
@ -1158,6 +1158,18 @@ export default function ControlPanel({
|
||||
showCardinality: !prev.showCardinality,
|
||||
})),
|
||||
},
|
||||
show_debug_coordinates: {
|
||||
state: settings.showDebugCoordinates ? (
|
||||
<i className="bi bi-toggle-on" />
|
||||
) : (
|
||||
<i className="bi bi-toggle-off" />
|
||||
),
|
||||
function: () =>
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
showDebugCoordinates: !prev.showDebugCoordinates,
|
||||
})),
|
||||
},
|
||||
theme: {
|
||||
children: [
|
||||
{
|
||||
@ -1526,8 +1538,13 @@ export default function ControlPanel({
|
||||
)}
|
||||
<div
|
||||
className="text-xl me-1"
|
||||
onMouseEnter={() => setShowEditName(true)}
|
||||
onMouseLeave={() => setShowEditName(false)}
|
||||
onPointerEnter={(e) => e.isPrimary && setShowEditName(true)}
|
||||
onPointerLeave={(e) => e.isPrimary && setShowEditName(false)}
|
||||
onPointerDown={(e) => {
|
||||
// Required for onPointerLeave to trigger when a touch pointer leaves
|
||||
// https://stackoverflow.com/a/70976017/1137077
|
||||
e.target.releasePointerCapture(e.pointerId);
|
||||
}}
|
||||
onClick={() => setModal(MODAL.RENAME)}
|
||||
>
|
||||
{window.name.split(" ")[0] === "t" ? "Templates/" : "Diagrams/"}
|
||||
|
@ -83,7 +83,7 @@ export default function SidePanel({ width, resize, setResize }) {
|
||||
className={`flex justify-center items-center p-1 h-auto hover-2 cursor-col-resize ${
|
||||
resize && "bg-semi-grey-2"
|
||||
}`}
|
||||
onMouseDown={() => setResize(true)}
|
||||
onPointerDown={(e) => e.isPrimary && setResize(true)}
|
||||
>
|
||||
<div className="w-1 border-x border-color h-1/6" />
|
||||
</div>
|
||||
|
@ -89,7 +89,6 @@ function positionEditorElement(editor, rect) {
|
||||
function FloatingLinkEditor({ editor }) {
|
||||
const editorRef = useRef(null);
|
||||
const inputRef = useRef(null);
|
||||
const mouseDownRef = useRef(false);
|
||||
const [linkUrl, setLinkUrl] = useState("");
|
||||
const [isEditMode, setEditMode] = useState(false);
|
||||
const [lastSelection, setLastSelection] = useState(null);
|
||||
@ -134,9 +133,7 @@ function FloatingLinkEditor({ editor }) {
|
||||
rect = domRange.getBoundingClientRect();
|
||||
}
|
||||
|
||||
if (!mouseDownRef.current) {
|
||||
positionEditorElement(editorElem, rect);
|
||||
}
|
||||
positionEditorElement(editorElem, rect);
|
||||
setLastSelection(selection);
|
||||
} else if (!activeElement || activeElement.className !== "link-input") {
|
||||
positionEditorElement(editorElem, null);
|
||||
|
@ -24,9 +24,17 @@ function Table({ table, grab }) {
|
||||
width={tableWidth}
|
||||
height={height}
|
||||
className="drop-shadow-lg rounded-md cursor-move"
|
||||
onMouseDown={grab}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
onPointerDown={(e) => {
|
||||
// Required for onPointerLeave to trigger when a touch pointer leaves
|
||||
// https://stackoverflow.com/a/70976017/1137077
|
||||
e.target.releasePointerCapture(e.pointerId);
|
||||
|
||||
if (!e.isPrimary) return;
|
||||
|
||||
grab(e);
|
||||
}}
|
||||
onPointerEnter={(e) => e.isPrimary && setIsHovered(true)}
|
||||
onPointerLeave={(e) => e.isPrimary && setIsHovered(false)}
|
||||
>
|
||||
<div
|
||||
className={`border-2 ${
|
||||
@ -46,8 +54,13 @@ function Table({ table, grab }) {
|
||||
className={`${
|
||||
i === table.fields.length - 1 ? "" : "border-b border-gray-400"
|
||||
} h-[36px] px-2 py-1 flex justify-between`}
|
||||
onMouseEnter={() => setHoveredField(i)}
|
||||
onMouseLeave={() => setHoveredField(-1)}
|
||||
onPointerEnter={(e) => e.isPrimary && setHoveredField(i)}
|
||||
onPointerLeave={(e) => e.isPrimary && setHoveredField(-1)}
|
||||
onPointerDown={(e) => {
|
||||
// Required for onPointerLeave to trigger when a touch pointer leaves
|
||||
// https://stackoverflow.com/a/70976017/1137077
|
||||
e.target.releasePointerCapture(e.pointerId);
|
||||
}}
|
||||
>
|
||||
<div className={hoveredField === i ? "text-zinc-500" : ""}>
|
||||
<button
|
||||
@ -185,9 +198,9 @@ export default function SimpleCanvas({ diagram, zoom }) {
|
||||
return (
|
||||
<svg
|
||||
className="w-full h-full cursor-grab"
|
||||
onMouseUp={releaseTable}
|
||||
onMouseMove={moveTable}
|
||||
onMouseLeave={releaseTable}
|
||||
onPointerUp={(e) => e.isPrimary && releaseTable()}
|
||||
onPointerMove={(e) => e.isPrimary && moveTable()}
|
||||
onPointerLeave={(e) => e.isPrimary && releaseTable()}
|
||||
>
|
||||
<defs>
|
||||
<pattern
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import ControlPanel from "./EditorHeader/ControlPanel";
|
||||
import Canvas from "./EditorCanvas/Canvas";
|
||||
import { CanvasContextProvider } from "../context/CanvasContext";
|
||||
import SidePanel from "./EditorSidePanel/SidePanel";
|
||||
import { DB, State } from "../data/constants";
|
||||
import { db } from "../data/db";
|
||||
@ -338,7 +339,7 @@ export default function WorkSpace() {
|
||||
}, [load]);
|
||||
|
||||
return (
|
||||
<div className="h-[100vh] flex flex-col overflow-hidden theme">
|
||||
<div className="h-full flex flex-col overflow-hidden theme">
|
||||
<ControlPanel
|
||||
diagramId={id}
|
||||
setDiagramId={setId}
|
||||
@ -349,15 +350,22 @@ export default function WorkSpace() {
|
||||
/>
|
||||
<div
|
||||
className="flex h-full overflow-y-auto"
|
||||
onMouseUp={() => setResize(false)}
|
||||
onMouseLeave={() => setResize(false)}
|
||||
onMouseMove={handleResize}
|
||||
onPointerUp={(e) => e.isPrimary && setResize(false)}
|
||||
onPointerLeave={(e) => e.isPrimary && setResize(false)}
|
||||
onPointerMove={(e) => e.isPrimary && handleResize(e)}
|
||||
onPointerDown={(e) => {
|
||||
// Required for onPointerLeave to trigger when a touch pointer leaves
|
||||
// https://stackoverflow.com/a/70976017/1137077
|
||||
e.target.releasePointerCapture(e.pointerId);
|
||||
}}
|
||||
>
|
||||
{layout.sidebar && (
|
||||
<SidePanel resize={resize} setResize={setResize} width={width} />
|
||||
)}
|
||||
<div className="relative w-full h-full overflow-hidden">
|
||||
<Canvas saveState={saveState} setSaveState={setSaveState} />
|
||||
<CanvasContextProvider className="h-full w-full">
|
||||
<Canvas saveState={saveState} setSaveState={setSaveState} />
|
||||
</CanvasContextProvider>
|
||||
{!(layout.sidebar || layout.toolbar || layout.header) && (
|
||||
<div className="fixed right-5 bottom-4">
|
||||
<FloatingControls />
|
||||
|
@ -23,15 +23,17 @@ export default function AreasContextProvider({ children }) {
|
||||
return temp.map((t, i) => ({ ...t, id: i }));
|
||||
});
|
||||
} else {
|
||||
const width = 200;
|
||||
const height = 200;
|
||||
setAreas((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: prev.length,
|
||||
name: `area_${prev.length}`,
|
||||
x: -transform.pan.x,
|
||||
y: -transform.pan.y,
|
||||
width: 200,
|
||||
height: 200,
|
||||
x: transform.pan.x - width / 2,
|
||||
y: transform.pan.y - height / 2,
|
||||
width,
|
||||
height,
|
||||
color: defaultBlue,
|
||||
},
|
||||
]);
|
||||
|
167
src/context/CanvasContext.jsx
Normal file
167
src/context/CanvasContext.jsx
Normal file
@ -0,0 +1,167 @@
|
||||
import { useTransform } from "../hooks";
|
||||
import { createContext, useCallback, useMemo, useRef, useState } from "react";
|
||||
import { useEventListener, useResizeObserver } from "usehooks-ts";
|
||||
|
||||
export const CanvasContext = createContext({
|
||||
canvas: {
|
||||
screenSize: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
viewBox: new DOMRect(),
|
||||
},
|
||||
coords: {
|
||||
toDiagramSpace(coords) {
|
||||
return coords;
|
||||
},
|
||||
toScreenSpace(coords) {
|
||||
return coords;
|
||||
},
|
||||
},
|
||||
pointer: {
|
||||
spaces: {
|
||||
screen: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
diagram: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
},
|
||||
style: "default",
|
||||
setStyle() {},
|
||||
},
|
||||
});
|
||||
|
||||
export function CanvasContextProvider({ children, ...attrs }) {
|
||||
const canvasWrapRef = useRef(null);
|
||||
const { transform } = useTransform();
|
||||
const canvasSize = useResizeObserver({
|
||||
ref: canvasWrapRef,
|
||||
box: "content-box",
|
||||
});
|
||||
const screenSize = useMemo(
|
||||
() => ({
|
||||
x: canvasSize.width ?? 0,
|
||||
y: canvasSize.height ?? 0,
|
||||
}),
|
||||
[canvasSize.height, canvasSize.width],
|
||||
);
|
||||
const viewBoxSize = useMemo(
|
||||
() => ({
|
||||
x: screenSize.x / transform.zoom,
|
||||
y: screenSize.y / transform.zoom,
|
||||
}),
|
||||
[screenSize.x, screenSize.y, transform.zoom],
|
||||
);
|
||||
const viewBox = useMemo(
|
||||
() =>
|
||||
new DOMRect(
|
||||
transform.pan.x - viewBoxSize.x / 2,
|
||||
transform.pan.y - viewBoxSize.y / 2,
|
||||
viewBoxSize.x,
|
||||
viewBoxSize.y,
|
||||
),
|
||||
[transform.pan.x, transform.pan.y, viewBoxSize.x, viewBoxSize.y],
|
||||
);
|
||||
|
||||
const toDiagramSpace = useCallback(
|
||||
(coord) => ({
|
||||
x:
|
||||
typeof coord.x === "number"
|
||||
? (coord.x / screenSize.x) * viewBox.width + viewBox.left
|
||||
: undefined,
|
||||
y:
|
||||
typeof coord.y === "number"
|
||||
? (coord.y / screenSize.y) * viewBox.height + viewBox.top
|
||||
: undefined,
|
||||
}),
|
||||
[
|
||||
screenSize.x,
|
||||
screenSize.y,
|
||||
viewBox.height,
|
||||
viewBox.left,
|
||||
viewBox.top,
|
||||
viewBox.width,
|
||||
],
|
||||
);
|
||||
|
||||
const toScreenSpace = useCallback(
|
||||
(coord) => ({
|
||||
x:
|
||||
typeof coord.x === "number"
|
||||
? ((coord.x - viewBox.left) / viewBox.width) * screenSize.x
|
||||
: undefined,
|
||||
y:
|
||||
typeof coord.y === "number"
|
||||
? ((coord.y - viewBox.top) / viewBox.height) * screenSize.y
|
||||
: undefined,
|
||||
}),
|
||||
[
|
||||
screenSize.x,
|
||||
screenSize.y,
|
||||
viewBox.height,
|
||||
viewBox.left,
|
||||
viewBox.top,
|
||||
viewBox.width,
|
||||
],
|
||||
);
|
||||
|
||||
const [pointerScreenCoords, setPointerScreenCoords] = useState({
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
const pointerDiagramCoords = useMemo(
|
||||
() => toDiagramSpace(pointerScreenCoords),
|
||||
[pointerScreenCoords, toDiagramSpace],
|
||||
);
|
||||
const [pointerStyle, setPointerStyle] = useState("default");
|
||||
|
||||
/**
|
||||
* @param {PointerEvent} e
|
||||
*/
|
||||
function detectPointerMovement(e) {
|
||||
const targetElm = /** @type {HTMLElement | null} */ (e.currentTarget);
|
||||
if (!e.isPrimary || !targetElm) return;
|
||||
|
||||
const canvasBounds = targetElm.getBoundingClientRect();
|
||||
|
||||
setPointerScreenCoords({
|
||||
x: e.clientX - canvasBounds.left,
|
||||
y: e.clientY - canvasBounds.top,
|
||||
});
|
||||
}
|
||||
|
||||
// Important for touch screen devices!
|
||||
useEventListener("pointerdown", detectPointerMovement, canvasWrapRef);
|
||||
|
||||
useEventListener("pointermove", detectPointerMovement, canvasWrapRef);
|
||||
|
||||
const contextValue = {
|
||||
canvas: {
|
||||
screenSize,
|
||||
viewBox,
|
||||
},
|
||||
coords: {
|
||||
toDiagramSpace,
|
||||
toScreenSpace,
|
||||
},
|
||||
pointer: {
|
||||
spaces: {
|
||||
screen: pointerScreenCoords,
|
||||
diagram: pointerDiagramCoords,
|
||||
},
|
||||
style: pointerStyle,
|
||||
setStyle: setPointerStyle,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<CanvasContext.Provider value={contextValue}>
|
||||
<div {...attrs} ref={canvasWrapRef}>
|
||||
{children}
|
||||
</div>
|
||||
</CanvasContext.Provider>
|
||||
)
|
||||
}
|
@ -30,8 +30,8 @@ export default function DiagramContextProvider({ children }) {
|
||||
{
|
||||
id: prev.length,
|
||||
name: `table_${prev.length}`,
|
||||
x: -transform.pan.x,
|
||||
y: -transform.pan.y,
|
||||
x: transform.pan.x,
|
||||
y: transform.pan.y,
|
||||
fields: [
|
||||
{
|
||||
name: "id",
|
||||
|
@ -23,16 +23,17 @@ export default function NotesContextProvider({ children }) {
|
||||
return temp.map((t, i) => ({ ...t, id: i }));
|
||||
});
|
||||
} else {
|
||||
const height = 88;
|
||||
setNotes((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: prev.length,
|
||||
x: -transform.pan.x,
|
||||
y: -transform.pan.y,
|
||||
x: transform.pan.x,
|
||||
y: transform.pan.y - height / 2,
|
||||
title: `note_${prev.length}`,
|
||||
content: "",
|
||||
color: defaultNoteTheme,
|
||||
height: 88,
|
||||
height,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
@ -1,7 +1,17 @@
|
||||
import { createContext, useState } from "react";
|
||||
import { tableWidth } from "../data/constants";
|
||||
|
||||
export const SettingsContext = createContext(null);
|
||||
export const SettingsContext = createContext({
|
||||
strictMode: false,
|
||||
showFieldSummary: true,
|
||||
showGrid: true,
|
||||
mode: "light",
|
||||
autosave: true,
|
||||
panning: true,
|
||||
showCardinality: true,
|
||||
tableWidth: tableWidth,
|
||||
showDebugCoordinates: false,
|
||||
});
|
||||
|
||||
export default function SettingsContextProvider({ children }) {
|
||||
const [settings, setSettings] = useState({
|
||||
@ -13,6 +23,7 @@ export default function SettingsContextProvider({ children }) {
|
||||
panning: true,
|
||||
showCardinality: true,
|
||||
tableWidth: tableWidth,
|
||||
showDebugCoordinates: false,
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -1,13 +1,43 @@
|
||||
import { createContext, useState } from "react";
|
||||
import { createContext, useCallback, useState } from "react";
|
||||
|
||||
export const TransformContext = createContext(null);
|
||||
|
||||
export default function TransformContextProvider({ children }) {
|
||||
const [transform, setTransform] = useState({
|
||||
const [transform, setTransformInternal] = useState({
|
||||
zoom: 1,
|
||||
pan: { x: 0, y: 0 },
|
||||
});
|
||||
|
||||
/**
|
||||
* @type {typeof DrawDB.TransformContext["setTransform"]}
|
||||
*/
|
||||
const setTransform = useCallback(
|
||||
(actionOrValue) => {
|
||||
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
||||
const findFirstNumber = (...values) =>
|
||||
values.find((value) => typeof value === "number" && !isNaN(value));
|
||||
|
||||
setTransformInternal((prev) => {
|
||||
if (typeof actionOrValue === "function") {
|
||||
actionOrValue = actionOrValue(prev);
|
||||
}
|
||||
|
||||
return {
|
||||
zoom: clamp(
|
||||
findFirstNumber(actionOrValue.zoom, prev.zoom, 1),
|
||||
0.02,
|
||||
5,
|
||||
),
|
||||
pan: {
|
||||
x: findFirstNumber(actionOrValue.pan?.x, prev.pan?.x, 0),
|
||||
y: findFirstNumber(actionOrValue.pan?.y, prev.pan?.y, 0),
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
[setTransformInternal],
|
||||
);
|
||||
|
||||
return (
|
||||
<TransformContext.Provider value={{ transform, setTransform }}>
|
||||
{children}
|
||||
|
@ -1,6 +1,11 @@
|
||||
import { createContext, useState } from "react";
|
||||
|
||||
export const UndoRedoContext = createContext(null);
|
||||
export const UndoRedoContext = createContext({
|
||||
undoStack: [],
|
||||
setUndoStack: () => {},
|
||||
redoStack: [],
|
||||
setRedoStack: () => {},
|
||||
});
|
||||
|
||||
export default function UndoRedoContextProvider({ children }) {
|
||||
const [undoStack, setUndoStack] = useState([]);
|
||||
|
@ -42,7 +42,9 @@ export const shortcuts = [
|
||||
title: "Reset view",
|
||||
description: "Resetting view will set diagram pan to (0, 0).",
|
||||
},
|
||||
{ shortcut: "CTRL+UP / Wheel up", title: "Zoom in" },
|
||||
{ shortcut: "CTRL+DOWN / Wheel down", title: "Zoom out" },
|
||||
{ shortcut: "CTRL+UP / CTRL+Wheel up", title: "Zoom in" },
|
||||
{ shortcut: "CTRL+DOWN / CTRL+Wheel down", title: "Zoom out" },
|
||||
{ shortcut: "Wheel up / Wheel down", title: "Pan Y" },
|
||||
{ shortcut: "SHIFT+Wheel up / SHIFT+Wheel down", title: "Pan X" },
|
||||
{ shortcut: "CTRL+H", title: "Open shortcuts" },
|
||||
];
|
||||
|
@ -1,4 +1,5 @@
|
||||
export { default as useAreas } from "./useAreas";
|
||||
export { default as useCanvas } from "./useCanvas";
|
||||
export { default as useLayout } from "./useLayout";
|
||||
export { default as useNotes } from "./useNotes";
|
||||
export { default as useSaveState } from "./useSaveState";
|
||||
|
6
src/hooks/useCanvas.js
Normal file
6
src/hooks/useCanvas.js
Normal file
@ -0,0 +1,6 @@
|
||||
import { useContext } from "react";
|
||||
import { CanvasContext } from "../context/CanvasContext";
|
||||
|
||||
export default function useCanvas() {
|
||||
return useContext(CanvasContext);
|
||||
}
|
@ -59,6 +59,13 @@ const en = {
|
||||
show_timeline: "Show timeline",
|
||||
autosave: "Autosave",
|
||||
panning: "Panning",
|
||||
show_debug_coordinates: "Show debug coordinates",
|
||||
transform: "Transform",
|
||||
viewbox: "View Box",
|
||||
cursor_coordinates: "Cursor Coordinates",
|
||||
coordinate_space: "Space",
|
||||
coordinate_space_screen: "Screen",
|
||||
coordinate_space_diagram: "Diagram",
|
||||
table_width: "Table width",
|
||||
language: "Language",
|
||||
flush_storage: "Flush storage",
|
||||
|
@ -2,6 +2,22 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
/*
|
||||
* Workaround to allow proper bottom positioning for fixed
|
||||
* elements in mobile browsers with collapsing url bars:
|
||||
* https://stackoverflow.com/a/17555766/1137077
|
||||
*/
|
||||
:root,
|
||||
html,
|
||||
body {
|
||||
@apply h-full min-h-full;
|
||||
}
|
||||
#root {
|
||||
@apply h-full min-h-full;
|
||||
}
|
||||
}
|
||||
|
||||
.semi-form-vertical .semi-form-field {
|
||||
margin: 0;
|
||||
padding-top: 8px !important;
|
||||
|
Loading…
Reference in New Issue
Block a user