Skip to content

Commit

Permalink
Merge pull request #57 from The-Zagy/import-elements
Browse files Browse the repository at this point in the history
  • Loading branch information
nagy-nabil authored Sep 2, 2023
2 parents 0d4e628 + 54a8a2b commit 71f273d
Show file tree
Hide file tree
Showing 13 changed files with 354 additions and 18 deletions.
36 changes: 35 additions & 1 deletion .github/workflows/preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ on:
push:
branches-ignore:
- main

jobs:
Deploy-Preview:
runs-on: ubuntu-latest
permissions:
deployments: write
pull-requests: write

steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
Expand All @@ -34,4 +39,33 @@ jobs:
- name: Build Project Artifacts
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy Project Artifacts to Vercel
run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }}
id: upload_artifacts
run: |
PREVIEW_LINK_TEMP=$(vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} | tail -n 1 | grep -o 'https://.*')
echo "PREVIEW_LINK=$PREVIEW_LINK_TEMP" >> "$GITHUB_ENV"
- uses: chrnorm/deployment-action@v2
name: CREATE GH DEPLOYMENT
id: deployment
with:
token: "${{ github.token }}"
environment-url: ${{ steps.upload_artifacts.outputs.PREVIEW_LINK}}
environment: preview
log-url: ${{ steps.upload_artifacts.outputs.PREVIEW_LINK}}

- name: Update deployment status (success)
if: success()
uses: chrnorm/deployment-status@v2
with:
token: "${{ github.token }}"
environment-url: ${{ steps.deployment.outputs.environment_url }}
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
state: "success"

- name: Update deployment status (failure)
if: failure()
uses: chrnorm/deployment-status@v2
with:
token: "${{ github.token }}"
environment-url: ${{ steps.deployment.outputs.environment_url }}
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
state: "failure"
11 changes: 9 additions & 2 deletions src/actions/delete.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Command, UndoableCommand } from "./types";
import { ZagyCanvasElement } from "@/types/general";
import { ZagyCanvasElement, ZagyCanvasImageElement } from "@/types/general";
import { useStore } from "@/store/index";
import { CursorFn } from "@/types/general";
import { Point, getHitElement } from "@/utils";
Expand Down Expand Up @@ -36,7 +36,14 @@ class DeleteAction {
if (!this.willDelete) return null;
const { elements } = useStore.getState();
const deletedElements: ZagyCanvasElement[] = elements
.filter((val) => val.willDelete)
.filter(
(val) =>
val.willDelete &&
!(
val.shape === "image" &&
(val as ZagyCanvasImageElement).imgRef instanceof Promise
)
)
.map((val) => ({ ...val, willDelete: false }));
if (deletedElements.length === 0) return null;
return {
Expand Down
13 changes: 10 additions & 3 deletions src/actions/deleteSelected.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { UndoableCommand } from "./types";
import { ZagyCanvasElement } from "@/types/general";
import { ZagyCanvasElement, ZagyCanvasImageElement } from "@/types/general";
import { useStore } from "@/store/index";

export class ActionDeleteSelected extends UndoableCommand {
Expand All @@ -8,11 +8,18 @@ export class ActionDeleteSelected extends UndoableCommand {
public execute() {
const { setElements, setSelectedElements, elements, selectedElements } =
useStore.getState();
this.#selectedElements = selectedElements;
// if the element is image and still loading to add it to the history just delete it
this.#selectedElements = selectedElements.filter(
(itm) =>
!(
itm.shape === "image" &&
(itm as ZagyCanvasImageElement).imgRef instanceof Promise
)
);
// remove any elements that exist on selected elements array
// create hash of ids for easy check while filtring the elements
const ids = new Set<string>();
for (const itm of this.#selectedElements) {
for (const itm of selectedElements) {
ids.add(itm.id);
}
const nextElements = elements.filter((itm) => !ids.has(itm.id));
Expand Down
36 changes: 36 additions & 0 deletions src/actions/importElements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { UndoableCommand } from "./types";
import { useStore } from "@/store/index";
import { generateImageElement } from "@/utils/canvas/generateElement";
import { Point, normalizeToGrid } from "@/utils";

export class ActionImportElements extends UndoableCommand {
#importedIds: Set<string>;

constructor(private dataTransfer: DataTransfer, private mouseCoords: Point) {
super();
this.#importedIds = new Set();
}

public execute() {
const { setElements, getPosition } = useStore.getState();
for (const item of this.dataTransfer.items) {
// if the content is text then we need to check to its structure if we can create ZagyElement from it or not, if not fallback to normal text
// ignore any files that is not image
// TODO branch to every possible item we can create from clipboard
if (item.type.indexOf("image") !== -1) {
const blob = item.getAsFile();
if (!blob) return;
const norm = normalizeToGrid(getPosition(), this.mouseCoords);
const el = generateImageElement(blob, norm);
this.#importedIds.add(el.id);
setElements((prev) => [...prev, el]);
}
}
return;
}

public undo() {
const { setElements } = useStore.getState();
setElements((prev) => prev.filter((i) => !this.#importedIds.has(i.id)));
}
}
55 changes: 54 additions & 1 deletion src/components/ZagyDraw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import clsx from "clsx";
import { RoughGenerator } from "roughjs/bin/generator";
import { Minus, Plus } from "lucide-react";
import { Button } from "./ui/button";
import { ActionImportElements } from "@/actions/importElements";
import { useCursor, useEvent, useGlobalEvent, useMultiPhaseEvent, useRenderScene } from "@/hooks";
import { commandManager } from "@/actions/commandManager";

Expand All @@ -19,8 +20,12 @@ import TextAction from "@/actions/createText";
import MoveElementAction from "@/actions/moveElement";
// import { normalizePos } from "utils";
import { regenerateCacheElement } from "@/utils/canvas/generateElement";
import { CursorFn } from "@/types/general";
// import { distance, getHitElement } from "@/utils";
import { getHitElement } from "@/utils";

const { setZoomLevel, setDimensions, setIsMouseDown, setElements } = useStore.getState();
const { setZoomLevel, setDimensions, setIsMouseDown, setElements, setCursorFn } =
useStore.getState();

// const debouncedRegenerateCacheElements = debounce(
// 100,
Expand Down Expand Up @@ -165,8 +170,56 @@ function ZagyDraw() {

useGlobalEvent("wheel", handleScroll);
useGlobalEvent("resize", handleResize);
useGlobalEvent("paste", (event) => {
event.preventDefault();
if (!event.clipboardData) return;
commandManager.executeCommand(
new ActionImportElements(event.clipboardData, [
mouseCoords.current[0] / zoomLevel,
mouseCoords.current[1] / zoomLevel,
])
);
});

useEvent("pointerdown", selectSingleElement, canvas.current);

// show mouse curosr as move when hovering above element in default mode
// TODO is this the right place to do this?
useGlobalEvent("pointermove", (event) => {
// TODO, this is used to get the mouse coords while pasting elements into the canvas, so we need to check is this good way to do it or we need to set this in global state
mouseCoords.current = [event.clientX, event.clientY];
if (cursorFn !== CursorFn.Default) return;
if (!canvas.current) return;
const ctx = canvas.current.getContext("2d");
if (!ctx) return;
const el = getHitElement(visibleElements, ctx, [event.clientX, event.clientY], position);
if (el) {
setCursorFn(CursorFn.Move);
// TODO: move this code whereever we will handle resize/rotate
// test mouse position to all corners
// top left
// if (distance([event.clientX, event.clientY], [el.x, el.y]) < 5) {
// setCursorFn(CursorFn["Nwse-resize"]);
// }
// // top right
// else if (distance([event.clientX, event.clientY], [el.endX, el.y]) < 5) {
// setCursorFn(CursorFn["Nesw-resize"]);
// }
// // bottom right
// else if (distance([event.clientX, event.clientY], [el.endX, el.endY]) < 5) {
// setCursorFn(CursorFn["Nwse-resize"]);
// }
// // bottom left
// else if (distance([event.clientX, event.clientY], [el.x, el.endY]) < 5) {
// setCursorFn(CursorFn["Nesw-resize"]);
// } else {
// // default is point inside the element
// setCursorFn(CursorFn.Move);
// }
} else {
setCursorFn(CursorFn.Default);
}
});
useMultiPhaseEvent(
"dragIntoCanvas",
[
Expand Down
5 changes: 4 additions & 1 deletion src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
const CELL_SIZE = 20;
const CACHE_CANVAS_SIZE_THRESHOLD = 10;

export { CELL_SIZE, CACHE_CANVAS_SIZE_THRESHOLD };
const PREVIEW_IMAGE_WIDTH = 200;
const PREVIEW_IMAGE_HEIGHT = 200;

export { CELL_SIZE, CACHE_CANVAS_SIZE_THRESHOLD, PREVIEW_IMAGE_WIDTH, PREVIEW_IMAGE_HEIGHT };
13 changes: 13 additions & 0 deletions src/hooks/useCursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,19 @@ export function useCursor(
case CursorFn.Erase:
document.body.style.cursor = "crosshair";
break;
// TODO, enable with implementing resize
// case CursorFn["Ew-resize"]:
// document.body.style.cursor = "ew-resize";
// break;
// case CursorFn["Ns-resize"]:
// document.body.style.cursor = "ns-resize";
// break;
// case CursorFn["Nesw-resize"]:
// document.body.style.cursor = "nesw-resize";
// break;
// case CursorFn["Nwse-resize"]:
// document.body.style.cursor = "nwse-resize";
// break;
default:
return;
}
Expand Down
1 change: 1 addition & 0 deletions src/tests/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ async function createElement(
handdrawn: CursorFn.FreeDraw,
line: CursorFn.Line,
text: CursorFn.Text,
image: CursorFn.Default,
};
await clickCursor(cursorTestId[elementType]);
fireEvent.pointerDown(
Expand Down
28 changes: 26 additions & 2 deletions src/types/general.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Drawable } from "roughjs/bin/core";

type Point = [number, number];
type ElementTypes = "rectangle" | "line" | "text" | "handdrawn";
type ElementTypes = "rectangle" | "line" | "text" | "handdrawn" | "image";

const FontTypeOptions = {
code: "FiraCode",
Expand Down Expand Up @@ -44,6 +44,8 @@ interface TextOptions extends SharedOptions {

type HanddrawnOptions = SharedOptions;

type ImageOptions = SharedOptions;

type GlobalElementOptions = TextOptions & RectOptions & LineOptions;

interface Position {
Expand Down Expand Up @@ -87,6 +89,16 @@ interface ZagyCanvasTextElement extends ZagyCanvasElement {
options: TextOptions;
}

interface ZagyCanvasImageElement extends ZagyCanvasElement {
shape: "image";
/**
* A string containing an object URL that can be used to reference the contents of the specified source object(URL.createObjectURL)
*/
image: string;
// TODO: change to use the cache prop already defined om ZagyElement
imgRef: Promise<void> | HTMLImageElement;
}

interface ZagyCanvasHandDrawnElement extends ZagyCanvasElement, Partial<CachableElement> {
shape: "handdrawn";
path2D: Path2D;
Expand All @@ -103,6 +115,11 @@ enum CursorFn {
Text,
Erase,
Move,
// TODO, enable when implementing the resize
// "Ew-resize",
// "Ns-resize",
// "Nesw-resize",
// "Nwse-resize",
}

function isRect(el: ZagyCanvasElement): el is ZagyCanvasRectElement {
Expand All @@ -120,6 +137,11 @@ function isText(el: ZagyCanvasElement): el is ZagyCanvasTextElement {
function isHanddrawn(el: ZagyCanvasElement): el is ZagyCanvasHandDrawnElement {
return el.shape === "handdrawn";
}

function isImage(el: ZagyCanvasElement): el is ZagyCanvasImageElement {
return el.shape === "image";
}

//function to check if elements extends CachableElement

export type {
Expand All @@ -140,5 +162,7 @@ export type {
FillStyleOptions,
CachableElement,
HanddrawnOptions,
ZagyCanvasImageElement,
ImageOptions,
};
export { CursorFn, FontTypeOptions, isLine, isRect, isText, isHanddrawn };
export { CursorFn, FontTypeOptions, isLine, isRect, isText, isHanddrawn, isImage };
Loading

0 comments on commit 71f273d

Please sign in to comment.