fix: rewrite coordinate management
After some initial smaller fixes, it turned out that I had broken the red line used when linking fields. Fixing this was not trivial as I found myself battling a lot of small bugs relating to scale and translation in the existing code. This was made extra difficult as a lot of coordinates were calculated when necessary in Canvas.jsx. This commit attempts to simplify the coordinate management in a few different ways: * There are now two distinct coordinate systems in use, typically referred to as "spaces". Screen space and diagram space. * Diagram space is no longer measured in pixels (though the dimension-less measure used instead still maps to pixels at 100% zoom). * The canvas now exposes helper methods for transforming between spaces. * Zoom and translation is now managed via the svg viewBox property. * This makes moving items in diagram space much easier as the coordinates remain constant regardless of zoom level. * The canvas now wraps the current mouse position in a context object, making mouse movement much easier to work with. * The transform.pan property now refers to the center of the screen. A new feature in this commit is that scroll wheel zoom is now based on the current cursor location, making the diagram more convenient to move around in. I have tried to focus on Canvas.jsx and avoid changes that might be desctructive on existing save files. I also believe more refactors and abstractions could be introduced based on these changes to make the diagram even easier to work with. However, I deem that out of scope for now.
This commit is contained in:
parent
32c82168fe
commit
e4e22dee20
23
package-lock.json
generated
23
package-lock.json
generated
@ -17,6 +17,7 @@
|
|||||||
"@uiw/react-codemirror": "^4.21.25",
|
"@uiw/react-codemirror": "^4.21.25",
|
||||||
"@vercel/analytics": "^1.2.2",
|
"@vercel/analytics": "^1.2.2",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
|
"classnames": "^2.5.1",
|
||||||
"dexie": "^3.2.4",
|
"dexie": "^3.2.4",
|
||||||
"dexie-react-hooks": "^1.1.7",
|
"dexie-react-hooks": "^1.1.7",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
@ -33,7 +34,8 @@
|
|||||||
"react-hotkeys-hook": "^4.4.1",
|
"react-hotkeys-hook": "^4.4.1",
|
||||||
"react-i18next": "^14.1.1",
|
"react-i18next": "^14.1.1",
|
||||||
"react-router-dom": "^6.21.0",
|
"react-router-dom": "^6.21.0",
|
||||||
"url": "^0.11.1"
|
"url": "^0.11.1",
|
||||||
|
"usehooks-ts": "^3.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.43",
|
"@types/react": "^18.2.43",
|
||||||
@ -4283,6 +4285,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
"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": {
|
"node_modules/lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
||||||
"integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ=="
|
"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": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
"@uiw/react-codemirror": "^4.21.25",
|
"@uiw/react-codemirror": "^4.21.25",
|
||||||
"@vercel/analytics": "^1.2.2",
|
"@vercel/analytics": "^1.2.2",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
|
"classnames": "^2.5.1",
|
||||||
"dexie": "^3.2.4",
|
"dexie": "^3.2.4",
|
||||||
"dexie-react-hooks": "^1.1.7",
|
"dexie-react-hooks": "^1.1.7",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
@ -35,7 +36,8 @@
|
|||||||
"react-hotkeys-hook": "^4.4.1",
|
"react-hotkeys-hook": "^4.4.1",
|
||||||
"react-i18next": "^14.1.1",
|
"react-i18next": "^14.1.1",
|
||||||
"react-router-dom": "^6.21.0",
|
"react-router-dom": "^6.21.0",
|
||||||
"url": "^0.11.1"
|
"url": "^0.11.1",
|
||||||
|
"usehooks-ts": "^3.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.43",
|
"@types/react": "^18.2.43",
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useContext, useRef, useState } from "react";
|
||||||
import { Button, Popover, Input } from "@douyinfe/semi-ui";
|
import { Button, Popover, Input } from "@douyinfe/semi-ui";
|
||||||
import { IconEdit, IconDeleteStroked } from "@douyinfe/semi-icons";
|
import { IconEdit, IconDeleteStroked } from "@douyinfe/semi-icons";
|
||||||
import {
|
import {
|
||||||
@ -15,16 +15,27 @@ import {
|
|||||||
useSelect,
|
useSelect,
|
||||||
useAreas,
|
useAreas,
|
||||||
useSaveState,
|
useSaveState,
|
||||||
useTransform,
|
|
||||||
} from "../../hooks";
|
} from "../../hooks";
|
||||||
import ColorPalette from "../ColorPicker";
|
import ColorPalette from "../ColorPicker";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useHover } from "usehooks-ts";
|
||||||
|
import { CanvasContext } from "../../context/CanvasContext";
|
||||||
|
|
||||||
export default function Area({ data, onPointerDown, setResize, setInitCoords }) {
|
export default function Area({
|
||||||
const [hovered, setHovered] = useState(false);
|
data,
|
||||||
|
onPointerDown,
|
||||||
|
setResize,
|
||||||
|
setInitCoords,
|
||||||
|
}) {
|
||||||
|
const ref = useRef(null);
|
||||||
|
const isHovered = useHover(ref);
|
||||||
|
const {
|
||||||
|
pointer: {
|
||||||
|
spaces: { diagram: pointer },
|
||||||
|
},
|
||||||
|
} = useContext(CanvasContext);
|
||||||
const { layout } = useLayout();
|
const { layout } = useLayout();
|
||||||
const { settings } = useSettings();
|
const { settings } = useSettings();
|
||||||
const { transform } = useTransform();
|
|
||||||
const { setSaveState } = useSaveState();
|
const { setSaveState } = useSaveState();
|
||||||
const { selectedElement, setSelectedElement } = useSelect();
|
const { selectedElement, setSelectedElement } = useSelect();
|
||||||
|
|
||||||
@ -35,8 +46,8 @@ export default function Area({ data, onPointerDown, setResize, setInitCoords })
|
|||||||
y: data.y,
|
y: data.y,
|
||||||
width: data.width,
|
width: data.width,
|
||||||
height: data.height,
|
height: data.height,
|
||||||
pointerX: e.clientX / transform.zoom,
|
pointerX: pointer.x,
|
||||||
pointerY: e.clientY / transform.zoom,
|
pointerY: pointer.y,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -84,10 +95,7 @@ export default function Area({ data, onPointerDown, setResize, setInitCoords })
|
|||||||
selectedElement.open;
|
selectedElement.open;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g
|
<g ref={ref}>
|
||||||
onPointerEnter={(e) => e.isPrimary && setHovered(true)}
|
|
||||||
onPointerLeave={(e) => e.isPrimary &&setHovered(false)}
|
|
||||||
>
|
|
||||||
<foreignObject
|
<foreignObject
|
||||||
key={data.id}
|
key={data.id}
|
||||||
x={data.x}
|
x={data.x}
|
||||||
@ -98,7 +106,7 @@ export default function Area({ data, onPointerDown, setResize, setInitCoords })
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`border-2 ${
|
className={`border-2 ${
|
||||||
hovered
|
isHovered
|
||||||
? "border-dashed border-blue-500"
|
? "border-dashed border-blue-500"
|
||||||
: selectedElement.element === ObjectType.AREA &&
|
: selectedElement.element === ObjectType.AREA &&
|
||||||
selectedElement.id === data.id
|
selectedElement.id === data.id
|
||||||
@ -114,7 +122,7 @@ export default function Area({ data, onPointerDown, setResize, setInitCoords })
|
|||||||
<div className="text-color select-none overflow-hidden text-ellipsis">
|
<div className="text-color select-none overflow-hidden text-ellipsis">
|
||||||
{data.name}
|
{data.name}
|
||||||
</div>
|
</div>
|
||||||
{(hovered || (areaIsSelected() && !layout.sidebar)) && (
|
{(isHovered || (areaIsSelected() && !layout.sidebar)) && (
|
||||||
<Popover
|
<Popover
|
||||||
visible={areaIsSelected() && !layout.sidebar}
|
visible={areaIsSelected() && !layout.sidebar}
|
||||||
onClickOutSide={onClickOutSide}
|
onClickOutSide={onClickOutSide}
|
||||||
@ -139,7 +147,7 @@ export default function Area({ data, onPointerDown, setResize, setInitCoords })
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</foreignObject>
|
</foreignObject>
|
||||||
{hovered && (
|
{isHovered && (
|
||||||
<>
|
<>
|
||||||
<circle
|
<circle
|
||||||
cx={data.x}
|
cx={data.x}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useRef, useState, useEffect } from "react";
|
import { useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Action,
|
Action,
|
||||||
Cardinality,
|
Cardinality,
|
||||||
@ -22,9 +22,19 @@ import {
|
|||||||
} from "../../hooks";
|
} from "../../hooks";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { diagram } from "../../data/heroDiagram";
|
import { diagram } from "../../data/heroDiagram";
|
||||||
|
import { CanvasContext, useCanvasContextProviderValue } from "../../context/CanvasContext";
|
||||||
|
import { useEventListener } from "usehooks-ts";
|
||||||
|
|
||||||
export default function Canvas() {
|
export default function Canvas() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const canvasRef = useRef(null);
|
||||||
|
const canvasContextValue = useCanvasContextProviderValue(canvasRef);
|
||||||
|
const {
|
||||||
|
canvas: { viewBox },
|
||||||
|
pointer,
|
||||||
|
} = canvasContextValue;
|
||||||
|
|
||||||
const { tables, updateTable, relationships, addRelationship } = useDiagram();
|
const { tables, updateTable, relationships, addRelationship } = useDiagram();
|
||||||
const { areas, updateArea } = useAreas();
|
const { areas, updateArea } = useAreas();
|
||||||
const { notes, updateNote } = useNotes();
|
const { notes, updateNote } = useNotes();
|
||||||
@ -50,17 +60,15 @@ export default function Canvas() {
|
|||||||
endX: 0,
|
endX: 0,
|
||||||
endY: 0,
|
endY: 0,
|
||||||
});
|
});
|
||||||
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
const [grabOffset, setGrabOffset] = useState({ x: 0, y: 0 });
|
||||||
const [hoveredTable, setHoveredTable] = useState({
|
const [hoveredTable, setHoveredTable] = useState({
|
||||||
tableId: -1,
|
tableId: -1,
|
||||||
field: -2,
|
field: -2,
|
||||||
});
|
});
|
||||||
const [panning, setPanning] = useState({
|
const [panning, setPanning] = useState({
|
||||||
isPanning: false,
|
isPanning: false,
|
||||||
x: 0,
|
panStart: { x: 0, y: 0 },
|
||||||
y: 0,
|
cursorStart: { x: 0, y: 0 },
|
||||||
dx: 0,
|
|
||||||
dy: 0,
|
|
||||||
});
|
});
|
||||||
const [areaResize, setAreaResize] = useState({ id: -1, dir: "none" });
|
const [areaResize, setAreaResize] = useState({ id: -1, dir: "none" });
|
||||||
const [initCoords, setInitCoords] = useState({
|
const [initCoords, setInitCoords] = useState({
|
||||||
@ -71,9 +79,6 @@ export default function Canvas() {
|
|||||||
pointerX: 0,
|
pointerX: 0,
|
||||||
pointerY: 0,
|
pointerY: 0,
|
||||||
});
|
});
|
||||||
const [cursor, setCursor] = useState("default");
|
|
||||||
|
|
||||||
const canvas = useRef(null);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {PointerEvent} e
|
* @param {PointerEvent} e
|
||||||
@ -83,12 +88,11 @@ export default function Canvas() {
|
|||||||
const handlePointerDownOnElement = (e, id, type) => {
|
const handlePointerDownOnElement = (e, id, type) => {
|
||||||
if (!e.isPrimary) return;
|
if (!e.isPrimary) return;
|
||||||
|
|
||||||
const { clientX, clientY } = e;
|
|
||||||
if (type === ObjectType.TABLE) {
|
if (type === ObjectType.TABLE) {
|
||||||
const table = tables.find((t) => t.id === id);
|
const table = tables.find((t) => t.id === id);
|
||||||
setOffset({
|
setGrabOffset({
|
||||||
x: clientX / transform.zoom - table.x,
|
x: table.x - pointer.spaces.diagram.x,
|
||||||
y: clientY / transform.zoom - table.y,
|
y: table.y - pointer.spaces.diagram.y,
|
||||||
});
|
});
|
||||||
setDragging({
|
setDragging({
|
||||||
element: type,
|
element: type,
|
||||||
@ -98,9 +102,9 @@ export default function Canvas() {
|
|||||||
});
|
});
|
||||||
} else if (type === ObjectType.AREA) {
|
} else if (type === ObjectType.AREA) {
|
||||||
const area = areas.find((t) => t.id === id);
|
const area = areas.find((t) => t.id === id);
|
||||||
setOffset({
|
setGrabOffset({
|
||||||
x: clientX / transform.zoom - area.x,
|
x: area.x - pointer.spaces.diagram.x,
|
||||||
y: clientY / transform.zoom - area.y,
|
y: area.y - pointer.spaces.diagram.y,
|
||||||
});
|
});
|
||||||
setDragging({
|
setDragging({
|
||||||
element: type,
|
element: type,
|
||||||
@ -110,9 +114,9 @@ export default function Canvas() {
|
|||||||
});
|
});
|
||||||
} else if (type === ObjectType.NOTE) {
|
} else if (type === ObjectType.NOTE) {
|
||||||
const note = notes.find((t) => t.id === id);
|
const note = notes.find((t) => t.id === id);
|
||||||
setOffset({
|
setGrabOffset({
|
||||||
x: clientX / transform.zoom - note.x,
|
x: note.x - pointer.spaces.diagram.x,
|
||||||
y: clientY / transform.zoom - note.y,
|
y: note.y - pointer.spaces.diagram.y,
|
||||||
});
|
});
|
||||||
setDragging({
|
setDragging({
|
||||||
element: type,
|
element: type,
|
||||||
@ -136,11 +140,10 @@ export default function Canvas() {
|
|||||||
if (!e.isPrimary) return;
|
if (!e.isPrimary) return;
|
||||||
|
|
||||||
if (linking) {
|
if (linking) {
|
||||||
const rect = canvas.current.getBoundingClientRect();
|
|
||||||
setLinkingLine({
|
setLinkingLine({
|
||||||
...linkingLine,
|
...linkingLine,
|
||||||
endX: (e.clientX - rect.left - transform.pan.x) / transform.zoom,
|
endX: pointer.spaces.diagram.x,
|
||||||
endY: (e.clientY - rect.top - transform.pan.y) / transform.zoom,
|
endY: pointer.spaces.diagram.y,
|
||||||
});
|
});
|
||||||
} else if (
|
} else if (
|
||||||
panning.isPanning &&
|
panning.isPanning &&
|
||||||
@ -150,53 +153,68 @@ export default function Canvas() {
|
|||||||
if (!settings.panning) {
|
if (!settings.panning) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const dx = e.clientX - panning.dx;
|
|
||||||
const dy = e.clientY - panning.dy;
|
|
||||||
setTransform((prev) => ({
|
setTransform((prev) => ({
|
||||||
...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) {
|
} else if (dragging.element === ObjectType.TABLE && dragging.id >= 0) {
|
||||||
const dx = e.clientX / transform.zoom - offset.x;
|
updateTable(dragging.id, {
|
||||||
const dy = e.clientY / transform.zoom - offset.y;
|
x: pointer.spaces.diagram.x + grabOffset.x,
|
||||||
updateTable(dragging.id, { x: dx, y: dy });
|
y: pointer.spaces.diagram.y + grabOffset.y,
|
||||||
|
});
|
||||||
} else if (
|
} else if (
|
||||||
dragging.element === ObjectType.AREA &&
|
dragging.element === ObjectType.AREA &&
|
||||||
dragging.id >= 0 &&
|
dragging.id >= 0 &&
|
||||||
areaResize.id === -1
|
areaResize.id === -1
|
||||||
) {
|
) {
|
||||||
const dx = e.clientX / transform.zoom - offset.x;
|
updateArea(dragging.id, {
|
||||||
const dy = e.clientY / transform.zoom - offset.y;
|
x: pointer.spaces.diagram.x + grabOffset.x,
|
||||||
updateArea(dragging.id, { x: dx, y: dy });
|
y: pointer.spaces.diagram.y + grabOffset.y,
|
||||||
|
});
|
||||||
} else if (dragging.element === ObjectType.NOTE && dragging.id >= 0) {
|
} else if (dragging.element === ObjectType.NOTE && dragging.id >= 0) {
|
||||||
const dx = e.clientX / transform.zoom - offset.x;
|
updateNote(dragging.id, {
|
||||||
const dy = e.clientY / transform.zoom - offset.y;
|
x: pointer.spaces.diagram.x + grabOffset.x,
|
||||||
updateNote(dragging.id, { x: dx, y: dy });
|
y: pointer.spaces.diagram.y + grabOffset.y,
|
||||||
|
});
|
||||||
} else if (areaResize.id !== -1) {
|
} else if (areaResize.id !== -1) {
|
||||||
if (areaResize.dir === "none") return;
|
if (areaResize.dir === "none") return;
|
||||||
let newDims = { ...initCoords };
|
let newDims = { ...initCoords };
|
||||||
delete newDims.pointerX;
|
delete newDims.pointerX;
|
||||||
delete newDims.pointerY;
|
delete newDims.pointerY;
|
||||||
const pointerX = e.clientX / transform.zoom;
|
setPanning((old) => ({ ...old, isPanning: false }));
|
||||||
const pointerY = e.clientY / transform.zoom;
|
|
||||||
setPanning({ isPanning: false, x: 0, y: 0 });
|
switch (areaResize.dir) {
|
||||||
if (areaResize.dir === "br") {
|
case "br":
|
||||||
newDims.width = initCoords.width + (pointerX - initCoords.pointerX);
|
newDims.width = pointer.spaces.diagram.x - initCoords.x;
|
||||||
newDims.height = initCoords.height + (pointerY - initCoords.pointerY);
|
newDims.height = pointer.spaces.diagram.y - initCoords.y;
|
||||||
} else if (areaResize.dir === "tl") {
|
break;
|
||||||
newDims.x = initCoords.x + (pointerX - initCoords.pointerX);
|
case "tl":
|
||||||
newDims.y = initCoords.y + (pointerY - initCoords.pointerY);
|
newDims.x = pointer.spaces.diagram.x;
|
||||||
newDims.width = initCoords.width - (pointerX - initCoords.pointerX);
|
newDims.y = pointer.spaces.diagram.y;
|
||||||
newDims.height = initCoords.height - (pointerY - initCoords.pointerY);
|
newDims.width =
|
||||||
} else if (areaResize.dir === "tr") {
|
initCoords.x + initCoords.width - pointer.spaces.diagram.x;
|
||||||
newDims.y = initCoords.y + (pointerY - initCoords.pointerY);
|
newDims.height =
|
||||||
newDims.width = initCoords.width + (pointerX - initCoords.pointerX);
|
initCoords.y + initCoords.height - pointer.spaces.diagram.y;
|
||||||
newDims.height = initCoords.height - (pointerY - initCoords.pointerY);
|
break;
|
||||||
} else if (areaResize.dir === "bl") {
|
case "tr":
|
||||||
newDims.x = initCoords.x + (pointerX - initCoords.pointerX);
|
newDims.y = pointer.spaces.diagram.y;
|
||||||
newDims.width = initCoords.width - (pointerX - initCoords.pointerX);
|
newDims.width = pointer.spaces.diagram.x - initCoords.x;
|
||||||
newDims.height = initCoords.height + (pointerY - initCoords.pointerY);
|
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 });
|
updateArea(areaResize.id, { ...newDims });
|
||||||
@ -219,11 +237,12 @@ export default function Canvas() {
|
|||||||
|
|
||||||
setPanning({
|
setPanning({
|
||||||
isPanning: true,
|
isPanning: true,
|
||||||
...transform.pan,
|
panStart: transform.pan,
|
||||||
dx: e.clientX,
|
// Diagram space depends on the current panning.
|
||||||
dy: e.clientY,
|
// Use screen space to avoid circular dependencies and undefined behavior.
|
||||||
|
cursorStart: pointer.spaces.screen,
|
||||||
});
|
});
|
||||||
setCursor("grabbing");
|
pointer.setStyle("grabbing");
|
||||||
};
|
};
|
||||||
|
|
||||||
const coordsDidUpdate = (element) => {
|
const coordsDidUpdate = (element) => {
|
||||||
@ -333,8 +352,8 @@ export default function Canvas() {
|
|||||||
open: false,
|
open: false,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
setPanning({ isPanning: false, x: 0, y: 0 });
|
setPanning((old) => ({ ...old, isPanning: false }));
|
||||||
setCursor("default");
|
pointer.setStyle("default");
|
||||||
if (linking) handleLinking();
|
if (linking) handleLinking();
|
||||||
setLinking(false);
|
setLinking(false);
|
||||||
if (areaResize.id !== -1 && didResize(areaResize.id)) {
|
if (areaResize.id !== -1 && didResize(areaResize.id)) {
|
||||||
@ -372,7 +391,7 @@ export default function Canvas() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleGripField = () => {
|
const handleGripField = () => {
|
||||||
setPanning(false);
|
setPanning((old) => ({ ...old, isPanning: false }));
|
||||||
setDragging({ element: ObjectType.NONE, id: -1, prevX: 0, prevY: 0 });
|
setDragging({ element: ObjectType.NONE, id: -1, prevX: 0, prevY: 0 });
|
||||||
setLinking(true);
|
setLinking(true);
|
||||||
};
|
};
|
||||||
@ -412,121 +431,126 @@ export default function Canvas() {
|
|||||||
addRelationship(newRelationship);
|
addRelationship(newRelationship);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseWheel = (e) => {
|
// Handle mouse wheel scrolling
|
||||||
e.preventDefault();
|
useEventListener(
|
||||||
setTransform((prev) => ({
|
"wheel",
|
||||||
...prev,
|
(e) => {
|
||||||
zoom: e.deltaY <= 0 ? prev.zoom * 1.05 : prev.zoom / 1.05,
|
e.preventDefault();
|
||||||
}));
|
// How "eager" the viewport is to
|
||||||
};
|
// center the cursor's coordinates
|
||||||
|
const eagernessFactor = 0.05;
|
||||||
useEffect(() => {
|
setTransform((prev) => ({
|
||||||
const canvasElement = canvas.current;
|
pan: {
|
||||||
canvasElement.addEventListener("wheel", handleMouseWheel, {
|
x:
|
||||||
passive: false,
|
prev.pan.x -
|
||||||
});
|
(pointer.spaces.diagram.x - prev.pan.x) *
|
||||||
return () => {
|
eagernessFactor *
|
||||||
canvasElement.removeEventListener("wheel", handleMouseWheel);
|
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,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
canvasRef,
|
||||||
|
{ passive: false },
|
||||||
|
);
|
||||||
|
|
||||||
const theme = localStorage.getItem("theme");
|
const theme = localStorage.getItem("theme");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<CanvasContext.Provider value={canvasContextValue}>
|
||||||
<div className="flex-grow h-full touch-none" id="canvas">
|
<div className="flex-grow h-full touch-none" id="canvas">
|
||||||
<div ref={canvas} className="w-full h-full">
|
<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="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
|
<svg
|
||||||
|
ref={canvasRef}
|
||||||
onPointerMove={handlePointerMove}
|
onPointerMove={handlePointerMove}
|
||||||
onPointerDown={handlePointerDown}
|
onPointerDown={handlePointerDown}
|
||||||
onPointerUp={handlePointerUp}
|
onPointerUp={handlePointerUp}
|
||||||
className="w-full h-full"
|
className="absolute w-full h-full touch-none"
|
||||||
style={{
|
viewBox={`${viewBox.left} ${viewBox.top} ${viewBox.width} ${viewBox.height}`}
|
||||||
cursor: cursor,
|
|
||||||
backgroundColor:
|
|
||||||
theme === "dark" ? "rgba(22, 22, 26, 1)" : "white",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{settings.showGrid && (
|
{areas.map((a) => (
|
||||||
<>
|
<Area
|
||||||
<defs>
|
key={a.id}
|
||||||
<pattern
|
data={a}
|
||||||
id="pattern-circles"
|
onPointerDown={(e) =>
|
||||||
x="0"
|
handlePointerDownOnElement(e, a.id, ObjectType.AREA)
|
||||||
y="0"
|
}
|
||||||
width="24"
|
setResize={setAreaResize}
|
||||||
height="24"
|
setInitCoords={setInitCoords}
|
||||||
patternUnits="userSpaceOnUse"
|
/>
|
||||||
patternContentUnits="userSpaceOnUse"
|
))}
|
||||||
>
|
{relationships.map((e, i) => (
|
||||||
<circle
|
<Relationship key={i} data={e} />
|
||||||
id="pattern-circle"
|
))}
|
||||||
cx="4"
|
{tables.map((table) => (
|
||||||
cy="4"
|
<Table
|
||||||
r="0.85"
|
key={table.id}
|
||||||
fill="rgb(99, 152, 191)"
|
tableData={table}
|
||||||
></circle>
|
setHoveredTable={setHoveredTable}
|
||||||
</pattern>
|
handleGripField={handleGripField}
|
||||||
</defs>
|
setLinkingLine={setLinkingLine}
|
||||||
<rect
|
onPointerDown={(e) =>
|
||||||
x="0"
|
handlePointerDownOnElement(e, table.id, ObjectType.TABLE)
|
||||||
y="0"
|
}
|
||||||
width="100%"
|
/>
|
||||||
height="100%"
|
))}
|
||||||
fill="url(#pattern-circles)"
|
{linking && (
|
||||||
></rect>
|
<path
|
||||||
</>
|
d={`M ${linkingLine.startX} ${linkingLine.startY} L ${linkingLine.endX} ${linkingLine.endY}`}
|
||||||
|
stroke="red"
|
||||||
|
strokeDasharray="8,8"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<g
|
{notes.map((n) => (
|
||||||
style={{
|
<Note
|
||||||
transform: `translate(${transform.pan.x}px, ${transform.pan.y}px) scale(${transform.zoom})`,
|
key={n.id}
|
||||||
transformOrigin: "top left",
|
data={n}
|
||||||
}}
|
onPointerDown={(e) =>
|
||||||
id="diagram"
|
handlePointerDownOnElement(e, n.id, ObjectType.NOTE)
|
||||||
>
|
}
|
||||||
{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"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{notes.map((n) => (
|
|
||||||
<Note
|
|
||||||
key={n.id}
|
|
||||||
data={n}
|
|
||||||
onPointerDown={(e) =>
|
|
||||||
handlePointerDownOnElement(e, n.id, ObjectType.NOTE)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</g>
|
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
{settings.showDebugCoordinates && (
|
{settings.showDebugCoordinates && (
|
||||||
@ -566,10 +590,10 @@ export default function Canvas() {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>TODO</td>
|
<td>{viewBox.left.toFixed(2)}</td>
|
||||||
<td>TODO</td>
|
<td>{viewBox.top.toFixed(2)}</td>
|
||||||
<td>TODO</td>
|
<td>{viewBox.width.toFixed(2)}</td>
|
||||||
<td>TODO</td>
|
<td>{viewBox.height.toFixed(2)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@ -587,19 +611,19 @@ export default function Canvas() {
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("coordinate_space_screen")}</td>
|
<td>{t("coordinate_space_screen")}</td>
|
||||||
<td>TODO</td>
|
<td>{pointer.spaces.screen.x.toFixed(2)}</td>
|
||||||
<td>TODO</td>
|
<td>{pointer.spaces.screen.y.toFixed(2)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("coordinate_space_diagram")}</td>
|
<td>{t("coordinate_space_diagram")}</td>
|
||||||
<td>TODO</td>
|
<td>{pointer.spaces.diagram.x.toFixed(2)}</td>
|
||||||
<td>TODO</td>
|
<td>{pointer.spaces.diagram.y.toFixed(2)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</CanvasContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -23,15 +23,17 @@ export default function AreasContextProvider({ children }) {
|
|||||||
return temp.map((t, i) => ({ ...t, id: i }));
|
return temp.map((t, i) => ({ ...t, id: i }));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
const width = 200;
|
||||||
|
const height = 200;
|
||||||
setAreas((prev) => [
|
setAreas((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
id: prev.length,
|
id: prev.length,
|
||||||
name: `area_${prev.length}`,
|
name: `area_${prev.length}`,
|
||||||
x: -transform.pan.x,
|
x: transform.pan.x - width / 2,
|
||||||
y: -transform.pan.y,
|
y: transform.pan.y - height / 2,
|
||||||
width: 200,
|
width,
|
||||||
height: 200,
|
height,
|
||||||
color: defaultBlue,
|
color: defaultBlue,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
160
src/context/CanvasContext.js
Normal file
160
src/context/CanvasContext.js
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
import { useTransform } from "../hooks";
|
||||||
|
import { createContext, useCallback, useMemo, 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 useCanvasContextProviderValue(canvasRef) {
|
||||||
|
const { transform } = useTransform();
|
||||||
|
const canvasSize = useResizeObserver({
|
||||||
|
ref: canvasRef,
|
||||||
|
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, canvasRef);
|
||||||
|
|
||||||
|
useEventListener("pointermove", detectPointerMovement, canvasRef);
|
||||||
|
|
||||||
|
return {
|
||||||
|
canvas: {
|
||||||
|
screenSize,
|
||||||
|
viewBox,
|
||||||
|
},
|
||||||
|
coords: {
|
||||||
|
toDiagramSpace,
|
||||||
|
toScreenSpace,
|
||||||
|
},
|
||||||
|
pointer: {
|
||||||
|
spaces: {
|
||||||
|
screen: pointerScreenCoords,
|
||||||
|
diagram: pointerDiagramCoords,
|
||||||
|
},
|
||||||
|
style: pointerStyle,
|
||||||
|
setStyle: setPointerStyle,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
@ -30,8 +30,8 @@ export default function DiagramContextProvider({ children }) {
|
|||||||
{
|
{
|
||||||
id: prev.length,
|
id: prev.length,
|
||||||
name: `table_${prev.length}`,
|
name: `table_${prev.length}`,
|
||||||
x: -transform.pan.x,
|
x: transform.pan.x,
|
||||||
y: -transform.pan.y,
|
y: transform.pan.y,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: "id",
|
name: "id",
|
||||||
|
@ -23,16 +23,17 @@ export default function NotesContextProvider({ children }) {
|
|||||||
return temp.map((t, i) => ({ ...t, id: i }));
|
return temp.map((t, i) => ({ ...t, id: i }));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
const height = 88;
|
||||||
setNotes((prev) => [
|
setNotes((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
id: prev.length,
|
id: prev.length,
|
||||||
x: -transform.pan.x,
|
x: transform.pan.x,
|
||||||
y: -transform.pan.y,
|
y: transform.pan.y - height / 2,
|
||||||
title: `note_${prev.length}`,
|
title: `note_${prev.length}`,
|
||||||
content: "",
|
content: "",
|
||||||
color: defaultNoteTheme,
|
color: defaultNoteTheme,
|
||||||
height: 88,
|
height,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,17 @@
|
|||||||
import { createContext, useState } from "react";
|
import { createContext, useState } from "react";
|
||||||
import { tableWidth } from "../data/constants";
|
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 }) {
|
export default function SettingsContextProvider({ children }) {
|
||||||
const [settings, setSettings] = useState({
|
const [settings, setSettings] = useState({
|
||||||
@ -13,7 +23,7 @@ export default function SettingsContextProvider({ children }) {
|
|||||||
panning: true,
|
panning: true,
|
||||||
showCardinality: true,
|
showCardinality: true,
|
||||||
tableWidth: tableWidth,
|
tableWidth: tableWidth,
|
||||||
showCursorCoordinates: false,
|
showDebugCoordinates: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -9,10 +9,11 @@ export default function TransformContextProvider({ children }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {typeof setTransformInternal}
|
* @type {typeof DrawDB.TransformContext["setTransform"]}
|
||||||
*/
|
*/
|
||||||
const setTransform = useCallback(
|
const setTransform = useCallback(
|
||||||
(actionOrValue) => {
|
(actionOrValue) => {
|
||||||
|
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
||||||
const findFirstNumber = (...values) =>
|
const findFirstNumber = (...values) =>
|
||||||
values.find((value) => typeof value === "number" && !isNaN(value));
|
values.find((value) => typeof value === "number" && !isNaN(value));
|
||||||
|
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
import { createContext, useState } from "react";
|
import { createContext, useState } from "react";
|
||||||
|
|
||||||
export const UndoRedoContext = createContext(null);
|
export const UndoRedoContext = createContext({
|
||||||
|
undoStack: [],
|
||||||
|
setUndoStack: () => {},
|
||||||
|
redoStack: [],
|
||||||
|
setRedoStack: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
export default function UndoRedoContextProvider({ children }) {
|
export default function UndoRedoContextProvider({ children }) {
|
||||||
const [undoStack, setUndoStack] = useState([]);
|
const [undoStack, setUndoStack] = useState([]);
|
||||||
|
Loading…
Reference in New Issue
Block a user