Skip to content

Commit

Permalink
copy paste (#175)
Browse files Browse the repository at this point in the history
  • Loading branch information
Brendonovich authored Aug 10, 2023
1 parent dbebb9d commit db9a843
Show file tree
Hide file tree
Showing 17 changed files with 433 additions and 148 deletions.
39 changes: 34 additions & 5 deletions app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,48 @@ import { PrintOutput } from "./components/PrintOutput";
import Settings from "./settings";

function App() {
const ui = createUIStore();
const UI = createUIStore();

onMount(async () => {
const savedProject = localStorage.getItem("project");
if (savedProject)
await core.load(SerializedProject.parse(JSON.parse(savedProject)));

const firstGraph = core.project.graphs.values().next();
if (firstGraph) ui.setCurrentGraph(firstGraph.value);
if (firstGraph) UI.setCurrentGraph(firstGraph.value);
});

onMount(() => {
const ctrlHandlers = (e: KeyboardEvent) => {
if (!e.metaKey && !e.ctrlKey) return;

switch (e.code) {
case "KeyC": {
if (!e.metaKey && !e.ctrlKey) return;
const selectedItem = UI.state.selectedItem;
if (selectedItem === null) return;

UI.copyItem(selectedItem);

break;
}
case "KeyV": {
if (!e.metaKey && !e.ctrlKey) return;

UI.pasteClipboard();
}
}
};

window.addEventListener("keydown", ctrlHandlers);

return () => {
window.removeEventListener("keydown", ctrlHandlers);
};
});

return (
<UIStoreProvider store={ui}>
<UIStoreProvider store={UI}>
<CoreProvider core={core}>
<div
class="w-screen h-screen flex flex-row overflow-hidden select-none"
Expand All @@ -32,10 +61,10 @@ function App() {
>
<div class="flex flex-col bg-neutral-600 w-64 shadow-2xl">
<Settings />
<GraphList onChange={(g) => ui.setCurrentGraph(g)} />
<GraphList onChange={(g) => UI.setCurrentGraph(g)} />
<PrintOutput />
</div>
<Show when={ui.state.currentGraph} fallback="No Graph">
<Show when={UI.state.currentGraph} fallback="No Graph">
{(graph) => <Graph graph={graph()} />}
</Show>
</div>
Expand Down
209 changes: 204 additions & 5 deletions app/src/UIStore.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,38 @@
import { Position, XY, Graph, Node, Pin, CommentBox } from "@macrograph/core";
import {
Position,
XY,
Graph,
Node,
Pin,
CommentBox,
Project,
SerializedNode,
SerializedCommentBox,
SerializedGraph,
SerializedProject,
core,
SerializedConnection,
ExecInput,
serializeConnections,
} from "@macrograph/core";
import { createMutable } from "solid-js/store";
import { createContext, createEffect, ParentProps, useContext } from "solid-js";
import {
$TRACK,
createContext,
createEffect,
onMount,
ParentProps,
useContext,
} from "solid-js";
import { ReactiveWeakMap } from "@solid-primitives/map";
import { z } from "zod";

export function createUIStore() {
const state = createMutable({
selectedItem: null as Node | CommentBox | null,
draggingPin: null as Pin | null,
hoveringPin: null as Pin | null,
mousePos: null as XY | null,
mouseDragLocation: null as XY | null,
/**
* Screen space relative to graph origin
Expand All @@ -17,7 +42,7 @@ export function createUIStore() {
mouseDownTranslate: null as XY | null,

currentGraph: null as Graph | null,
nodeBounds: new Map<Node, { width: number; height: number }>(),
nodeBounds: new WeakMap<Node, { width: number; height: number }>(),

graphOffset: {
x: 0,
Expand All @@ -32,10 +57,25 @@ export function createUIStore() {
pinPositions: new ReactiveWeakMap<Pin, XY>(),
});

onMount(() => {
const handler = (e: MouseEvent) => {
state.mousePos = {
x: e.clientX,
y: e.clientY,
};
};

window.addEventListener("mousemove", handler);
return () => window.removeEventListener("mousemove", handler);
});

createEffect(() => {
state.currentGraph;
if (!state.currentGraph) return;

const project = state.currentGraph?.project;
if (!project) return;

state.nodeBounds = new Map();
if (project.graphs.size === 0) state.currentGraph = null;
});

return {
Expand Down Expand Up @@ -109,9 +149,168 @@ export function createUIStore() {
this.setTranslate({ x: 0, y: 0 });
state.currentGraph = graph;
},
copyItem(item: Node | CommentBox | Graph | Project) {
let data: z.infer<typeof CopyItem>;

if (item instanceof Node)
data = {
type: "node",
node: item.serialize(),
};
else if (item instanceof CommentBox) {
const nodes = item.getNodes(
item.graph.nodes.values(),
(node) => state.nodeBounds.get(node) ?? null
);

const connections = serializeConnections(nodes.values());

data = {
type: "commentBox",
commentBox: item.serialize(),
nodes: nodes.map((n) => n.serialize()),
connections,
};
} else if (item instanceof Graph)
data = {
type: "graph",
graph: item.serialize(),
};
else if (item instanceof Project)
data = {
type: "project",
project: item.serialize(),
};
// impossible
else return;

const string = JSON.stringify(data);

navigator.clipboard.writeText(btoa(string));
},
async pasteClipboard() {
const text = await navigator.clipboard.readText();

const json = JSON.parse(atob(text));

const item = CopyItem.parse(json);

switch (item.type) {
case "node": {
if (!state.currentGraph) return;

if (!state.mousePos) throw new Error("Mouse position not set");

item.node.id = state.currentGraph.generateNodeId();

const node = Node.deserialize(state.currentGraph, {
...item.node,
position: this.toGraphSpace({
x: state.mousePos.x - 10 - state.graphOffset.x,
y: state.mousePos.y - 10 - state.graphOffset.y,
}),
});

if (!node) throw new Error("Failed to deserialize node");

state.currentGraph.nodes.set(item.node.id, node);

break;
}
case "commentBox": {
if (!state.currentGraph) return;

if (!state.mousePos) throw new Error("Mouse position not set");

const commentBox = CommentBox.deserialize(state.currentGraph, {
...item.commentBox,
position: this.toGraphSpace({
x: state.mousePos.x - 10 - state.graphOffset.x,
y: state.mousePos.y - 10 - state.graphOffset.y,
}),
});

if (!commentBox) throw new Error("Failed to deserialize comment box");
state.currentGraph.commentBoxes.add(commentBox);

const nodeIdMap = new Map<number, number>();

for (const nodeJson of item.nodes) {
const id = state.currentGraph.generateNodeId();
nodeIdMap.set(nodeJson.id, id);
nodeJson.id = id;

const node = Node.deserialize(state.currentGraph, {
...nodeJson,
position: {
x:
commentBox.position.x +
nodeJson.position.x -
item.commentBox.position.x,
y:
commentBox.position.y +
nodeJson.position.y -
item.commentBox.position.y,
},
});

if (!node) throw new Error("Failed to deserialize node");

state.currentGraph.nodes.set(node.id, node);
}

state.currentGraph.deserializeConnections(item.connections, {
nodeIdMap,
});

break;
}
case "graph": {
item.graph.id = core.project.generateGraphId();

const graph = await Graph.deserialize(core.project, item.graph);

if (!graph) throw new Error("Failed to deserialize graph");

core.project.graphs.set(graph.id, graph);

break;
}
case "project": {
const project = await Project.deserialize(core, item.project);

if (!project) throw new Error("Failed to deserialize project");

core.project = project;

break;
}
}
},
};
}

const CopyItem = z.discriminatedUnion("type", [
z.object({
type: z.literal("node"),
node: SerializedNode,
}),
z.object({
type: z.literal("commentBox"),
commentBox: SerializedCommentBox,
nodes: z.array(SerializedNode),
connections: z.array(SerializedConnection),
}),
z.object({
type: z.literal("graph"),
graph: SerializedGraph,
}),
z.object({
type: z.literal("project"),
project: SerializedProject,
}),
]);

export type UIStore = ReturnType<typeof createUIStore>;

const UIStoreContext = createContext<UIStore>(null as any);
Expand Down
33 changes: 7 additions & 26 deletions app/src/components/Graph/CommentBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,18 @@ import { CommentBox } from "@macrograph/core";
import clsx from "clsx";
import { createEffect, createSignal, onCleanup, onMount, Show } from "solid-js";
import { useUIStore } from "~/UIStore";
import { useGraph } from "./Graph";

interface Props {
box: CommentBox;
}

export default (props: Props) => {
const box = () => props.box;
const graph = () => props.box.graph;
const position = () => props.box.position;
const size = () => props.box.size;

const UI = useUIStore();
const graph = useGraph();

const [editing, setEditing] = createSignal(false);

Expand Down Expand Up @@ -46,29 +45,6 @@ export default (props: Props) => {
case 0: {
UI.setSelectedItem(props.box);

const innerNodes = [...graph().nodes.values()].filter(
(node) => {
if (
node.position.x < position().x ||
node.position.y < position().y
)
return false;

const nodeSize = UI.state.nodeBounds.get(node);
if (!nodeSize) return false;

if (
node.position.x + nodeSize.width >
position().x + size().x ||
node.position.y + nodeSize.height >
position().y + size().y
)
return false;

return true;
}
);

const handleMouseMove = (e: MouseEvent) => {
const scale = UI.state.scale;

Expand All @@ -77,7 +53,12 @@ export default (props: Props) => {
y: box().position.y + e.movementY / scale,
};

innerNodes.forEach((node) => {
const nodes = box().getNodes(
graph().nodes.values(),
(node) => UI.state.nodeBounds.get(node) ?? null
);

nodes.forEach((node) => {
node.position = {
x: node.position.x + e.movementX / scale,
y: node.position.y + e.movementY / scale,
Expand Down
Loading

0 comments on commit db9a843

Please sign in to comment.