diff --git a/src/components/Canvas/Canvas.tsx b/src/components/Canvas/Canvas.tsx index eba3e4c..db93ca3 100644 --- a/src/components/Canvas/Canvas.tsx +++ b/src/components/Canvas/Canvas.tsx @@ -4,13 +4,14 @@ import { parseColor } from "react-aria-components"; import { useShallow } from "zustand/react/shallow"; import useStoreSubscription from "@/state/hooks/useStoreSubscription"; import useStore from "@/state/hooks/useStore"; -import * as Utils from "@/lib/utils"; // Types import type { MouseEvent as ReactMouseEvent } from "react"; import { type Coordinates, CanvasElementPath } from "@/types"; import useThrottle from "@/state/hooks/useThrottle"; import useCanvasRedrawListener from "@/state/hooks/useCanvasRedrawListener"; +import useCanvasRef from "@/state/hooks/useCanvasRef"; +import { redrawCanvas } from "@/lib/utils"; // Styles using Tailwind @@ -26,35 +27,36 @@ const Canvas = forwardRef(function Canvas( ) { const { mode, - background, shape, width, height, dpi, - scale, - position, changeMode, changeColor, createElement, getActiveLayer, - pushHistory + pushHistory, + getPointerPosition, + centerCanvas } = useStore( useShallow((state) => ({ mode: state.mode, - background: state.background, shape: state.shape, width: state.width, height: state.height, dpi: state.dpi, - scale: state.scale, position: state.position, changeMode: state.changeMode, changeColor: state.changeColor, createElement: state.createElement, getActiveLayer: state.getActiveLayer, - pushHistory: state.pushHistory + pushHistory: state.pushHistory, + getPointerPosition: state.getPointerPosition, + drawCanvas: state.drawCanvas, + centerCanvas: state.centerCanvas })) ); + const { setRef } = useCanvasRef(); const color = useStoreSubscription((state) => state.color); const strokeWidth = useStoreSubscription((state) => state.strokeWidth); const shapeMode = useStoreSubscription((state) => state.shapeMode); @@ -89,47 +91,38 @@ const Canvas = forwardRef(function Canvas( ctx.globalAlpha = opacity.current; + // Clip the drawing to the bounds of the canvas + ctx.save(); + ctx.rect(0, 0, width, height); + ctx.clip(); + // Calculate the position of the mouse relative to the canvas. - const { x, y } = Utils.getCanvasPosition(e.clientX, e.clientY, canvas); + const { x, y } = getPointerPosition(canvas, e.clientX, e.clientY); + const floorX = Math.floor(x); + const floorY = Math.floor(y); if (!isDrawing.current) { - initialPosition.current = { x, y }; + initialPosition.current = { x: floorX, y: floorY }; } const activeLayer = getActiveLayer(); isDrawing.current = !activeLayer.hidden; if (mode === "brush" || mode === "eraser") { currentPath2D.current = new Path2D(); - currentPath2D.current.moveTo(x, y); + currentPath2D.current.moveTo(floorX, floorY); // Save the current path. - currentPath.current.push({ x, y, startingPoint: true }); + currentPath.current.push({ x: floorX, y: floorY, startingPoint: true }); } else if (mode === "eye_drop" && !isGrabbing) { - // `.getImageData()` retreives the x and y coordinates of the pixel - // differently if the canvas is scaled. So, we need to multiply the - // x and y coordinates by the DPI to get the correct pixel. - const pixel = ctx.getImageData( - Math.floor(x * dpi), - Math.floor(y * dpi), - 1, - 1 - ).data; + // `getPointerPosition` gives us the position in world coordinates, + // but we need the position in canvas coordinates for `getImageData`. + const rect = canvas.getBoundingClientRect(); + const canvasX = Math.floor(e.clientX - rect.left); + const canvasY = Math.floor(e.clientY - rect.top); + const pixel = ctx.getImageData(canvasX, canvasY, 1, 1).data; const colorStr = `rgb(${pixel[0]}, ${pixel[1]}, ${pixel[2]})`; - let color; - - if (colorStr === "rgb(0, 0, 0)") { - // If the color is transparent, we want to assume - // that the user wanted to select the background color - // which visually is white. For the color to be - // transparent is correct, but from a UX perspective, - // it's not what the user would expect. So, - // we'll set the color to white. - color = parseColor("rgb(255, 255, 255)"); - } else { - color = parseColor(colorStr); - } - changeColor(color.toString("hex")); + changeColor(parseColor(colorStr).toString("hex")); changeMode("move"); } }; @@ -138,56 +131,58 @@ const Canvas = forwardRef(function Canvas( // If the left mouse button is not pressed, then we should not draw. // If the layer is hidden, we should not draw. // If the user is grabbing the canvas (for moving), we should not draw. - const activeLayer = canvasRef.current; - if (!activeLayer) { + const canvas = canvasRef.current; + if (!canvas) { throw new Error("Canvas Ref is not set. This is a bug."); } - const onCanvas = - e.target === activeLayer || activeLayer.contains(e.target as Node); + const onCanvas = e.target === canvas || canvas.contains(e.target as Node); if (e.buttons !== 1 || !isDrawing.current || isGrabbing) { return; } - if (mode === "shapes") { + if (mode === "shapes" || !currentPath2D.current) { currentPath2D.current = new Path2D(); - document.dispatchEvent(new CustomEvent("canvas:redraw")); } - const ctx = activeLayer.getContext("2d"); + const ctx = canvas.getContext("2d"); if (!ctx) throw new Error("Couldn't get the 2D context of the canvas."); // Calculate the position of the mouse relative to the canvas. - const { x, y } = Utils.getCanvasPosition(e.clientX, e.clientY, activeLayer); + const { x, y } = getPointerPosition(canvas, e.clientX, e.clientY); + const floorX = Math.floor(x); + const floorY = Math.floor(y); ctx.globalCompositeOperation = mode === "eraser" ? "destination-out" : "source-over"; ctx.fillStyle = color.current; ctx.strokeStyle = color.current; + // ctx.globalAlpha = mode === "eraser" ? 0 : opacity.current; ctx.lineWidth = strokeWidth.current * dpi; const currentShapeMode = shapeMode.current; - if (!currentPath2D.current) { - currentPath2D.current = new Path2D(); - } - switch (mode) { case "brush": case "eraser": { if (!onCanvas) return; - ctx.strokeStyle = color.current; ctx.lineWidth = strokeWidth.current * dpi; ctx.lineCap = "round"; ctx.lineJoin = "round"; - currentPath2D.current.lineTo(x, y); + currentPath2D.current.lineTo(floorX, floorY); ctx.stroke(currentPath2D.current); - currentPath.current.push({ x, y, startingPoint: false }); + currentPath.current.push({ + x: floorX, + y: floorY, + startingPoint: false + }); + // drawCanvas(activeLayer); break; } case "shapes": { + redrawCanvas(); if (shape === "circle") { const width = x - initialPosition.current.x; const height = y - initialPosition.current.y; @@ -262,7 +257,9 @@ const Canvas = forwardRef(function Canvas( if (!ctx) throw new Error("Couldn't get the 2D context of the canvas."); - const { x, y } = Utils.getCanvasPosition(e.clientX, e.clientY, canvas); + ctx.restore(); + + const { x, y } = getPointerPosition(canvas, e.clientX, e.clientY); const { x: initX, y: initY } = initialPosition.current; let elementType; @@ -297,6 +294,7 @@ const Canvas = forwardRef(function Canvas( properties }); currentPath.current = []; + redrawCanvas(); }; const onMouseEnter = (e: ReactMouseEvent) => { @@ -311,29 +309,30 @@ const Canvas = forwardRef(function Canvas( useEffect(() => { document.addEventListener("mousemove", onMouseMove); + setRef(canvasRef.current); return () => document.removeEventListener("mousemove", onMouseMove); - }, [onMouseMove]); + }, [onMouseMove, setRef]); + + // Initially center the canvas. + useEffect(() => { + const ref = canvasRef.current; + if (!ref) return; - const transform = `translate(${position.x}px, ${position.y}px) scale(${scale})`; + centerCanvas(ref); + }, [centerCanvas]); return ( ); }); diff --git a/src/components/CanvasPane/CanvasPane.tsx b/src/components/CanvasPane/CanvasPane.tsx index baeaa96..78aba4b 100644 --- a/src/components/CanvasPane/CanvasPane.tsx +++ b/src/components/CanvasPane/CanvasPane.tsx @@ -13,22 +13,26 @@ import ScaleIndicator from "@/components/ScaleIndicator/ScaleIndicator"; // Types import type { ReactNode } from "react"; import type { Coordinates } from "@/types"; +import { redrawCanvas } from "@/lib/utils"; const MemoizedCanvas = memo(Canvas); const MemoizedDrawingToolbar = memo(DrawingToolbar); const MemoizedScaleIndicator = memo(ScaleIndicator); -function CanvasPane(): ReactNode { +type CanvasPaneProps = Readonly<{ + loading?: boolean; +}>; + +function CanvasPane({ loading }: CanvasPaneProps): ReactNode { const { mode, scale, changeX, changeY, - increaseScale, - decreaseScale, changeElementProperties, createElement, getActiveLayer, + performZoom, pushHistory } = useStore( useShallow((state) => ({ @@ -36,11 +40,10 @@ function CanvasPane(): ReactNode { scale: state.scale, changeX: state.changeX, changeY: state.changeY, - increaseScale: state.increaseScale, - decreaseScale: state.decreaseScale, changeElementProperties: state.changeElementProperties, createElement: state.createElement, getActiveLayer: state.getActiveLayer, + performZoom: state.performZoom, pushHistory: state.pushHistory })) ); @@ -60,14 +63,14 @@ function CanvasPane(): ReactNode { // Effect is getting ugly... Might be a good idea to split // this into multiple effects. useEffect(() => { - const canvasSpace = canvasSpaceRef.current; + const canvasSpace = canvasRef.current; if (!canvasSpace) return; const isClickingOnSpace = (e: MouseEvent) => e.target === canvasSpace || canvasSpace.contains(e.target as Node); function handleMouseDown(e: MouseEvent) { - if (e.buttons !== 1 || !canvasSpace) return; + if (e.buttons !== 1) return; clientPosition.current = { x: e.clientX, y: e.clientY }; startMovePosition.current = { x: e.clientX, y: e.clientY }; @@ -104,7 +107,7 @@ function CanvasPane(): ReactNode { } function handleMouseMove(e: MouseEvent) { - if (e.buttons !== 1 || !isGrabbing || !canvasSpace) return; + if (e.buttons !== 1 || !isGrabbing) return; const canvas = canvasRef.current; const layer = getActiveLayer(); @@ -113,38 +116,26 @@ function CanvasPane(): ReactNode { let dx = e.clientX - clientPosition.current.x; let dy = e.clientY - clientPosition.current.y; - const { - left: sLeft, - width: sWidth, - height: sHeight, - top: sTop - } = canvasSpace.getBoundingClientRect(); - if (isPanning && isGrabbing) { - const { - left: cLeft, - top: cTop, - width: cWidth, - height: cHeight - } = canvas.getBoundingClientRect(); - - // Check if the layer is outside the canvas space. - // If it is, we don't want to move it. - // Note: We add 20 so that we can still see the layer when it's almost outside the canvas space. - if (cLeft + dx <= -cWidth + sLeft + 20 || cLeft + dx >= sWidth + 20) { - dx = 0; // Set to 0 so that the layer doesn't move. - } + // TODO: Have to revisit the calculation to know how the canvas is considered off screen. + // As a temporary solution, a button in the left toolbar pane is added to reset the canvas view. + // const { left, top } = isCanvasOffscreen(canvas, dx, dy); - if (cTop + dy <= -cHeight + sTop + 20 || cTop + dy >= sHeight + 20) { - dy = 0; // Set to 0 so that the layer doesn't move. - } + // if (left) dx = 0; + // if (top) dy = 0; // Apply the changes. changeX(dx); changeY(dy); + redrawCanvas(); } else if (isMoving) { // Move the shapes for the current layer. + // divide the dx and dy by the scale to get the correct + // 'mouse feel' like movement. + dx /= scale; + dy /= scale; + changeElementProperties( (state) => { if (state.type === "brush" || state.type === "eraser") { @@ -166,6 +157,7 @@ function CanvasPane(): ReactNode { }, (element) => element.layerId === layer.id ); + redrawCanvas(); } clientPosition.current = { x: e.clientX, y: e.clientY }; } @@ -192,47 +184,32 @@ function CanvasPane(): ReactNode { function handleKeyDown(e: KeyboardEvent) { setShiftKey(e.shiftKey); setCtrlKey(e.ctrlKey); - - if (e.key === "+") { - e.preventDefault(); - increaseScale(); - } else if (e.key === "_") { - e.preventDefault(); - decreaseScale(); - } - - if (e.type === "keyup") { - return; - } } function handleZoom(e: Event) { if (!canvasSpace) return; if (e instanceof WheelEvent) { - if (!e.shiftKey) return; - - if (e.deltaY > 0) { - decreaseScale(); - } else { - increaseScale(); - } + console.log(e.deltaY); + performZoom(e.clientX, e.clientY, e.deltaY / 10); // Handle the click event } else if (e instanceof MouseEvent) { if (!isClickingOnSpace(e)) return; // Shift key means we are moving. We don't want to zoom in this case. - if (e.buttons === 0 && !e.shiftKey) { + if ( + e.buttons === 0 && + !e.shiftKey && + (mode === "zoom_in" || mode === "zoom_out") + ) { // Left click - if (mode === "zoom_in") { - increaseScale(); - } else if (mode === "zoom_out") { - decreaseScale(); - } else { - return; // We don't want to zoom if the mode is not zoom - } + let factor = 25; + if (mode === "zoom_in") factor *= -1; + performZoom(e.clientX, e.clientY, factor); } } + + redrawCanvas(); } document.addEventListener("mousedown", handleMouseDown); @@ -262,8 +239,6 @@ function CanvasPane(): ReactNode { isGrabbing, changeElementProperties, createElement, - increaseScale, - decreaseScale, changeX, changeY, getActiveLayer, @@ -271,7 +246,9 @@ function CanvasPane(): ReactNode { ctrlKey, currentShape, currentColor, - pushHistory + pushHistory, + performZoom, + scale ]); return ( @@ -287,20 +264,18 @@ function CanvasPane(): ReactNode { )} -
- -
- - + + + {!loading ? ( + + ) : ( +
+ Loading... +
+ )} ); } diff --git a/src/components/CanvasReferenceProvider/CanvasReferenceProvider.tsx b/src/components/CanvasReferenceProvider/CanvasReferenceProvider.tsx new file mode 100644 index 0000000..764f03c --- /dev/null +++ b/src/components/CanvasReferenceProvider/CanvasReferenceProvider.tsx @@ -0,0 +1,28 @@ +import { createContext, useMemo, useState } from "react"; + +import type { Dispatch, ReactNode, SetStateAction } from "react"; + +const CanvasReferenceContext = createContext<{ + ref: HTMLCanvasElement | null; + setRef: Dispatch>; +} | null>(null); + +type CanvasReferenceProviderProps = Readonly<{ + children: ReactNode; +}>; + +function CanvasReferenceProvider({ + children +}: CanvasReferenceProviderProps): ReactNode { + const [ref, setRef] = useState(null); + + const value = useMemo(() => ({ ref, setRef }), [ref]); + + return ( + + {children} + + ); +} + +export { CanvasReferenceContext, CanvasReferenceProvider }; diff --git a/src/components/LayerInfo/LayerInfo.tsx b/src/components/LayerInfo/LayerInfo.tsx index f9fbff2..9a86d02 100644 --- a/src/components/LayerInfo/LayerInfo.tsx +++ b/src/components/LayerInfo/LayerInfo.tsx @@ -21,6 +21,7 @@ import type { Layer } from "@/types"; import LayerPreview from "@/components/LayerPreview/LayerPreview"; import Tooltip from "@/components/Tooltip/Tooltip"; import ElementsStore from "@/state/stores/ElementsStore"; +import { redrawCanvas } from "@/lib/utils"; type LayerInfoProps = Readonly< Layer & { @@ -73,9 +74,7 @@ function LayerInfo({ const onToggleVisibility = () => { toggleVisibility(id); - document.dispatchEvent( - new CustomEvent("canvas:redraw", { detail: { noChange: true } }) - ); + redrawCanvas(true); }; const onDelete = () => { @@ -97,9 +96,7 @@ function LayerInfo({ } else { moveLayerDown(id); } - document.dispatchEvent( - new CustomEvent("canvas:redraw", { detail: { noChange: true } }) - ); + redrawCanvas(true); }; const onRename = () => { @@ -126,7 +123,7 @@ function LayerInfo({ "flex items-center w-full max-w-full h-[2.6rem] py-[0.2rem] px-[0.5rem] whitespace-nowrap border border-[rgb(56,55,55)] last:rounded-b-[5px]", "group", { - "bg-[#d1836a]": active, + "bg-accent": active, "bg-[rgb(36,36,36)]": !active } )} @@ -201,7 +198,7 @@ function LayerInfo({ ) : (
diff --git a/src/components/LayerPreview/LayerPreview.tsx b/src/components/LayerPreview/LayerPreview.tsx index 8faeae4..666ef80 100644 --- a/src/components/LayerPreview/LayerPreview.tsx +++ b/src/components/LayerPreview/LayerPreview.tsx @@ -11,11 +11,12 @@ type LayerPreviewProps = Readonly<{ const PREVIEW_WIDTH = 36; // Width of the preview canvas const DEBOUNCE_REDRAW = true; +const PREVIEW_DRAW = true; function LayerPreview({ id }: LayerPreviewProps): ReactNode { const previewRef = useRef(null); - useCanvasRedrawListener(previewRef, id, DEBOUNCE_REDRAW); + useCanvasRedrawListener(previewRef, id, DEBOUNCE_REDRAW, PREVIEW_DRAW); return ( - {renderedModes} +
{renderedModes}
); } diff --git a/src/components/Main/Main.tsx b/src/components/Main/Main.tsx index 303f0ef..663d6e8 100644 --- a/src/components/Main/Main.tsx +++ b/src/components/Main/Main.tsx @@ -1,9 +1,9 @@ // Lib import ElementsStore from "@/state/stores/ElementsStore"; import LayersStore from "@/state/stores/LayersStore"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import useStore from "@/state/hooks/useStore"; -import useStoreEffect from "@/state/hooks/useStoreEffect"; +import { useShallow } from "zustand/react/shallow"; // Types import type { ReactNode } from "react"; @@ -13,20 +13,31 @@ import CanvasPane from "@/components/CanvasPane/CanvasPane"; import LeftToolbar from "@/components/LeftToolbar/LeftToolbar"; import LayerPane from "@/components/LayerPane/LayerPane"; import ReferenceWindow from "@/components/ReferenceWindow/ReferenceWindow"; +import { redrawCanvas } from "@/lib/utils"; +import ImageElementStore from "@/state/stores/ImageElementStore"; function Main(): ReactNode { - const { setElements, setLayers } = useStore((store) => ({ - setElements: store.setElements, - setLayers: store.setLayers - })); - const refereceWindowEnabled = useStore( - (store) => store.referenceWindowEnabled + const { + setElements, + setLayers, + refereceWindowEnabled, + loadCanvasProperties + } = useStore( + useShallow((store) => ({ + setElements: store.setElements, + setLayers: store.setLayers, + refereceWindowEnabled: store.referenceWindowEnabled, + loadCanvasProperties: store.loadCanvasProperties + })) ); + const [loading, setLoading] = useState(true); useEffect(() => { async function updateLayersAndElements() { const elements = await ElementsStore.getElements(); const layers = await LayersStore.getLayers(); + await ImageElementStore.loadImages(); + loadCanvasProperties(); // There must always be at least one layer. // If there are no layers, do not update, @@ -43,36 +54,13 @@ function Main(): ReactNode { })) ); } - setElements( - elements.map(([, element]) => ({ - ...element, - focused: false - })) - ); + setElements(elements.map(([, element]) => element)); + setLoading(false); + redrawCanvas(); } updateLayersAndElements(); - }, [setElements, setLayers]); - - useStoreEffect( - (state) => ({ layers: state.layers, elements: state.elements }), - (current, previous) => { - const changeInLayerToggle = - current.layers.length === previous.layers.length && - current.layers.some( - (layer, index) => layer.active !== previous.layers[index].active - ); - const layerAdded = current.layers.length > previous.layers.length; - - document.dispatchEvent( - new CustomEvent("canvas:redraw", { - detail: { - noChange: changeInLayerToggle || layerAdded - } - }) - ); - } - ); + }, [setElements, setLayers, loadCanvasProperties]); return (
- + {/* Reference window */} {refereceWindowEnabled && } diff --git a/src/components/Navbar/Navbar.tsx b/src/components/Navbar/Navbar.tsx index d0c5ce1..1a4e6dc 100644 --- a/src/components/Navbar/Navbar.tsx +++ b/src/components/Navbar/Navbar.tsx @@ -5,16 +5,27 @@ import { useShallow } from "zustand/shallow"; import useStore from "@/state/hooks/useStore"; import LayersStore from "@/state/stores/LayersStore"; import ElementsStore from "@/state/stores/ElementsStore"; +import { detectOperatingSystem, redrawCanvas } from "@/lib/utils"; +import useCanvasRef from "@/state/hooks/useCanvasRef"; // Icons import Fullscreen from "@/components/icons/Fullscreen/Fullscreen"; -import Image from "@/components/icons/Image/Image"; +import { default as ImageIcon } from "@/components/icons/Image/Image"; import Export from "@/components/icons/Export/Export"; import FloppyDisk from "@/components/icons/FloppyDisk/FloppyDisk"; import Close from "@/components/icons/Close/Close"; +import ZoomOut from "../icons/ZoomOut/ZoomOut"; +import ZoomIn from "../icons/ZoomIn/ZoomIn"; +import Rotate from "../icons/Rotate/Rotate"; +import FolderOpen from "../icons/FolderOpen/FolderOpen"; // Types -import type { ComponentProps, ReactElement, ReactNode } from "react"; +import type { + ChangeEvent, + ComponentProps, + ReactElement, + ReactNode +} from "react"; // Components import { @@ -27,27 +38,37 @@ import { MenubarShortcut } from "@/components/ui/menubar"; import NavbarFileSaveStatus from "../NavbarFileSaveStatus/NavbarFileSaveStatus"; -import ZoomIn from "../icons/ZoomIn/ZoomIn"; -import { detectOperatingSystem } from "@/lib/utils"; -import ZoomOut from "../icons/ZoomOut/ZoomOut"; +import ImageElementStore from "@/state/stores/ImageElementStore"; function Navbar(): ReactNode { const { prepareForExport, prepareForSave, toggleReferenceWindow, - increaseScale, - decreaseScale + performZoom, + centerCanvas, + setZoom, + resetLayersAndElements, + createElement, + changeDimensions, + clearHistory } = useStore( useShallow((state) => ({ prepareForExport: state.prepareForExport, prepareForSave: state.prepareForSave, toggleReferenceWindow: state.toggleReferenceWindow, - increaseScale: state.increaseScale, - decreaseScale: state.decreaseScale + performZoom: state.performZoom, + centerCanvas: state.centerCanvas, + setZoom: state.setZoom, + resetLayersAndElements: state.resetLayersAndElements, + createElement: state.createElement, + changeDimensions: state.changeDimensions, + clearHistory: state.clearHistory })) ); + const { ref } = useCanvasRef(); const downloadRef = useRef(null); + const openFileRef = useRef(null); const [saveStatus, setSaveStatus] = useState<"saving" | "saved" | "error">( "saved" ); @@ -80,7 +101,8 @@ function Navbar(): ReactNode { position: i })) ), - ElementsStore.addElements(elements) + ElementsStore.addElements(elements), + ImageElementStore.saveImages() ); await Promise.all(promises); @@ -93,9 +115,13 @@ function Navbar(): ReactNode { const handleExportFile = async () => { if (!downloadRef.current) throw new Error("Download ref not found"); + if (!ref) { + alert("Canvas ref not found."); + return; + } try { - const blob = await prepareForExport(); + const blob = await prepareForExport(ref); const url = URL.createObjectURL(blob); @@ -110,6 +136,31 @@ function Navbar(): ReactNode { } }; + function getMiddleOfCanvas() { + if (!ref) { + throw new Error("Canvas ref does not exist."); + } + + const { left, top, width, height } = ref.getBoundingClientRect(); + + return { + middleX: (left + width) / 2, + middleY: (top + height) / 2 + }; + } + + function increaseZoom() { + const { middleX, middleY } = getMiddleOfCanvas(); + performZoom(middleX, middleY, -50); + redrawCanvas(); + } + + function decreaseZoom() { + const { middleX, middleY } = getMiddleOfCanvas(); + performZoom(middleX, middleY, 50); + redrawCanvas(); + } + const toggleFullScreen = () => { const doc = window.document; const docEl = doc.documentElement; @@ -121,8 +172,78 @@ function Navbar(): ReactNode { } }; + function resetCanvasView() { + if (!ref) { + throw new Error("Canvas ref does not exist."); + } + centerCanvas(ref); + setZoom(1); + redrawCanvas(); + } + + const openFile = useCallback(() => { + if (!openFileRef.current) { + throw new Error("Download ref does not exist."); + } + + openFileRef.current.click(); + }, []); + + async function handleOpeningFile(e: ChangeEvent) { + const file = e.target.files?.[0]; + + if (!file) { + alert("No file was inputted."); + return; + } + + if (!ref) { + throw new Error("Canvas ref not found."); + } + + const shouldLoad = window.confirm( + "Loading this file will erase all current data. Proceed?" + ); + if (shouldLoad) { + // Erase everything... + await LayersStore.clearStore(); + await ElementsStore.clearStore(); + await ImageElementStore.clearStore(); + window.localStorage.clear(); + resetLayersAndElements(); // Reset the Zustand state. + + // Upload the image. + const image = new Image(); + + image.onload = function () { + URL.revokeObjectURL(image.src); + + changeDimensions({ + width: image.naturalWidth, + height: image.naturalHeight + }); + const element = createElement("image", { + width: image.naturalWidth, + height: image.naturalHeight + }); + ImageElementStore.putImage(element.id, image); + centerCanvas(ref); + redrawCanvas(); + clearHistory(); + }; + + image.src = URL.createObjectURL(file); + } + } + const menuOptions: MenuOptions = { File: [ + { + text: "Open File", + action: openFile, + icon: FolderOpen, + shortcut: "O" + }, { text: "Save File", action: handleSaveFile, @@ -136,22 +257,32 @@ function Navbar(): ReactNode { } ], View: [ + /** + * TODO: This button is a temporary fallback for resetting the canvas for when it goes off screen. The better solution + is to prevent the canvas from going off screen, but the math is currently unknown for how to do that at the moment. This + is a temporary solution. + */ + { + text: "Reset Canvas View", + action: resetCanvasView, + icon: Rotate + }, { text: "Zoom In", - action: increaseScale, + action: increaseZoom, icon: ZoomIn, shortcut: "Plus" }, { text: "Zoom Out", - action: decreaseScale, + action: decreaseZoom, icon: ZoomOut, shortcut: "Minus" }, { text: "Reference Window", action: toggleReferenceWindow, - icon: Image + icon: ImageIcon }, { text: "Toggle Full Screen", @@ -166,6 +297,9 @@ function Navbar(): ReactNode { if (e.key === "s" && e.ctrlKey) { e.preventDefault(); handleSaveFile(); + } else if (e.key === "o" && e.ctrlKey) { + e.preventDefault(); + openFile(); } }; @@ -174,11 +308,11 @@ function Navbar(): ReactNode { return () => { document.removeEventListener("keydown", handleKeyDown); }; - }, [handleSaveFile]); + }, [handleSaveFile, openFile]); return (
-
); } diff --git a/src/components/ToolbarButton/ToolbarButton.tsx b/src/components/ToolbarButton/ToolbarButton.tsx index 3ce24eb..84dc49f 100644 --- a/src/components/ToolbarButton/ToolbarButton.tsx +++ b/src/components/ToolbarButton/ToolbarButton.tsx @@ -70,8 +70,10 @@ function ToolbarButton({ const performAction = useCallback(() => { if (name === "undo") { undo(); + UTILS.redrawCanvas(); } else if (name === "redo") { redo(); + UTILS.redrawCanvas(); } else { changeMode(name); } @@ -92,8 +94,6 @@ function ToolbarButton({ chosenShortcut += e.key.toLowerCase(); } - console.log(chosenShortcut, shortcut); - if (chosenShortcut === shortcut) { performAction(); } @@ -113,12 +113,12 @@ function ToolbarButton({ >