Skip to content

Commit

Permalink
feat: use browser fs to import/export whole scene
Browse files Browse the repository at this point in the history
  • Loading branch information
nagy-nabil committed Sep 8, 2023
1 parent d861d46 commit 866511e
Show file tree
Hide file tree
Showing 12 changed files with 147 additions and 50 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@radix-ui/react-switch": "^1.0.3",
"@testing-library/user-event": "^14.4.3",
"@types/throttle-debounce": "^5.0.0",
"browser-fs-access": "^0.34.1",
"class-variance-authority": "^0.7.0",
"clsx": "^1.2.1",
"lucide-react": "^0.268.0",
Expand Down
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 9 additions & 20 deletions src/actions/ExportScene.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { fileSave } from "browser-fs-access";
import { Command } from "./types";
import {
type ZagyCanvasElement,
Expand All @@ -11,9 +12,8 @@ import {
ZagyPortableT,
} from "@/types/general";
import { useStore } from "@/store/index";
import { isText } from "@/types/general";
import { isText, isHanddrawn } from "@/types/general";
import { ZagyCanvasTextElement } from "@/types/general";
import { isHanddrawn } from "@/types/general";
import { ZagyCanvasHandDrawnElement } from "@/types/general";

export const DestOpts = {
Expand Down Expand Up @@ -103,29 +103,18 @@ export class ActionExportScene extends Command {
}
// don't copy dump text into the user clipboard if there's no data to copy
if (portable.elements.length === 0) return;

// choose export mechanism
if (this.dest === DestOpts.CLIPBOARD) {
await navigator.clipboard.writeText(JSON.stringify(portable));
} else if (this.dest === DestOpts.JSON) {
// Create a Blob from the JSON string
const blob = new Blob([JSON.stringify(portable)], { type: "application/zagydraw" });
// Create a Blob URL
const blobUrl = URL.createObjectURL(blob);

// Create an <a> element
const downloadLink = document.createElement("a");

// Set the href attribute to the Blob URL
downloadLink.href = blobUrl;

// Set the download attribute to specify the filename
downloadLink.download = "zagy.zagydraw";

// Trigger a click event to open the file save dialog
downloadLink.click();

// Clean up by revoking the Blob URL
URL.revokeObjectURL(blobUrl);
await fileSave(blob, {
fileName: "zagy.zagydraw",
description: "Zagy Portable",
extensions: [".zagydraw"],
id: "zagydraw",
});
}
} catch (e) {
console.log("🪵 [copySelected.ts:15] ~ token ~ \x1b[0;32me\x1b[0m = ", e);
Expand Down
2 changes: 1 addition & 1 deletion src/actions/createText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class TextAction {
if (ctx === null) return;
const { currentText, position } = useStore.getState();
const normalizedPosition = normalizePos(position, this.lastMouseDownPosition);
element = generateTextElement(ctx, currentText, normalizedPosition, {});
element = generateTextElement(currentText, normalizedPosition, {});

const { setCurrentText, setIsWriting } = useStore.getState();
this.isAlreadyElement = false;
Expand Down
14 changes: 2 additions & 12 deletions src/actions/importElements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,7 @@ import {
export class ActionImportElements extends UndoableCommand {
#importedIds: Set<string>;

constructor(
private dataTransfer: DataTransfer,
private mouseCoords: Point,
private canvas: HTMLCanvasElement,
) {
constructor(private dataTransfer: DataTransfer, private mouseCoords: Point) {
super();
this.#importedIds = new Set();
}
Expand Down Expand Up @@ -89,8 +85,6 @@ export class ActionImportElements extends UndoableCommand {
const items = JSON.parse(pasted);
isZagyPortable(items);
const roughGenerator = new RoughGenerator();
const ctx = this.canvas.getContext("2d");
if (!ctx) return;
// we need to keep same structure and order of the copied elements relative to the bounding rect that they were copied from
// to do so i will create bounding rect between pasted elements to know each point difference from the original bounding rect
// create new bounding rect on the current mouse position, then calc each element new position, element x = newBounding.x + (oldBounding.x - el.x);
Expand Down Expand Up @@ -120,7 +114,6 @@ export class ActionImportElements extends UndoableCommand {
} else if (isText(el)) {
elsToPush.push(
generateTextElement(
ctx,
el.text.join("\n"),
[el.x, el.y],
el.options,
Expand All @@ -139,10 +132,7 @@ export class ActionImportElements extends UndoableCommand {
elsToPush.forEach((el) => this.#importedIds.add(el.id));
setElements((prev) => [...prev, ...elsToPush]);
} catch {
const ctx = this.canvas.getContext("2d");
if (!ctx) return;
const textEl = generateTextElement(ctx, pasted, this.mouseCoords);
console.log(textEl);
const textEl = generateTextElement(pasted, this.mouseCoords);
setElements((prev) => [...prev, textEl]);
}
});
Expand Down
84 changes: 84 additions & 0 deletions src/actions/openScene.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { RoughGenerator } from "roughjs/bin/generator";
import { fileOpen } from "browser-fs-access";
import { Command } from "./types";
import {
ZagyCanvasElement,
isZagyPortable,
isLine,
isRect,
isText,
isImage,
isHanddrawn,
} from "@/types/general";
import { useStore } from "@/store/index";
import {
generateCacheLineElement,
generateCacheRectElement,
generateCachedHandDrawnElement,
generateTextElement,
} from "@/utils/canvas/generateElement";

/**
* future version should support loading scene from Z+ as well
*/
export class ActionOpenScene extends Command {
public async execute() {
const { setSelectedElements, setElements, setPosition, zoomLevel } = useStore.getState();
try {
const blob = await fileOpen({
// List of allowed file extensions (with leading '.'), defaults to `''`.
extensions: [".zagydraw"],
// Textual description for file dialog , defaults to `''`.
description: "Open Zagydraw",
// By specifying an ID, the user agent can remember different directories for different IDs.
id: "zagydraw",
});
const text = await (await blob.handle?.getFile())?.text();
if (text) {
const portable = JSON.parse(text);
isZagyPortable(portable);
const roughGenerator = new RoughGenerator();
const elsToPush: ZagyCanvasElement[] = [];
for (const el of portable.elements) {
if (isRect(el)) {
elsToPush.push(
generateCacheRectElement(
roughGenerator,
[el.x, el.y],
[el.endX, el.endY],
zoomLevel,
el.options,
),
);
} else if (isLine(el)) {
elsToPush.push(
generateCacheLineElement(
roughGenerator,
el.point1,
el.point2,
zoomLevel,
el.options,
),
);
} else if (isText(el)) {
elsToPush.push(
generateTextElement(el.text.join("\n"), [el.x, el.y], el.options),
);
} else if (isHanddrawn(el)) {
elsToPush.push(
generateCachedHandDrawnElement(el.paths, zoomLevel, el.options),
);
} else if (isImage(el)) {
// generateImageElement();
}

setPosition({ x: 0, y: 0 });
setSelectedElements(() => []);
setElements(() => [...elsToPush]);
}
}
} catch (e) {
console.log("🪵 [copySelected.ts:15] ~ token ~ \x1b[0;32me\x1b[0m = ", e);
}
}
}
39 changes: 32 additions & 7 deletions src/components/DropDown.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Trash2, Github, Keyboard, Menu, Save, File, AlertTriangle } from "lucide-react";

import { useState } from "react";
import { ModeToggle } from "./mode-toggle";
import { commandManager } from "@/actions/commandManager";
import { ActionClearCanvas } from "@/actions/resetCanvas";
Expand Down Expand Up @@ -35,6 +36,8 @@ import {
} from "@/components/ui/dialog";
import { ActionExportScene, DestOpts } from "@/actions/ExportScene";
import { SHORTCUTS } from "@/constants";
import { ActionOpenScene } from "@/actions/openScene";
import useKeyboardShortcut from "@/hooks/useShortcut";

function ResetCanvasAlert() {
return (
Expand Down Expand Up @@ -117,8 +120,24 @@ function SaveToDialog() {
}

function OpenSceneDialog() {
const [isOpen, setIsOpen] = useState(false);
useKeyboardShortcut(
{
onShortcut: (e) => {
e.preventDefault();
setIsOpen(true);
},
orderMatters: true,
},
...SHORTCUTS["scene"]["open"].keys,
);

return (
<Dialog>
<Dialog
open={isOpen}
onOpenChange={(open) => {
setIsOpen(open);
}}>
{/*FIX: open dialog/alert dialog inside dropdown or context menu
@url https://github.com/radix-ui/primitives/issues/1836#issuecomment-1674338372
*/}
Expand All @@ -133,7 +152,7 @@ function OpenSceneDialog() {
<DialogTitle>Load From File</DialogTitle>
</DialogHeader>
<div className="flex h-full w-full flex-wrap justify-center gap-5 overflow-y-auto">
<div className="flex w-full flex-col gap-3 bg-yellow-100 md:flex-row">
<div className="flex w-full flex-col items-center justify-center gap-3 bg-yellow-100 md:flex-row">
<p className="flex h-fit w-fit items-center justify-center rounded-full border-2 bg-yellow-400 text-4xl text-black">
<AlertTriangle />
</p>
Expand All @@ -142,7 +161,13 @@ function OpenSceneDialog() {
<span className="font-bold">replace your existing content.</span>
You can back up your drawing first using one of the options below.
</p>
<Button variant="default" className="w-content bg-yellow-400 text-black">
<Button
onClick={() => {
commandManager.executeCommand(new ActionOpenScene());
setIsOpen(false);
}}
variant="default"
className="w-content bg-yellow-400 text-black">
Load From File
</Button>
</div>
Expand Down Expand Up @@ -195,18 +220,18 @@ function ShortCutsModal() {
<DialogTitle>Keyboard Shortcuts</DialogTitle>
</DialogHeader>
{Object.keys(SHORTCUTS).map((group) => (
<>
<div key={group}>
<h1 className="text-4xl" key={group}>
{group}
</h1>
{Object.keys(SHORTCUTS[group]).map((key) => (
<p>
{Object.keys(SHORTCUTS[group as keyof typeof SHORTCUTS]).map((key) => (
<p key={key}>
{SHORTCUTS[group][key]["description"]}
{" : "}
<span>{SHORTCUTS[group][key]["keys"].join(" + ")}</span>
</p>
))}
</>
</div>
))}
</DialogContent>
</Dialog>
Expand Down
2 changes: 1 addition & 1 deletion src/components/ToolbarElementConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ export default function ToolbarLeft() {
);
} else if (isText(el)) {
els.push(
generateTextElement(ctx, el.text.join("\n"), [el.x, el.y], {
generateTextElement(el.text.join("\n"), [el.x, el.y], {
...el.options,
[k]: value,
}),
Expand Down
9 changes: 4 additions & 5 deletions src/components/ZagyDraw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,11 +175,10 @@ function ZagyDraw() {
if (!event.clipboardData) return;
if (!canvas.current) return;
commandManager.executeCommand(
new ActionImportElements(
event.clipboardData,
[mouseCoords.current[0] / zoomLevel, mouseCoords.current[1] / zoomLevel],
canvas.current,
),
new ActionImportElements(event.clipboardData, [
mouseCoords.current[0] / zoomLevel,
mouseCoords.current[1] / zoomLevel,
]),
);
});

Expand Down
3 changes: 3 additions & 0 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ export const SHORTCUTS = {
text: { description: "create text", keys: ["6"] },
erase: { description: "erase", keys: ["7"] },
},
scene: {
open: { description: "open from file", keys: ["ControlLeft", "o"] },
},
} as const satisfies {
[g: string]: { [k: string]: { description: string; keys: readonly [Keys, ...Keys[]] } };
};
6 changes: 3 additions & 3 deletions src/utils/canvas/generateElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,12 +265,14 @@ const generateCacheLineElement = (
* return text as lines, and calc text element position(width/height) from text string
*/
function textElementHelper(
ctx: CanvasRenderingContext2D,
text: string,
startPos: Point,
fontSize: number,
font: TextOptions["font"],
): { text: string[]; startPos: Point; endPos: Point } {
const tempCanvas = document.createElement("canvas");
const ctx = tempCanvas.getContext("2d");
if (!ctx) throw new Error("GENERATE TEXT: must have ctx to be able to create new text");
const lines = text.split("\n");
// text element width is the largest line width
let largestLineIndex = 0;
Expand Down Expand Up @@ -298,14 +300,12 @@ function textElementHelper(
}

const generateTextElement = (
ctx: CanvasRenderingContext2D,
text: string,
startPos: [number, number],
options: Partial<TextOptions & { id: string }> = {},
): ZagyCanvasTextElement => {
const normalizedOptions = normalizeTextOptions(options);
const norm = textElementHelper(
ctx,
text,
startPos,
normalizedOptions.fontSize,
Expand Down
1 change: 0 additions & 1 deletion src/utils/canvas/renderElements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ function renderTextElement(el: ZagyCanvasTextElement, ctx: CanvasRenderingContex
ctx.save();

ctx.font = `${el.options.fontSize}px ` + FontTypeOptions[el.options.font];
console.log("re", `${el.options.fontSize}px ` + FontTypeOptions[el.options.font]);
ctx.fillStyle = el.options.stroke;
ctx.textBaseline = "top";
el.text.forEach((val, i) => ctx.fillText(val, el.x, el.y + i * el.options.fontSize));
Expand Down

0 comments on commit 866511e

Please sign in to comment.