All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m59s
263 lines
7.1 KiB
Svelte
263 lines
7.1 KiB
Svelte
<script lang="ts">
|
|
import type { Edge, NodeInstance } from "@nodarium/types";
|
|
import { createKeyMap } from "../../helpers/createKeyMap";
|
|
import AddMenu from "../components/AddMenu.svelte";
|
|
import Background from "../background/Background.svelte";
|
|
import BoxSelection from "../components/BoxSelection.svelte";
|
|
import EdgeEl from "../edges/Edge.svelte";
|
|
import NodeEl from "../node/Node.svelte";
|
|
import Camera from "../components/Camera.svelte";
|
|
import { Canvas } from "@threlte/core";
|
|
import HelpView from "../components/HelpView.svelte";
|
|
import { getGraphManager, getGraphState } from "../graph-state.svelte";
|
|
import { HTML } from "@threlte/extras";
|
|
import { FileDropEventManager, MouseEventManager } from "./events";
|
|
import { maxZoom, minZoom } from "./constants";
|
|
|
|
const {
|
|
keymap,
|
|
}: {
|
|
keymap: ReturnType<typeof createKeyMap>;
|
|
} = $props();
|
|
|
|
const graph = getGraphManager();
|
|
const graphState = getGraphState();
|
|
const fileDropEvents = new FileDropEventManager(graph, graphState);
|
|
const mouseEvents = new MouseEventManager(graph, graphState);
|
|
|
|
function getEdgePosition(edge: Edge) {
|
|
const fromNode = graph.nodes.get(edge[0].id);
|
|
const toNode = graph.nodes.get(edge[2].id);
|
|
|
|
// This check is important because nodes might not be there during some transitions.
|
|
if (!fromNode || !toNode) {
|
|
return [0, 0, 0, 0];
|
|
}
|
|
|
|
const pos1 = graphState.getSocketPosition(fromNode, edge[1]);
|
|
const pos2 = graphState.getSocketPosition(toNode, edge[3]);
|
|
return [pos1[0], pos1[1], pos2[0], pos2[1]];
|
|
}
|
|
|
|
function handleNodeCreation(node: NodeInstance) {
|
|
const newNode = graph.createNode({
|
|
type: node.type,
|
|
position: node.position,
|
|
props: node.props,
|
|
});
|
|
if (!newNode) return;
|
|
|
|
if (graphState.activeSocket) {
|
|
if (typeof graphState.activeSocket.index === "number") {
|
|
const socketType =
|
|
graphState.activeSocket.node.state?.type?.outputs?.[
|
|
graphState.activeSocket.index
|
|
];
|
|
|
|
const input = Object.entries(newNode?.state?.type?.inputs || {}).find(
|
|
(inp) => inp[1].type === socketType,
|
|
);
|
|
|
|
if (input) {
|
|
graph.createEdge(
|
|
graphState.activeSocket.node,
|
|
graphState.activeSocket.index,
|
|
newNode,
|
|
input[0],
|
|
);
|
|
}
|
|
} else {
|
|
const socketType =
|
|
graphState.activeSocket.node.state?.type?.inputs?.[
|
|
graphState.activeSocket.index
|
|
];
|
|
|
|
const output = newNode.state?.type?.outputs?.find((out) => {
|
|
if (socketType?.type === out) return true;
|
|
if (socketType?.accepts?.includes(out as any)) return true;
|
|
return false;
|
|
});
|
|
|
|
if (output) {
|
|
graph.createEdge(
|
|
newNode,
|
|
output.indexOf(output),
|
|
graphState.activeSocket.node,
|
|
graphState.activeSocket.index,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
graphState.activeSocket = null;
|
|
graphState.addMenuPosition = null;
|
|
}
|
|
</script>
|
|
|
|
<svelte:window
|
|
onmousemove={(ev) => mouseEvents.handleMouseMove(ev)}
|
|
onmouseup={(ev) => mouseEvents.handleMouseUp(ev)}
|
|
/>
|
|
|
|
<div
|
|
onwheel={(ev) => mouseEvents.handleMouseScroll(ev)}
|
|
bind:this={graphState.wrapper}
|
|
class="graph-wrapper"
|
|
class:is-panning={graphState.isPanning}
|
|
class:is-hovering={graphState.hoveredNodeId !== -1}
|
|
aria-label="Graph"
|
|
role="button"
|
|
tabindex="0"
|
|
bind:clientWidth={graphState.width}
|
|
bind:clientHeight={graphState.height}
|
|
onkeydown={(ev) => keymap.handleKeyboardEvent(ev)}
|
|
onmousedown={(ev) => mouseEvents.handleMouseDown(ev)}
|
|
{...fileDropEvents.getEventListenerProps()}
|
|
>
|
|
<input
|
|
type="file"
|
|
accept="application/wasm,application/json"
|
|
id="drop-zone"
|
|
disabled={!graphState.isDragging}
|
|
ondragend={(ev) => fileDropEvents.handleDragEnd(ev)}
|
|
ondragleave={(ev) => fileDropEvents.handleDragEnd(ev)}
|
|
/>
|
|
<label for="drop-zone"></label>
|
|
|
|
<Canvas shadows={false} renderMode="on-demand" colorManagementEnabled={false}>
|
|
<Camera
|
|
bind:camera={graphState.camera}
|
|
position={graphState.cameraPosition}
|
|
/>
|
|
|
|
{#if graphState.showGrid !== false}
|
|
<Background
|
|
cameraPosition={graphState.cameraPosition}
|
|
{maxZoom}
|
|
{minZoom}
|
|
width={graphState.width}
|
|
height={graphState.height}
|
|
/>
|
|
{/if}
|
|
|
|
{#if graphState.boxSelection && graphState.mouseDown}
|
|
<BoxSelection
|
|
cameraPosition={graphState.cameraPosition}
|
|
p1={{
|
|
x:
|
|
graphState.cameraPosition[0] +
|
|
(graphState.mouseDown[0] - graphState.width / 2) /
|
|
graphState.cameraPosition[2],
|
|
y:
|
|
graphState.cameraPosition[1] +
|
|
(graphState.mouseDown[1] - graphState.height / 2) /
|
|
graphState.cameraPosition[2],
|
|
}}
|
|
p2={{ x: graphState.mousePosition[0], y: graphState.mousePosition[1] }}
|
|
/>
|
|
{/if}
|
|
|
|
{#if graph.status === "idle"}
|
|
{#if graphState.addMenuPosition}
|
|
<AddMenu onnode={handleNodeCreation} />
|
|
{/if}
|
|
|
|
{#if graphState.activeSocket}
|
|
<EdgeEl
|
|
z={graphState.cameraPosition[2]}
|
|
x1={graphState.activeSocket.position[0]}
|
|
y1={graphState.activeSocket.position[1]}
|
|
x2={graphState.edgeEndPosition?.[0] ?? graphState.mousePosition[0]}
|
|
y2={graphState.edgeEndPosition?.[1] ?? graphState.mousePosition[1]}
|
|
/>
|
|
{/if}
|
|
|
|
{#each graph.edges as edge}
|
|
{@const [x1, y1, x2, y2] = getEdgePosition(edge)}
|
|
<EdgeEl z={graphState.cameraPosition[2]} {x1} {y1} {x2} {y2} />
|
|
{/each}
|
|
|
|
<HTML transform={false}>
|
|
<div
|
|
role="tree"
|
|
id="graph"
|
|
tabindex="0"
|
|
class="wrapper"
|
|
style:transform={`scale(${graphState.cameraPosition[2] * 0.1})`}
|
|
class:hovering-sockets={graphState.activeSocket}
|
|
>
|
|
{#each graph.nodes.values() as node (node.id)}
|
|
<NodeEl
|
|
{node}
|
|
inView={graphState.isNodeInView(node)}
|
|
z={graphState.cameraPosition[2]}
|
|
/>
|
|
{/each}
|
|
</div>
|
|
</HTML>
|
|
{:else if graph.status === "loading"}
|
|
<span>Loading</span>
|
|
{:else if graph.status === "error"}
|
|
<span>Error</span>
|
|
{/if}
|
|
</Canvas>
|
|
</div>
|
|
|
|
{#if graphState.showHelp}
|
|
<HelpView registry={graph.registry} />
|
|
{/if}
|
|
|
|
<style>
|
|
.graph-wrapper {
|
|
position: relative;
|
|
z-index: 0;
|
|
transition: opacity 0.3s ease;
|
|
height: 100%;
|
|
}
|
|
|
|
.wrapper {
|
|
position: absolute;
|
|
z-index: 100;
|
|
width: 0px;
|
|
height: 0px;
|
|
}
|
|
|
|
.is-hovering {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.is-panning {
|
|
cursor: grab;
|
|
}
|
|
|
|
input {
|
|
position: absolute;
|
|
z-index: 1;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: var(--layer-2);
|
|
opacity: 0;
|
|
}
|
|
input:disabled {
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
input:disabled + label {
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
label {
|
|
position: absolute;
|
|
z-index: 1;
|
|
top: 10px;
|
|
left: 10px;
|
|
border-radius: 5px;
|
|
width: calc(100% - 20px);
|
|
height: calc(100% - 25px);
|
|
border: dashed 4px var(--layer-2);
|
|
background: var(--layer-1);
|
|
opacity: 0.5;
|
|
}
|
|
</style>
|