Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

File dropdown #33

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:
jobs:
tests:
runs-on: ubuntu-latest
name: Execute Vite tests
name: Vitest

steps:
- name: Checkout repository
Expand All @@ -35,4 +35,4 @@ jobs:
- name: Upload coverage to SonarQube
uses: sonarsource/sonarqube-scan-action@v4
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
Binary file added assets/icons/IdeaDrawnNewLogo_transparent.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 13 additions & 12 deletions components/Canvas/Canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import useIndexed from "../../state/hooks/useIndexed";
import useStoreSubscription from "../../state/hooks/useStoreSubscription";
import useLayerReferences from "../../state/hooks/useLayerReferences";
import useStore from "../../state/hooks/useStore";
import * as Utils from "../../utils";
import * as Utils from "../../lib/utils";
import clsx from "clsx";

// Types
Expand All @@ -24,6 +24,7 @@ type DBLayer = {
image: Blob;
name: string;
position: number;
id: string;
};

const Canvas: FC<CanvasProps> = ({ isGrabbing }) => {
Expand Down Expand Up @@ -59,7 +60,7 @@ const Canvas: FC<CanvasProps> = ({ isGrabbing }) => {
}))
);

const { references, add, remove } = useLayerReferences();
const { references, add } = useLayerReferences();
const { get } = useIndexed();

const isDrawing = useRef<boolean>(false);
Expand Down Expand Up @@ -252,14 +253,14 @@ const Canvas: FC<CanvasProps> = ({ isGrabbing }) => {
return new Promise<[string, DBLayer][]>((resolve) => {
const newLayers: Layer[] = [];

const sorted = entries.sort((a, b) => b[1].position - a[1].position); // Sort by position, where the highest position is the top layer.
const sorted = entries.sort((a, b) => a[1].position - b[1].position); // Sort by position, where the highest position is the top layer.

sorted.forEach((entry, i) => {
const [layerId, layer] = entry;
const [id, { name }] = entry;

newLayers.push({
name: layer.name,
id: layerId,
name: name,
id: id,
active: i === 0,
hidden: false
});
Expand All @@ -277,8 +278,8 @@ const Canvas: FC<CanvasProps> = ({ isGrabbing }) => {

function updateLayerContents(entries: [string, DBLayer][]) {
entries.forEach((entry) => {
const [, layer] = entry;
const canvas = references.current[layer.position];
const [, { position, image }] = entry;
const canvas = references.current[position];

if (!canvas) return;

Expand All @@ -300,7 +301,7 @@ const Canvas: FC<CanvasProps> = ({ isGrabbing }) => {
document.dispatchEvent(ev);
};

img.src = URL.createObjectURL(layer.image);
img.src = URL.createObjectURL(image);
});
}

Expand Down Expand Up @@ -350,9 +351,9 @@ const Canvas: FC<CanvasProps> = ({ isGrabbing }) => {
transform
}}
ref={(element) => {
if (element !== null) {
add(element, i);
}
if (element !== null) {
add(element, i);
}
}}
id={layer.id}
width={width * dpi}
Expand Down
156 changes: 106 additions & 50 deletions components/CanvasPane/CanvasPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,31 @@ import useWindowDimensions from "../../state/hooks/useWindowDimesions";
import useStore from "../../state/hooks/useStore";
import { useShallow } from "zustand/react/shallow";

// Redux Actions

// Components
import DrawingToolbar from "../DrawingToolbar/DrawingToolbar";
import Canvas from "../Canvas/Canvas";
import CanvasPointerMarker from "../CanvasPointerMarker/CanvasPointerMarker";
import CanvasPointerSelection from "../CanvasPointerSelection/CanvasPointerSelection";
import ShapeElement from "../ShapeElement/ShapeElement";
import ScaleIndicator from "../ScaleIndicator/ScaleIndicator";

// Types
import type { FC } from "react";
import type { Coordinates } from "../../types";

// Styles
import "./CanvasPane.styles.css";
import useStoreSubscription from "../../state/hooks/useStoreSubscription";

const MemoizedShapeElement = memo(ShapeElement);
const MemoizedCanvas = memo(Canvas);
const MemoizedDrawingToolbar = memo(DrawingToolbar);
const MemoizedScaleIndicator = memo(ScaleIndicator);

const CanvasPane: FC = () => {
const {
mode,
scale,
changeX,
changeY,
increaseScale,
Expand All @@ -40,6 +42,7 @@ const CanvasPane: FC = () => {
} = useStore(
useShallow((state) => ({
mode: state.mode,
scale: state.scale,
changeX: state.changeX,
changeY: state.changeY,
increaseScale: state.increaseScale,
Expand All @@ -51,10 +54,14 @@ const CanvasPane: FC = () => {
createElement: state.createElement
}))
);
const currentShape = useStoreSubscription((state) => state.shape);
const currentColor = useStoreSubscription((state) => state.color);
const canvasSpaceRef = useRef<HTMLDivElement>(null);
const clientPosition = useRef<Coordinates>({ x: 0, y: 0 });
const isSelecting = useRef<boolean>(false);
const createdShapeId = useRef<string | null>(null);
const [shiftKey, setShiftKey] = useState<boolean>(false);
const [ctrlKey, setCtrlKey] = useState<boolean>(false);
const [isGrabbing, setIsGrabbing] = useState<boolean>(false);
const { getActiveLayer } = useLayerReferences();
const dimensions = useWindowDimensions();
Expand Down Expand Up @@ -90,10 +97,9 @@ const CanvasPane: FC = () => {

if (!activeLayer) throw new Error("No active layer found");

const rect = canvasSpace.getBoundingClientRect();
createElement("text", {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
x: e.clientX,
y: e.clientY,
width: 100,
height: 30,
focused: true,
Expand All @@ -104,48 +110,35 @@ const CanvasPane: FC = () => {
},
layerId: activeLayer.id
});
return;
}

if (mode === "shapes" && ctrlKey) {
const activeLayer = getActiveLayer();

const id = createElement(currentShape.current, {
x: e.clientX,
y: e.clientY,
width: 18,
height: 18,
focused: false,
layerId: activeLayer.id,
fill: currentColor.current
});
createdShapeId.current = id;
}

setIsGrabbing(isOnCanvas);
}

function handleMouseMove(e: MouseEvent) {
if (e.buttons !== 1 || !canMove || !isGrabbing || !canvasSpace) return;
if (e.buttons !== 1 || !isGrabbing || !canvasSpace) return;

const layer = getActiveLayer();

const {
left: lLeft,
top: lTop,
width: lWidth,
height: lHeight
} = layer.getBoundingClientRect();

const {
left: sLeft,
width: sWidth,
height: sHeight,
top: sTop
} = canvasSpace.getBoundingClientRect();

let dx = e.clientX - clientPosition.current.x;
let dy = e.clientY - clientPosition.current.y;

// 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 (lLeft + dx <= -lWidth + sLeft + 20 || lLeft + dx >= sWidth + 20) {
dx = 0; // Set to 0 so that the layer doesn't move.
}

if (lTop + dy <= -lHeight + sTop + 20 || lTop + dy >= sHeight + 20) {
dy = 0; // Set to 0 so that the layer doesn't move.
}

// Apply the changes.
changeX(dx);
changeY(dy);

// We grab the elements using the class name "element"
// rather than the state variable so that this effect
// doesn't depend on the state variable.
Expand All @@ -154,37 +147,95 @@ const CanvasPane: FC = () => {
(element: Element) => element.id
) as string[];

changeElementProperties(
(state) => {
let { x, y } = state;
const {
left: sLeft,
width: sWidth,
height: sHeight,
top: sTop
} = canvasSpace.getBoundingClientRect();

if (canMove && isGrabbing) {
const {
left: lLeft,
top: lTop,
width: lWidth,
height: lHeight
} = layer.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 (lLeft + dx <= -lWidth + sLeft + 20 || lLeft + dx >= sWidth + 20) {
dx = 0; // Set to 0 so that the layer doesn't move.
}

if (lTop + dy <= -lHeight + sTop + 20 || lTop + dy >= sHeight + 20) {
dy = 0; // Set to 0 so that the layer doesn't move.
}

// Apply the changes.
changeX(dx);
changeY(dy);

if (isNaN(x)) {
x = sLeft + sWidth / 2 - state.width / 2 - sLeft;
changeElementProperties(
(state) => ({
...state,
x: state.x + dx,
y: state.y + dy
}),
...elementIds
);
} else if (mode === "shapes") {
// Required for shapes to have a minimum size.
// It is also set in `ShapeElement` in the resizing
// logic. If you change it here, make sure to change
// it there as well.
const MIN_SIZE = 18;
const pointerX = e.clientX;
const pointerY = e.clientY;

changeElementProperties((state) => {
let newWidth = Math.max(MIN_SIZE, state.width + dx);
let newHeight = Math.max(MIN_SIZE, state.height + dy);

if (pointerX < state.x) {
newWidth = MIN_SIZE;
}

if (isNaN(y)) {
y = sTop + sHeight / 2 - state.height / 2 - sTop;
if (pointerY < state.y) {
newHeight = MIN_SIZE;
}

return {
...state,
x: x + dx,
y: y + dy
width: newWidth,
height: newHeight
};
},
...elementIds
);

}, createdShapeId.current!);
}
clientPosition.current = { x: e.clientX, y: e.clientY };
}

function handleMouseUp() {
// clientPosition.current = { x: 0, y: 0 };
setIsGrabbing(false);

if (mode === "shapes") {
createdShapeId.current = null;

const ev = new CustomEvent("imageupdate", {
detail: {
layer: getActiveLayer()
}
});

document.dispatchEvent(ev);
}
}

function handleKeyDown(e: KeyboardEvent) {
setShiftKey(e.shiftKey);
setCtrlKey(e.ctrlKey);

if (e.key === "+") {
e.preventDefault();
Expand Down Expand Up @@ -299,7 +350,10 @@ const CanvasPane: FC = () => {
changeX,
changeY,
getActiveLayer,
shiftKey
shiftKey,
ctrlKey,
currentShape,
currentColor
]);

return (
Expand All @@ -313,7 +367,7 @@ const CanvasPane: FC = () => {
shiftKey={shiftKey}
/>
)}
{(mode === "select" || mode === "shapes") && !isMoving && (
{(mode === "select" || (mode === "shapes" && !ctrlKey)) && !isMoving && (
<CanvasPointerSelection
canvasSpaceReference={canvasSpaceRef}
isSelecting={isSelecting}
Expand All @@ -340,6 +394,8 @@ const CanvasPane: FC = () => {
>
<MemoizedCanvas isGrabbing={isMoving} />
</div>

<MemoizedScaleIndicator scale={scale} />
</div>
);
};
Expand Down
5 changes: 2 additions & 3 deletions components/CanvasPointerMarker/CanvasPointerMarker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useState, useEffect, useRef } from "react";
import useStore from "../../state/hooks/useStore";
import useStoreSubscription from "../../state/hooks/useStoreSubscription";
import { useShallow } from "zustand/react/shallow";
import * as Utils from "../../utils";
import * as Utils from "../../lib/utils";

// Types
import type { FC, RefObject } from "react";
Expand Down Expand Up @@ -51,8 +51,7 @@ const CanvasPointerMarker: FC<CanvasPointerMarker> = ({
setIsVisible(false);
}

const { x, y, left, top, width, height } =
canvasSpace.getBoundingClientRect();
const { left, top, width, height } = canvasSpace.getBoundingClientRect();
let newX;
let newY;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Lib
import { useRef, useEffect, useState } from "react";
import * as UTILS from "../../utils";
import * as UTILS from "../../lib/utils";
import useLayerReferences from "../../state/hooks/useLayerReferences";
import useStoreSubscription from "../../state/hooks/useStoreSubscription";
import useStore from "../../state/hooks/useStore";
Expand Down
Loading