feat: extract graph-interface into seperate package
This commit is contained in:
parent
404fcbfe39
commit
2ed1501747
@ -13,7 +13,8 @@
|
|||||||
"story:preview": "histoire preview"
|
"story:preview": "histoire preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodes/input-elements": "link:../packages/input-elements",
|
"@nodes/graph-interface": "link:../packages/graph-interface",
|
||||||
|
"@nodes/ui": "link:../packages/ui",
|
||||||
"@sveltejs/kit": "^2.5.0",
|
"@sveltejs/kit": "^2.5.0",
|
||||||
"@tauri-apps/api": "2.0.0-beta.2",
|
"@tauri-apps/api": "2.0.0-beta.2",
|
||||||
"@tauri-apps/plugin-shell": "^2.0.0-beta.0",
|
"@tauri-apps/plugin-shell": "^2.0.0-beta.0",
|
||||||
@ -21,12 +22,7 @@
|
|||||||
"@threlte/extras": "^8.7.5",
|
"@threlte/extras": "^8.7.5",
|
||||||
"@threlte/flex": "^1.0.1",
|
"@threlte/flex": "^1.0.1",
|
||||||
"@types/three": "^0.159.0",
|
"@types/three": "^0.159.0",
|
||||||
"input-elements": "link:../packages/input-elements",
|
"three": "^0.159.0"
|
||||||
"jsondiffpatch": "^0.6.0",
|
|
||||||
"meshline": "^3.2.0",
|
|
||||||
"plantarium-nodes-math": "link:../nodes/max/plantarium/math/pkg",
|
|
||||||
"three": "^0.159.0",
|
|
||||||
"three.meshline": "^1.4.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@histoire/plugin-svelte": "^0.17.9",
|
"@histoire/plugin-svelte": "^0.17.9",
|
||||||
|
@ -1,164 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { GraphManager } from "$lib/graph-manager";
|
|
||||||
import { HTML } from "@threlte/extras";
|
|
||||||
import { onMount } from "svelte";
|
|
||||||
|
|
||||||
export let position: [x: number, y: number] | null;
|
|
||||||
|
|
||||||
export let graph: GraphManager;
|
|
||||||
|
|
||||||
let input: HTMLInputElement;
|
|
||||||
let value: string = "";
|
|
||||||
let activeNodeId: string = "";
|
|
||||||
|
|
||||||
const allNodes = graph.getNodeTypes();
|
|
||||||
|
|
||||||
function filterNodes() {
|
|
||||||
return allNodes.filter((node) => node.id.includes(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
$: nodes = value === "" ? allNodes : filterNodes();
|
|
||||||
$: if (nodes) {
|
|
||||||
if (activeNodeId === "") {
|
|
||||||
activeNodeId = nodes[0].id;
|
|
||||||
} else if (nodes.length) {
|
|
||||||
const node = nodes.find((node) => node.id === activeNodeId);
|
|
||||||
if (!node) {
|
|
||||||
activeNodeId = nodes[0].id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleKeyDown(event: KeyboardEvent) {
|
|
||||||
event.stopImmediatePropagation();
|
|
||||||
const value = (event.target as HTMLInputElement).value;
|
|
||||||
|
|
||||||
if (event.key === "Escape") {
|
|
||||||
position = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === "ArrowDown") {
|
|
||||||
const index = nodes.findIndex((node) => node.id === activeNodeId);
|
|
||||||
activeNodeId = nodes[(index + 1) % nodes.length].id;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === "ArrowUp") {
|
|
||||||
const index = nodes.findIndex((node) => node.id === activeNodeId);
|
|
||||||
activeNodeId = nodes[(index - 1 + nodes.length) % nodes.length].id;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === "Enter") {
|
|
||||||
if (activeNodeId && position) {
|
|
||||||
graph.createNode({ type: activeNodeId, position });
|
|
||||||
position = null;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
input.disabled = false;
|
|
||||||
setTimeout(() => input.focus(), 50);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<HTML position.x={position?.[0]} position.z={position?.[1]} transform={false}>
|
|
||||||
<div class="wrapper">
|
|
||||||
<div class="header">
|
|
||||||
<input
|
|
||||||
id="add-menu"
|
|
||||||
type="text"
|
|
||||||
aria-label="Search for a node type"
|
|
||||||
role="searchbox"
|
|
||||||
placeholder="Search..."
|
|
||||||
disabled={false}
|
|
||||||
on:keydown={handleKeyDown}
|
|
||||||
bind:value
|
|
||||||
bind:this={input}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content">
|
|
||||||
{#each nodes as node}
|
|
||||||
<div
|
|
||||||
class="result"
|
|
||||||
role="treeitem"
|
|
||||||
tabindex="0"
|
|
||||||
aria-selected={node.id === activeNodeId}
|
|
||||||
on:keydown={(event) => {
|
|
||||||
if (event.key === "Enter") {
|
|
||||||
if (position) {
|
|
||||||
graph.createNode({ type: node.id, position });
|
|
||||||
position = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
on:mousedown={() => {
|
|
||||||
if (position) {
|
|
||||||
graph.createNode({ type: node.id, position });
|
|
||||||
position = null;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
on:focus={() => {
|
|
||||||
activeNodeId = node.id;
|
|
||||||
}}
|
|
||||||
class:selected={node.id === activeNodeId}
|
|
||||||
on:mouseover={() => {
|
|
||||||
activeNodeId = node.id;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{node.id}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</HTML>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
input {
|
|
||||||
background: var(--background-color-lighter);
|
|
||||||
font-family: var(--font-family);
|
|
||||||
border: none;
|
|
||||||
color: var(--text-color);
|
|
||||||
padding: 0.8em;
|
|
||||||
width: calc(100% - 2px);
|
|
||||||
box-sizing: border-box;
|
|
||||||
font-size: 1em;
|
|
||||||
margin-left: 1px;
|
|
||||||
margin-top: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:focus {
|
|
||||||
outline: solid 2px rgba(255, 255, 255, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapper {
|
|
||||||
position: absolute;
|
|
||||||
background: var(--background-color);
|
|
||||||
border-radius: 7px;
|
|
||||||
overflow: hidden;
|
|
||||||
border: solid 2px var(--background-color-lighter);
|
|
||||||
width: 150px;
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
min-height: none;
|
|
||||||
width: 100%;
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.result {
|
|
||||||
padding: 1em 0.9em;
|
|
||||||
border-bottom: solid 1px var(--background-color-lighter);
|
|
||||||
opacity: 0.7;
|
|
||||||
font-size: 0.9em;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result[aria-selected="true"] {
|
|
||||||
background: var(--background-color-lighter);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,31 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { HTML } from "@threlte/extras";
|
|
||||||
|
|
||||||
export let p1 = { x: 0, y: 0 };
|
|
||||||
export let p2 = { x: 0, y: 0 };
|
|
||||||
|
|
||||||
export let cameraPosition = [0, 1, 0];
|
|
||||||
|
|
||||||
$: width = Math.abs(p1.x - p2.x) * cameraPosition[2];
|
|
||||||
$: height = Math.abs(p1.y - p2.y) * cameraPosition[2];
|
|
||||||
|
|
||||||
$: x = Math.max(p1.x, p2.x) - width / cameraPosition[2];
|
|
||||||
$: y = Math.max(p1.y, p2.y) - height / cameraPosition[2];
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<HTML position.x={x} position.z={y} transform={false}>
|
|
||||||
<div
|
|
||||||
class="selection"
|
|
||||||
style={`width: ${width}px; height: ${height}px;`}
|
|
||||||
></div>
|
|
||||||
</HTML>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.selection {
|
|
||||||
width: 40px;
|
|
||||||
height: 20px;
|
|
||||||
border: solid 0.2px rgba(200, 200, 200, 0.8);
|
|
||||||
border-style: dashed;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,56 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { T } from "@threlte/core";
|
|
||||||
|
|
||||||
import BackgroundVert from "./Background.vert";
|
|
||||||
import BackgroundFrag from "./Background.frag";
|
|
||||||
import { colors } from "../graph/stores";
|
|
||||||
|
|
||||||
export let minZoom = 4;
|
|
||||||
export let maxZoom = 150;
|
|
||||||
|
|
||||||
export let cameraPosition: [number, number, number] = [0, 1, 0];
|
|
||||||
|
|
||||||
export let width = globalThis?.innerWidth || 100;
|
|
||||||
export let height = globalThis?.innerHeight || 100;
|
|
||||||
|
|
||||||
let bw = 2;
|
|
||||||
let bh = 2;
|
|
||||||
|
|
||||||
$: if (width && height) {
|
|
||||||
bw = width / cameraPosition[2];
|
|
||||||
bh = height / cameraPosition[2];
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<T.Group
|
|
||||||
position.x={cameraPosition[0]}
|
|
||||||
position.z={cameraPosition[1]}
|
|
||||||
position.y={-1.0}
|
|
||||||
>
|
|
||||||
<T.Mesh rotation.x={-Math.PI / 2} position.y={0.2} scale.x={bw} scale.y={bh}>
|
|
||||||
<T.PlaneGeometry args={[1, 1]} />
|
|
||||||
<T.ShaderMaterial
|
|
||||||
transparent
|
|
||||||
vertexShader={BackgroundVert}
|
|
||||||
fragmentShader={BackgroundFrag}
|
|
||||||
uniforms={{
|
|
||||||
camPos: {
|
|
||||||
value: [0, 1, 0],
|
|
||||||
},
|
|
||||||
backgroundColor: {
|
|
||||||
value: [0, 0, 0],
|
|
||||||
},
|
|
||||||
zoomLimits: {
|
|
||||||
value: [2, 50],
|
|
||||||
},
|
|
||||||
dimensions: {
|
|
||||||
value: [100, 100],
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
uniforms.camPos.value={cameraPosition}
|
|
||||||
uniforms.backgroundColor.value={$colors.backgroundColorDarker}
|
|
||||||
uniforms.zoomLimits.value={[minZoom, maxZoom]}
|
|
||||||
uniforms.dimensions.value={[width, height]}
|
|
||||||
/>
|
|
||||||
</T.Mesh>
|
|
||||||
</T.Group>
|
|
@ -1,24 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { MeshLineGeometry, MeshLineMaterial } from "@threlte/extras";
|
|
||||||
import { points, lines } from "./store";
|
|
||||||
import { T } from "@threlte/core";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#each $points as point}
|
|
||||||
<T.Mesh
|
|
||||||
position.x={point.x}
|
|
||||||
position.y={point.y}
|
|
||||||
position.z={point.z}
|
|
||||||
rotation.x={-Math.PI / 2}
|
|
||||||
>
|
|
||||||
<T.CircleGeometry args={[0.2, 32]} />
|
|
||||||
<T.MeshBasicMaterial color="red" />
|
|
||||||
</T.Mesh>
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
{#each $lines as line}
|
|
||||||
<T.Mesh>
|
|
||||||
<MeshLineGeometry points={line} />
|
|
||||||
<MeshLineMaterial color="red" linewidth={1} attenuate={false} />
|
|
||||||
</T.Mesh>
|
|
||||||
{/each}
|
|
@ -1,799 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { animate, lerp, snapToGrid } from "$lib/helpers";
|
|
||||||
import { LinearSRGBColorSpace } from "three";
|
|
||||||
import { Canvas } from "@threlte/core";
|
|
||||||
import type { OrthographicCamera } from "three";
|
|
||||||
import Background from "../background/Background.svelte";
|
|
||||||
import type { GraphManager } from "$lib/graph-manager";
|
|
||||||
import { onMount, setContext } from "svelte";
|
|
||||||
import Camera from "../Camera.svelte";
|
|
||||||
import GraphView from "./GraphView.svelte";
|
|
||||||
import type { Node, Node as NodeType, Socket } from "@nodes/types";
|
|
||||||
import FloatingEdge from "../edges/FloatingEdge.svelte";
|
|
||||||
import {
|
|
||||||
activeNodeId,
|
|
||||||
activeSocket,
|
|
||||||
hoveredSocket,
|
|
||||||
possibleSockets,
|
|
||||||
possibleSocketIds,
|
|
||||||
selectedNodes,
|
|
||||||
} from "./stores";
|
|
||||||
import BoxSelection from "../BoxSelection.svelte";
|
|
||||||
import AddMenu from "../AddMenu.svelte";
|
|
||||||
|
|
||||||
export let graph: GraphManager;
|
|
||||||
setContext("graphManager", graph);
|
|
||||||
const status = graph.status;
|
|
||||||
const nodes = graph.nodes;
|
|
||||||
const edges = graph.edges;
|
|
||||||
const graphId = graph.id;
|
|
||||||
|
|
||||||
let wrapper: HTMLDivElement;
|
|
||||||
$: rect =
|
|
||||||
wrapper && width
|
|
||||||
? wrapper.getBoundingClientRect()
|
|
||||||
: { x: 0, y: 0, width: 0, height: 0 };
|
|
||||||
|
|
||||||
let camera: OrthographicCamera;
|
|
||||||
const minZoom = 1;
|
|
||||||
const maxZoom = 40;
|
|
||||||
let mousePosition = [0, 0];
|
|
||||||
let mouseDown: null | [number, number] = null;
|
|
||||||
let mouseDownId = -1;
|
|
||||||
let boxSelection = false;
|
|
||||||
let loaded = false;
|
|
||||||
const cameraDown = [0, 0];
|
|
||||||
let cameraPosition: [number, number, number] = [0, 0, 4];
|
|
||||||
let addMenuPosition: [number, number] | null = null;
|
|
||||||
let clipboard: null | {
|
|
||||||
nodes: Node[];
|
|
||||||
edges: [number, number, number, string][];
|
|
||||||
} = null;
|
|
||||||
|
|
||||||
$: if (cameraPosition && loaded) {
|
|
||||||
localStorage.setItem("cameraPosition", JSON.stringify(cameraPosition));
|
|
||||||
}
|
|
||||||
|
|
||||||
let width = globalThis?.innerWidth ?? 100;
|
|
||||||
let height = globalThis?.innerHeight ?? 100;
|
|
||||||
|
|
||||||
let cameraBounds = [-1000, 1000, -1000, 1000];
|
|
||||||
$: cameraBounds = [
|
|
||||||
cameraPosition[0] - width / cameraPosition[2] / 2,
|
|
||||||
cameraPosition[0] + width / cameraPosition[2] / 2,
|
|
||||||
cameraPosition[1] - height / cameraPosition[2] / 2,
|
|
||||||
cameraPosition[1] + height / cameraPosition[2] / 2,
|
|
||||||
];
|
|
||||||
function setCameraTransform(x: number, y: number, z: number) {
|
|
||||||
if (!camera) return;
|
|
||||||
camera.position.x = x;
|
|
||||||
camera.position.z = y;
|
|
||||||
camera.zoom = z;
|
|
||||||
cameraPosition = [x, y, z];
|
|
||||||
}
|
|
||||||
|
|
||||||
export let debug = {};
|
|
||||||
$: debug = {
|
|
||||||
activeNodeId: $activeNodeId,
|
|
||||||
activeSocket: $activeSocket
|
|
||||||
? `${$activeSocket?.node.id}-${$activeSocket?.index}`
|
|
||||||
: null,
|
|
||||||
hoveredSocket: $hoveredSocket
|
|
||||||
? `${$hoveredSocket?.node.id}-${$hoveredSocket?.index}`
|
|
||||||
: null,
|
|
||||||
selectedNodes: [...($selectedNodes?.values() || [])],
|
|
||||||
cameraPosition,
|
|
||||||
};
|
|
||||||
|
|
||||||
function updateNodePosition(node: NodeType) {
|
|
||||||
if (node?.tmp?.ref) {
|
|
||||||
if (node.tmp["x"] !== undefined && node.tmp["y"] !== undefined) {
|
|
||||||
node.tmp.ref.style.setProperty("--nx", `${node.tmp.x * 10}px`);
|
|
||||||
node.tmp.ref.style.setProperty("--ny", `${node.tmp.y * 10}px`);
|
|
||||||
node.tmp.mesh.position.x = node.tmp.x + 10;
|
|
||||||
node.tmp.mesh.position.z = node.tmp.y + getNodeHeight(node.type) / 2;
|
|
||||||
if (
|
|
||||||
node.tmp.x === node.position[0] &&
|
|
||||||
node.tmp.y === node.position[1]
|
|
||||||
) {
|
|
||||||
delete node.tmp.x;
|
|
||||||
delete node.tmp.y;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
node.tmp.ref.style.setProperty("--nx", `${node.position[0] * 10}px`);
|
|
||||||
node.tmp.ref.style.setProperty("--ny", `${node.position[1] * 10}px`);
|
|
||||||
node.tmp.mesh.position.x = node.position[0] + 10;
|
|
||||||
node.tmp.mesh.position.z =
|
|
||||||
node.position[1] + getNodeHeight(node.type) / 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setContext("updateNodePosition", updateNodePosition);
|
|
||||||
|
|
||||||
const nodeHeightCache: Record<string, number> = {};
|
|
||||||
function getNodeHeight(nodeTypeId: string) {
|
|
||||||
if (nodeTypeId in nodeHeightCache) {
|
|
||||||
return nodeHeightCache[nodeTypeId];
|
|
||||||
}
|
|
||||||
const node = graph.getNodeType(nodeTypeId);
|
|
||||||
if (!node?.inputs) {
|
|
||||||
return 5;
|
|
||||||
}
|
|
||||||
const height =
|
|
||||||
5 + 10 * Object.keys(node.inputs).filter((i) => i !== "seed").length;
|
|
||||||
nodeHeightCache[nodeTypeId] = height;
|
|
||||||
return height;
|
|
||||||
}
|
|
||||||
setContext("getNodeHeight", getNodeHeight);
|
|
||||||
|
|
||||||
setContext("isNodeInView", (node: NodeType) => {
|
|
||||||
const height = getNodeHeight(node.type);
|
|
||||||
const width = 20;
|
|
||||||
return (
|
|
||||||
node.position[0] > cameraBounds[0] - width &&
|
|
||||||
node.position[0] < cameraBounds[1] &&
|
|
||||||
node.position[1] > cameraBounds[2] - height &&
|
|
||||||
node.position[1] < cameraBounds[3]
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
function getNodeIdFromEvent(event: MouseEvent) {
|
|
||||||
let clickedNodeId = -1;
|
|
||||||
|
|
||||||
let mx = event.clientX - rect.x;
|
|
||||||
let my = event.clientY - rect.y;
|
|
||||||
|
|
||||||
if (event.button === 0) {
|
|
||||||
// check if the clicked element is a node
|
|
||||||
if (event.target instanceof HTMLElement) {
|
|
||||||
const nodeElement = event.target.closest(".node");
|
|
||||||
const nodeId = nodeElement?.getAttribute?.("data-node-id");
|
|
||||||
if (nodeId) {
|
|
||||||
clickedNodeId = parseInt(nodeId, 10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we do not have an active node,
|
|
||||||
// we are going to check if we clicked on a node by coordinates
|
|
||||||
if (clickedNodeId === -1) {
|
|
||||||
const [downX, downY] = projectScreenToWorld(mx, my);
|
|
||||||
for (const node of $nodes.values()) {
|
|
||||||
const x = node.position[0];
|
|
||||||
const y = node.position[1];
|
|
||||||
const height = getNodeHeight(node.type);
|
|
||||||
if (downX > x && downX < x + 20 && downY > y && downY < y + height) {
|
|
||||||
clickedNodeId = node.id;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return clickedNodeId;
|
|
||||||
}
|
|
||||||
|
|
||||||
setContext("setDownSocket", (socket: Socket) => {
|
|
||||||
$activeSocket = socket;
|
|
||||||
|
|
||||||
let { node, index, position } = socket;
|
|
||||||
|
|
||||||
// remove existing edge
|
|
||||||
if (typeof index === "string") {
|
|
||||||
const edges = graph.getEdgesToNode(node);
|
|
||||||
for (const edge of edges) {
|
|
||||||
if (edge[3] === index) {
|
|
||||||
node = edge[0];
|
|
||||||
index = edge[1];
|
|
||||||
position = getSocketPosition(node, index);
|
|
||||||
graph.removeEdge(edge);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mouseDown = position;
|
|
||||||
$activeSocket = {
|
|
||||||
node,
|
|
||||||
index,
|
|
||||||
position,
|
|
||||||
};
|
|
||||||
|
|
||||||
$possibleSockets = graph
|
|
||||||
.getPossibleSockets($activeSocket)
|
|
||||||
.map(([node, index]) => {
|
|
||||||
return {
|
|
||||||
node,
|
|
||||||
index,
|
|
||||||
position: getSocketPosition(node, index),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
$possibleSocketIds = new Set(
|
|
||||||
$possibleSockets.map((s) => `${s.node.id}-${s.index}`),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
function getSnapLevel() {
|
|
||||||
const z = cameraPosition[2];
|
|
||||||
if (z > 66) {
|
|
||||||
return 8;
|
|
||||||
} else if (z > 55) {
|
|
||||||
return 4;
|
|
||||||
} else if (z > 11) {
|
|
||||||
return 2;
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSocketPosition(
|
|
||||||
node: NodeType,
|
|
||||||
index: string | number,
|
|
||||||
): [number, number] {
|
|
||||||
if (typeof index === "number") {
|
|
||||||
return [
|
|
||||||
(node?.tmp?.x ?? node.position[0]) + 20,
|
|
||||||
(node?.tmp?.y ?? node.position[1]) + 2.5 + 10 * index,
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
const _index = Object.keys(node.tmp?.type?.inputs || {}).indexOf(index);
|
|
||||||
return [
|
|
||||||
node?.tmp?.x ?? node.position[0],
|
|
||||||
(node?.tmp?.y ?? node.position[1]) + 10 + 10 * _index,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setContext("getSocketPosition", getSocketPosition);
|
|
||||||
|
|
||||||
function projectScreenToWorld(x: number, y: number): [number, number] {
|
|
||||||
return [
|
|
||||||
cameraPosition[0] + (x - width / 2) / cameraPosition[2],
|
|
||||||
cameraPosition[1] + (y - height / 2) / cameraPosition[2],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMouseMove(event: MouseEvent) {
|
|
||||||
let mx = event.clientX - rect.x;
|
|
||||||
let my = event.clientY - rect.y;
|
|
||||||
|
|
||||||
mousePosition = projectScreenToWorld(mx, my);
|
|
||||||
|
|
||||||
if (!mouseDown) return;
|
|
||||||
|
|
||||||
// we are creating a new edge here
|
|
||||||
if ($possibleSockets?.length) {
|
|
||||||
let smallestDist = 1000;
|
|
||||||
let _socket;
|
|
||||||
for (const socket of $possibleSockets) {
|
|
||||||
const dist = Math.sqrt(
|
|
||||||
(socket.position[0] - mousePosition[0]) ** 2 +
|
|
||||||
(socket.position[1] - mousePosition[1]) ** 2,
|
|
||||||
);
|
|
||||||
if (dist < smallestDist) {
|
|
||||||
smallestDist = dist;
|
|
||||||
_socket = socket;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_socket && smallestDist < 0.9) {
|
|
||||||
mousePosition = _socket.position;
|
|
||||||
$hoveredSocket = _socket;
|
|
||||||
} else {
|
|
||||||
$hoveredSocket = null;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle box selection
|
|
||||||
if (boxSelection) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
const mouseD = projectScreenToWorld(mouseDown[0], mouseDown[1]);
|
|
||||||
const x1 = Math.min(mouseD[0], mousePosition[0]);
|
|
||||||
const x2 = Math.max(mouseD[0], mousePosition[0]);
|
|
||||||
const y1 = Math.min(mouseD[1], mousePosition[1]);
|
|
||||||
const y2 = Math.max(mouseD[1], mousePosition[1]);
|
|
||||||
for (const node of $nodes.values()) {
|
|
||||||
if (!node?.tmp) continue;
|
|
||||||
const x = node.position[0];
|
|
||||||
const y = node.position[1];
|
|
||||||
const height = getNodeHeight(node.type);
|
|
||||||
if (x > x1 - 20 && x < x2 && y > y1 - height && y < y2) {
|
|
||||||
$selectedNodes?.add(node.id);
|
|
||||||
} else {
|
|
||||||
$selectedNodes?.delete(node.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$selectedNodes = $selectedNodes;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// here we are handling dragging of nodes
|
|
||||||
if ($activeNodeId !== -1 && mouseDownId !== -1) {
|
|
||||||
const node = graph.getNode($activeNodeId);
|
|
||||||
if (!node || event.buttons !== 1) return;
|
|
||||||
|
|
||||||
node.tmp = node.tmp || {};
|
|
||||||
|
|
||||||
const oldX = node.tmp.downX || 0;
|
|
||||||
const oldY = node.tmp.downY || 0;
|
|
||||||
|
|
||||||
let newX = oldX + (mx - mouseDown[0]) / cameraPosition[2];
|
|
||||||
let newY = oldY + (my - mouseDown[1]) / cameraPosition[2];
|
|
||||||
|
|
||||||
if (event.ctrlKey) {
|
|
||||||
const snapLevel = getSnapLevel();
|
|
||||||
newX = snapToGrid(newX, 5 / snapLevel);
|
|
||||||
newY = snapToGrid(newY, 5 / snapLevel);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!node.tmp.isMoving) {
|
|
||||||
const dist = Math.sqrt((oldX - newX) ** 2 + (oldY - newY) ** 2);
|
|
||||||
if (dist > 0.2) {
|
|
||||||
node.tmp.isMoving = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const vecX = oldX - newX;
|
|
||||||
const vecY = oldY - newY;
|
|
||||||
|
|
||||||
if ($selectedNodes?.size) {
|
|
||||||
for (const nodeId of $selectedNodes) {
|
|
||||||
const n = graph.getNode(nodeId);
|
|
||||||
if (!n?.tmp) continue;
|
|
||||||
n.tmp.x = (n?.tmp?.downX || 0) - vecX;
|
|
||||||
n.tmp.y = (n?.tmp?.downY || 0) - vecY;
|
|
||||||
updateNodePosition(n);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
node.tmp.x = newX;
|
|
||||||
node.tmp.y = newY;
|
|
||||||
|
|
||||||
updateNodePosition(node);
|
|
||||||
|
|
||||||
$edges = $edges;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// here we are handling panning of camera
|
|
||||||
let newX = cameraDown[0] - (mx - mouseDown[0]) / cameraPosition[2];
|
|
||||||
let newY = cameraDown[1] - (my - mouseDown[1]) / cameraPosition[2];
|
|
||||||
|
|
||||||
setCameraTransform(newX, newY, cameraPosition[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const zoomSpeed = 2;
|
|
||||||
function handleMouseScroll(event: WheelEvent) {
|
|
||||||
console.log(event);
|
|
||||||
const bodyIsFocused =
|
|
||||||
document.activeElement === document.body ||
|
|
||||||
document.activeElement === wrapper ||
|
|
||||||
document?.activeElement?.id === "graph";
|
|
||||||
if (!bodyIsFocused) return;
|
|
||||||
|
|
||||||
// Define zoom speed and clamp it between -1 and 1
|
|
||||||
const isNegative = event.deltaY < 0;
|
|
||||||
const normalizedDelta = Math.abs(event.deltaY * 0.01);
|
|
||||||
const delta = Math.pow(0.95, zoomSpeed * normalizedDelta);
|
|
||||||
|
|
||||||
// Calculate new zoom level and clamp it between minZoom and maxZoom
|
|
||||||
const newZoom = Math.max(
|
|
||||||
minZoom,
|
|
||||||
Math.min(
|
|
||||||
maxZoom,
|
|
||||||
isNegative ? cameraPosition[2] / delta : cameraPosition[2] * delta,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Calculate the ratio of the new zoom to the original zoom
|
|
||||||
const zoomRatio = newZoom / cameraPosition[2];
|
|
||||||
|
|
||||||
// Update camera position and zoom level
|
|
||||||
setCameraTransform(
|
|
||||||
mousePosition[0] - (mousePosition[0] - cameraPosition[0]) / zoomRatio,
|
|
||||||
mousePosition[1] - (mousePosition[1] - cameraPosition[1]) / zoomRatio,
|
|
||||||
newZoom,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMouseDown(event: MouseEvent) {
|
|
||||||
if (mouseDown) return;
|
|
||||||
|
|
||||||
if (event.target instanceof HTMLElement) {
|
|
||||||
if (
|
|
||||||
event.target.nodeName !== "CANVAS" &&
|
|
||||||
!event.target.classList.contains("node") &&
|
|
||||||
!event.target.classList.contains("content")
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mx = event.clientX - rect.x;
|
|
||||||
let my = event.clientY - rect.y;
|
|
||||||
|
|
||||||
mouseDown = [mx, my];
|
|
||||||
cameraDown[0] = cameraPosition[0];
|
|
||||||
cameraDown[1] = cameraPosition[1];
|
|
||||||
|
|
||||||
const clickedNodeId = getNodeIdFromEvent(event);
|
|
||||||
mouseDownId = clickedNodeId;
|
|
||||||
|
|
||||||
// if we clicked on a node
|
|
||||||
if (clickedNodeId !== -1) {
|
|
||||||
if ($activeNodeId === -1) {
|
|
||||||
$activeNodeId = clickedNodeId;
|
|
||||||
// if the selected node is the same as the clicked node
|
|
||||||
} else if ($activeNodeId === clickedNodeId) {
|
|
||||||
//$activeNodeId = -1;
|
|
||||||
// if the clicked node is different from the selected node and secondary
|
|
||||||
} else if (event.ctrlKey) {
|
|
||||||
$selectedNodes = $selectedNodes || new Set();
|
|
||||||
$selectedNodes.add($activeNodeId);
|
|
||||||
$selectedNodes.delete(clickedNodeId);
|
|
||||||
$activeNodeId = clickedNodeId;
|
|
||||||
// select the node
|
|
||||||
} else if (event.shiftKey) {
|
|
||||||
const activeNode = graph.getNode($activeNodeId);
|
|
||||||
const newNode = graph.getNode(clickedNodeId);
|
|
||||||
if (activeNode && newNode) {
|
|
||||||
const edge = graph.getNodesBetween(activeNode, newNode);
|
|
||||||
if (edge) {
|
|
||||||
const selected = new Set(edge.map((n) => n.id));
|
|
||||||
selected.add(clickedNodeId);
|
|
||||||
$selectedNodes = selected;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (!$selectedNodes?.has(clickedNodeId)) {
|
|
||||||
$activeNodeId = clickedNodeId;
|
|
||||||
$selectedNodes?.clear();
|
|
||||||
$selectedNodes = $selectedNodes;
|
|
||||||
}
|
|
||||||
} else if (event.ctrlKey) {
|
|
||||||
boxSelection = true;
|
|
||||||
}
|
|
||||||
const node = graph.getNode($activeNodeId);
|
|
||||||
if (!node) return;
|
|
||||||
node.tmp = node.tmp || {};
|
|
||||||
node.tmp.downX = node.position[0];
|
|
||||||
node.tmp.downY = node.position[1];
|
|
||||||
if ($selectedNodes) {
|
|
||||||
for (const nodeId of $selectedNodes) {
|
|
||||||
const n = graph.getNode(nodeId);
|
|
||||||
if (!n) continue;
|
|
||||||
n.tmp = n.tmp || {};
|
|
||||||
n.tmp.downX = n.position[0];
|
|
||||||
n.tmp.downY = n.position[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyNodes() {
|
|
||||||
if ($activeNodeId === -1 && !$selectedNodes?.size) return;
|
|
||||||
let _nodes = [$activeNodeId, ...($selectedNodes?.values() || [])]
|
|
||||||
.map((id) => graph.getNode(id))
|
|
||||||
.filter(Boolean) as Node[];
|
|
||||||
|
|
||||||
const _edges = graph.getEdgesBetweenNodes(_nodes);
|
|
||||||
|
|
||||||
_nodes = _nodes.map((_node) => {
|
|
||||||
const node = globalThis.structuredClone({
|
|
||||||
..._node,
|
|
||||||
tmp: {
|
|
||||||
downX: mousePosition[0] - _node.position[0],
|
|
||||||
downY: mousePosition[1] - _node.position[1],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return node;
|
|
||||||
});
|
|
||||||
|
|
||||||
clipboard = {
|
|
||||||
nodes: _nodes,
|
|
||||||
edges: _edges,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function pasteNodes() {
|
|
||||||
if (!clipboard) return;
|
|
||||||
|
|
||||||
const _nodes = clipboard.nodes
|
|
||||||
.map((node) => {
|
|
||||||
node.tmp = node.tmp || {};
|
|
||||||
node.position[0] = mousePosition[0] - (node?.tmp?.downX || 0);
|
|
||||||
node.position[1] = mousePosition[1] - (node?.tmp?.downY || 0);
|
|
||||||
return node;
|
|
||||||
})
|
|
||||||
.filter(Boolean) as Node[];
|
|
||||||
|
|
||||||
const newNodes = graph.createGraph(_nodes, clipboard.edges);
|
|
||||||
$selectedNodes = new Set(newNodes.map((n) => n.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleKeyDown(event: KeyboardEvent) {
|
|
||||||
const bodyIsFocused =
|
|
||||||
document.activeElement === document.body ||
|
|
||||||
document?.activeElement?.id === "graph";
|
|
||||||
|
|
||||||
if (event.key === "l") {
|
|
||||||
const activeNode = graph.getNode($activeNodeId);
|
|
||||||
if (activeNode) {
|
|
||||||
const nodes = graph.getLinkedNodes(activeNode);
|
|
||||||
$selectedNodes = new Set(nodes.map((n) => n.id));
|
|
||||||
}
|
|
||||||
console.log(activeNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === "Escape") {
|
|
||||||
$activeNodeId = -1;
|
|
||||||
$selectedNodes?.clear();
|
|
||||||
$selectedNodes = $selectedNodes;
|
|
||||||
(document.activeElement as HTMLElement)?.blur();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === "A" && event.shiftKey) {
|
|
||||||
addMenuPosition = [mousePosition[0], mousePosition[1]];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === ".") {
|
|
||||||
const average = [0, 0];
|
|
||||||
for (const node of $nodes.values()) {
|
|
||||||
average[0] += node.position[0];
|
|
||||||
average[1] += node.position[1];
|
|
||||||
}
|
|
||||||
average[0] = average[0] ? average[0] / $nodes.size : 0;
|
|
||||||
average[1] = average[1] ? average[1] / $nodes.size : 0;
|
|
||||||
|
|
||||||
const camX = cameraPosition[0];
|
|
||||||
const camY = cameraPosition[1];
|
|
||||||
const camZ = cameraPosition[2];
|
|
||||||
|
|
||||||
const ease = (t: number) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t);
|
|
||||||
|
|
||||||
animate(500, (a: number) => {
|
|
||||||
setCameraTransform(
|
|
||||||
lerp(camX, average[0], ease(a)),
|
|
||||||
lerp(camY, average[1], ease(a)),
|
|
||||||
lerp(camZ, 2, ease(a)),
|
|
||||||
);
|
|
||||||
if (mouseDown) return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === "a" && event.ctrlKey) {
|
|
||||||
$selectedNodes = new Set($nodes.keys());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === "c" && event.ctrlKey) {
|
|
||||||
copyNodes();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === "v" && event.ctrlKey) {
|
|
||||||
pasteNodes();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === "z" && event.ctrlKey) {
|
|
||||||
graph.undo();
|
|
||||||
for (const node of $nodes.values()) {
|
|
||||||
updateNodePosition(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === "y" && event.ctrlKey) {
|
|
||||||
graph.redo();
|
|
||||||
for (const node of $nodes.values()) {
|
|
||||||
updateNodePosition(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
(event.key === "Delete" ||
|
|
||||||
event.key === "Backspace" ||
|
|
||||||
event.key === "x") &&
|
|
||||||
bodyIsFocused
|
|
||||||
) {
|
|
||||||
graph.startUndoGroup();
|
|
||||||
if ($activeNodeId !== -1) {
|
|
||||||
const node = graph.getNode($activeNodeId);
|
|
||||||
if (node) {
|
|
||||||
graph.removeNode(node, { restoreEdges: event.ctrlKey });
|
|
||||||
$activeNodeId = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($selectedNodes) {
|
|
||||||
for (const nodeId of $selectedNodes) {
|
|
||||||
const node = graph.getNode(nodeId);
|
|
||||||
if (node) {
|
|
||||||
graph.removeNode(node, { restoreEdges: event.ctrlKey });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$selectedNodes.clear();
|
|
||||||
$selectedNodes = $selectedNodes;
|
|
||||||
}
|
|
||||||
graph.saveUndoGroup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMouseUp(event: MouseEvent) {
|
|
||||||
const activeNode = graph.getNode($activeNodeId);
|
|
||||||
|
|
||||||
const clickedNodeId = getNodeIdFromEvent(event);
|
|
||||||
|
|
||||||
if (clickedNodeId !== -1) {
|
|
||||||
if (activeNode) {
|
|
||||||
if (!activeNode?.tmp?.isMoving && !event.ctrlKey && !event.shiftKey) {
|
|
||||||
$selectedNodes?.clear();
|
|
||||||
$selectedNodes = $selectedNodes;
|
|
||||||
$activeNodeId = clickedNodeId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeNode?.tmp?.isMoving) {
|
|
||||||
activeNode.tmp = activeNode.tmp || {};
|
|
||||||
activeNode.tmp.isMoving = false;
|
|
||||||
const snapLevel = getSnapLevel();
|
|
||||||
activeNode.position[0] = snapToGrid(
|
|
||||||
activeNode?.tmp?.x ?? activeNode.position[0],
|
|
||||||
5 / snapLevel,
|
|
||||||
);
|
|
||||||
activeNode.position[1] = snapToGrid(
|
|
||||||
activeNode?.tmp?.y ?? activeNode.position[1],
|
|
||||||
5 / snapLevel,
|
|
||||||
);
|
|
||||||
const nodes = [
|
|
||||||
...[...($selectedNodes?.values() || [])].map((id) => graph.getNode(id)),
|
|
||||||
] as NodeType[];
|
|
||||||
|
|
||||||
const vec = [
|
|
||||||
activeNode.position[0] - (activeNode?.tmp.x || 0),
|
|
||||||
activeNode.position[1] - (activeNode?.tmp.y || 0),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const node of nodes) {
|
|
||||||
if (!node) continue;
|
|
||||||
node.tmp = node.tmp || {};
|
|
||||||
const { x, y } = node.tmp;
|
|
||||||
if (x !== undefined && y !== undefined) {
|
|
||||||
node.position[0] = x + vec[0];
|
|
||||||
node.position[1] = y + vec[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nodes.push(activeNode);
|
|
||||||
animate(500, (a: number) => {
|
|
||||||
for (const node of nodes) {
|
|
||||||
if (
|
|
||||||
node?.tmp &&
|
|
||||||
node.tmp["x"] !== undefined &&
|
|
||||||
node.tmp["y"] !== undefined
|
|
||||||
) {
|
|
||||||
node.tmp.x = lerp(node.tmp.x, node.position[0], a);
|
|
||||||
node.tmp.y = lerp(node.tmp.y, node.position[1], a);
|
|
||||||
updateNodePosition(node);
|
|
||||||
if (node?.tmp?.isMoving) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$edges = $edges;
|
|
||||||
});
|
|
||||||
graph.save();
|
|
||||||
} else if ($hoveredSocket && $activeSocket) {
|
|
||||||
if (
|
|
||||||
typeof $hoveredSocket.index === "number" &&
|
|
||||||
typeof $activeSocket.index === "string"
|
|
||||||
) {
|
|
||||||
graph.createEdge(
|
|
||||||
$hoveredSocket.node,
|
|
||||||
$hoveredSocket.index || 0,
|
|
||||||
$activeSocket.node,
|
|
||||||
$activeSocket.index,
|
|
||||||
);
|
|
||||||
} else if (
|
|
||||||
typeof $activeSocket.index == "number" &&
|
|
||||||
typeof $hoveredSocket.index === "string"
|
|
||||||
) {
|
|
||||||
graph.createEdge(
|
|
||||||
$activeSocket.node,
|
|
||||||
$activeSocket.index || 0,
|
|
||||||
$hoveredSocket.node,
|
|
||||||
$hoveredSocket.index,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
graph.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if camera moved
|
|
||||||
if (
|
|
||||||
clickedNodeId === -1 &&
|
|
||||||
!boxSelection &&
|
|
||||||
cameraDown[0] === cameraPosition[0] &&
|
|
||||||
cameraDown[1] === cameraPosition[1]
|
|
||||||
) {
|
|
||||||
$activeNodeId = -1;
|
|
||||||
$selectedNodes?.clear();
|
|
||||||
$selectedNodes = $selectedNodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
mouseDown = null;
|
|
||||||
boxSelection = false;
|
|
||||||
$activeSocket = null;
|
|
||||||
$possibleSockets = [];
|
|
||||||
$possibleSocketIds = null;
|
|
||||||
$hoveredSocket = null;
|
|
||||||
addMenuPosition = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
if (localStorage.getItem("cameraPosition")) {
|
|
||||||
const cPosition = JSON.parse(localStorage.getItem("cameraPosition")!);
|
|
||||||
if (Array.isArray(cPosition)) {
|
|
||||||
setCameraTransform(cPosition[0], cPosition[1], cPosition[2]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
loaded = true;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
on:wheel={handleMouseScroll}
|
|
||||||
bind:this={wrapper}
|
|
||||||
class="wrapper"
|
|
||||||
aria-label="Graph"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
bind:clientWidth={width}
|
|
||||||
bind:clientHeight={height}
|
|
||||||
on:keydown={handleKeyDown}
|
|
||||||
on:mousemove={handleMouseMove}
|
|
||||||
on:mousedown={handleMouseDown}
|
|
||||||
on:mouseup={handleMouseUp}
|
|
||||||
>
|
|
||||||
<Canvas
|
|
||||||
shadows={false}
|
|
||||||
renderMode="on-demand"
|
|
||||||
colorManagementEnabled={false}
|
|
||||||
colorSpace={LinearSRGBColorSpace}
|
|
||||||
>
|
|
||||||
<Camera bind:camera position={cameraPosition} />
|
|
||||||
|
|
||||||
<Background {cameraPosition} {maxZoom} {minZoom} {width} {height} />
|
|
||||||
|
|
||||||
{#if boxSelection && mouseDown}
|
|
||||||
<BoxSelection
|
|
||||||
{cameraPosition}
|
|
||||||
p1={{
|
|
||||||
x: cameraPosition[0] + (mouseDown[0] - width / 2) / cameraPosition[2],
|
|
||||||
y:
|
|
||||||
cameraPosition[1] + (mouseDown[1] - height / 2) / cameraPosition[2],
|
|
||||||
}}
|
|
||||||
p2={{ x: mousePosition[0], y: mousePosition[1] }}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if $status === "idle"}
|
|
||||||
{#if addMenuPosition}
|
|
||||||
<AddMenu bind:position={addMenuPosition} {graph} />
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if $activeSocket}
|
|
||||||
<FloatingEdge
|
|
||||||
from={{ x: $activeSocket.position[0], y: $activeSocket.position[1] }}
|
|
||||||
to={{ x: mousePosition[0], y: mousePosition[1] }}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<GraphView {nodes} {edges} {cameraPosition} />
|
|
||||||
{:else if $status === "loading"}
|
|
||||||
<span>Loading</span>
|
|
||||||
{:else if $status === "error"}
|
|
||||||
<span>Error</span>
|
|
||||||
{/if}
|
|
||||||
</Canvas>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.wrapper {
|
|
||||||
position: relative;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,84 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { Edge as EdgeType, Node as NodeType } from "@nodes/types";
|
|
||||||
import { HTML } from "@threlte/extras";
|
|
||||||
import Edge from "../edges/Edge.svelte";
|
|
||||||
import Node from "../node/Node.svelte";
|
|
||||||
import { getContext, onMount } from "svelte";
|
|
||||||
import type { Writable } from "svelte/store";
|
|
||||||
import { activeSocket } from "./stores";
|
|
||||||
|
|
||||||
export let nodes: Writable<Map<number, NodeType>>;
|
|
||||||
export let edges: Writable<EdgeType[]>;
|
|
||||||
|
|
||||||
export let cameraPosition = [0, 0, 4];
|
|
||||||
|
|
||||||
const isNodeInView = getContext<(n: NodeType) => boolean>("isNodeInView");
|
|
||||||
|
|
||||||
const getSocketPosition =
|
|
||||||
getContext<(node: NodeType, index: string | number) => [number, number]>(
|
|
||||||
"getSocketPosition",
|
|
||||||
);
|
|
||||||
|
|
||||||
function getEdgePosition(edge: EdgeType) {
|
|
||||||
const pos1 = getSocketPosition(edge[0], edge[1]);
|
|
||||||
const pos2 = getSocketPosition(edge[2], edge[3]);
|
|
||||||
|
|
||||||
return [pos1[0], pos1[1], pos2[0], pos2[1]];
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
for (const node of $nodes.values()) {
|
|
||||||
if (node?.tmp?.ref) {
|
|
||||||
node.tmp.ref.style.setProperty("--nx", `${node.position[0] * 10}px`);
|
|
||||||
node.tmp.ref.style.setProperty("--ny", `${node.position[1] * 10}px`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#each $edges as edge (`${edge[0].id}-${edge[1]}-${edge[2].id}-${edge[3]}`)}
|
|
||||||
{@const pos = getEdgePosition(edge)}
|
|
||||||
{@const [x1, y1, x2, y2] = pos}
|
|
||||||
<Edge
|
|
||||||
from={{
|
|
||||||
x: x1,
|
|
||||||
y: y1,
|
|
||||||
}}
|
|
||||||
to={{
|
|
||||||
x: x2,
|
|
||||||
y: y2,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
<HTML transform={false}>
|
|
||||||
<div
|
|
||||||
role="tree"
|
|
||||||
id="graph"
|
|
||||||
tabindex="0"
|
|
||||||
class="wrapper"
|
|
||||||
class:zoom-small={cameraPosition[2] < 2}
|
|
||||||
class:hovering-sockets={activeSocket}
|
|
||||||
style={`--cz: ${cameraPosition[2]}; --node-display: ${cameraPosition[2] < 2 ? "none" : "block"};`}
|
|
||||||
>
|
|
||||||
{#each $nodes.values() as node (node.id)}
|
|
||||||
<Node
|
|
||||||
{node}
|
|
||||||
inView={cameraPosition && isNodeInView(node)}
|
|
||||||
z={cameraPosition[2]}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</HTML>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.wrapper {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 100;
|
|
||||||
width: 0px;
|
|
||||||
height: 0px;
|
|
||||||
transform: scale(calc(var(--cz) * 0.1));
|
|
||||||
display: var(--node-display, block);
|
|
||||||
opacity: calc((var(--cz) - 2.5) / 3.5);
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,10 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { GraphManager } from "$lib/graph-manager";
|
|
||||||
export let manager: GraphManager;
|
|
||||||
|
|
||||||
import Graph from "$lib/components/graph/Graph.svelte";
|
|
||||||
export let debug: any;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- <PerfMonitor /> -->
|
|
||||||
<Graph graph={manager} bind:debug />
|
|
@ -1,2 +0,0 @@
|
|||||||
import Wrapper from './Wrapper.svelte';
|
|
||||||
export default Wrapper;
|
|
@ -1,135 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { Node } from "@nodes/types";
|
|
||||||
import { getContext, onMount } from "svelte";
|
|
||||||
import NodeHeader from "./NodeHeader.svelte";
|
|
||||||
import NodeParameter from "./NodeParameter.svelte";
|
|
||||||
import { activeNodeId, selectedNodes } from "../graph/stores";
|
|
||||||
import { T } from "@threlte/core";
|
|
||||||
import { Color, type Mesh } from "three";
|
|
||||||
import NodeFrag from "./Node.frag";
|
|
||||||
import NodeVert from "./Node.vert";
|
|
||||||
|
|
||||||
export let node: Node;
|
|
||||||
export let inView = true;
|
|
||||||
export let z = 2;
|
|
||||||
|
|
||||||
$: isActive = $activeNodeId === node.id;
|
|
||||||
$: isSelected = !!$selectedNodes?.has(node.id);
|
|
||||||
|
|
||||||
const updateNodePosition =
|
|
||||||
getContext<(n: Node) => void>("updateNodePosition");
|
|
||||||
|
|
||||||
const getNodeHeight = getContext<(n: string) => number>("getNodeHeight");
|
|
||||||
|
|
||||||
const type = node?.tmp?.type;
|
|
||||||
|
|
||||||
const parameters = Object.entries(type?.inputs || {}).filter(
|
|
||||||
(p) => p[1].type !== "seed",
|
|
||||||
);
|
|
||||||
|
|
||||||
let ref: HTMLDivElement;
|
|
||||||
let meshRef: Mesh;
|
|
||||||
|
|
||||||
const height = getNodeHeight(node.type);
|
|
||||||
|
|
||||||
$: if (node && ref && meshRef) {
|
|
||||||
node.tmp = node.tmp || {};
|
|
||||||
node.tmp.ref = ref;
|
|
||||||
node.tmp.mesh = meshRef;
|
|
||||||
updateNodePosition(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
node.tmp = node.tmp || {};
|
|
||||||
node.tmp.ref = ref;
|
|
||||||
node.tmp.mesh = meshRef;
|
|
||||||
updateNodePosition(node);
|
|
||||||
});
|
|
||||||
|
|
||||||
const colorDark = new Color();
|
|
||||||
colorDark.setStyle("#151515");
|
|
||||||
//colorDark.();
|
|
||||||
|
|
||||||
const colorBright = new Color("#202020");
|
|
||||||
//colorBright.convertLinearToSRGB();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<T.Mesh
|
|
||||||
position.x={node.position[0] + 10}
|
|
||||||
position.z={node.position[1] + height / 2}
|
|
||||||
position.y={0.8}
|
|
||||||
rotation.x={-Math.PI / 2}
|
|
||||||
bind:ref={meshRef}
|
|
||||||
visible={z < 7}
|
|
||||||
>
|
|
||||||
<T.PlaneGeometry args={[20, height]} radius={1} />
|
|
||||||
<T.ShaderMaterial
|
|
||||||
vertexShader={NodeVert}
|
|
||||||
fragmentShader={NodeFrag}
|
|
||||||
transparent
|
|
||||||
uniforms={{
|
|
||||||
uColorBright: { value: colorBright },
|
|
||||||
uColorDark: { value: colorDark },
|
|
||||||
uSelectedColor: { value: new Color("#9d5f28") },
|
|
||||||
uActiveColor: { value: new Color("white") },
|
|
||||||
uSelected: { value: false },
|
|
||||||
uActive: { value: false },
|
|
||||||
uStrokeWidth: { value: 1.0 },
|
|
||||||
uWidth: { value: 20 },
|
|
||||||
uHeight: { value: height },
|
|
||||||
}}
|
|
||||||
uniforms.uSelected.value={isSelected}
|
|
||||||
uniforms.uActive.value={isActive}
|
|
||||||
uniforms.uStrokeWidth.value={(7 - z) / 3}
|
|
||||||
/>
|
|
||||||
</T.Mesh>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="node"
|
|
||||||
class:active={isActive}
|
|
||||||
class:selected={isSelected}
|
|
||||||
class:out-of-view={!inView}
|
|
||||||
data-node-id={node.id}
|
|
||||||
bind:this={ref}
|
|
||||||
>
|
|
||||||
<NodeHeader {node} />
|
|
||||||
|
|
||||||
{#each parameters as [key, value], i}
|
|
||||||
<NodeParameter
|
|
||||||
bind:node
|
|
||||||
id={key}
|
|
||||||
input={value}
|
|
||||||
isLast={i == parameters.length - 1}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.node {
|
|
||||||
position: absolute;
|
|
||||||
box-sizing: border-box;
|
|
||||||
user-select: none !important;
|
|
||||||
cursor: pointer;
|
|
||||||
width: 200px;
|
|
||||||
color: var(--text-color);
|
|
||||||
transform: translate3d(var(--nx), var(--ny), 0);
|
|
||||||
z-index: 1;
|
|
||||||
font-weight: 300;
|
|
||||||
--stroke: var(--background-color-lighter);
|
|
||||||
--stroke-width: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node.active {
|
|
||||||
--stroke: white;
|
|
||||||
--stroke-width: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node.selected {
|
|
||||||
--stroke: #9d5f28;
|
|
||||||
--stroke-width: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node.out-of-view {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,137 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { createNodePath } from "$lib/helpers";
|
|
||||||
import type { Node, Socket } from "@nodes/types";
|
|
||||||
import { getContext } from "svelte";
|
|
||||||
|
|
||||||
export let node: Node;
|
|
||||||
|
|
||||||
const setDownSocket = getContext<(socket: Socket) => void>("setDownSocket");
|
|
||||||
const getSocketPosition =
|
|
||||||
getContext<(node: Node, index: number) => [number, number]>(
|
|
||||||
"getSocketPosition",
|
|
||||||
);
|
|
||||||
|
|
||||||
function handleMouseDown(event: MouseEvent) {
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
setDownSocket({
|
|
||||||
node,
|
|
||||||
index: 0,
|
|
||||||
position: getSocketPosition(node, 0),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const cornerTop = 10;
|
|
||||||
const rightBump = !!node?.tmp?.type?.outputs?.length;
|
|
||||||
const aspectRatio = 0.25;
|
|
||||||
|
|
||||||
const path = createNodePath({
|
|
||||||
depth: 4,
|
|
||||||
height: 24,
|
|
||||||
y: 50,
|
|
||||||
cornerTop,
|
|
||||||
rightBump,
|
|
||||||
aspectRatio,
|
|
||||||
});
|
|
||||||
const pathDisabled = createNodePath({
|
|
||||||
depth: 0,
|
|
||||||
height: 15,
|
|
||||||
y: 50,
|
|
||||||
cornerTop,
|
|
||||||
rightBump,
|
|
||||||
aspectRatio,
|
|
||||||
});
|
|
||||||
const pathHover = createNodePath({
|
|
||||||
depth: 5,
|
|
||||||
height: 30,
|
|
||||||
y: 50,
|
|
||||||
cornerTop,
|
|
||||||
rightBump,
|
|
||||||
aspectRatio,
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="wrapper" data-node-id={node.id}>
|
|
||||||
<div class="content">
|
|
||||||
{node.type.split("/").pop()}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="click-target"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
on:mousedown={handleMouseDown}
|
|
||||||
/>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 100 100"
|
|
||||||
width="100"
|
|
||||||
height="100"
|
|
||||||
preserveAspectRatio="none"
|
|
||||||
style={`
|
|
||||||
--path: path("${path}");
|
|
||||||
--hover-path: path("${pathHover}");
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<path vector-effect="non-scaling-stroke" stroke="white" stroke-width="0.1"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.wrapper {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.click-target {
|
|
||||||
position: absolute;
|
|
||||||
right: 0px;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateX(50%) translateY(-50%);
|
|
||||||
height: 30px;
|
|
||||||
width: 30px;
|
|
||||||
z-index: 100;
|
|
||||||
border-radius: 50%;
|
|
||||||
/* background: red; */
|
|
||||||
/* opacity: 0.2; */
|
|
||||||
}
|
|
||||||
|
|
||||||
.click-target:hover + svg path {
|
|
||||||
d: var(--hover-path);
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: -1;
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg path {
|
|
||||||
stroke-width: 0.2px;
|
|
||||||
transition:
|
|
||||||
d 0.3s ease,
|
|
||||||
fill 0.3s ease;
|
|
||||||
fill: var(--background-color-lighter);
|
|
||||||
stroke: var(--stroke);
|
|
||||||
stroke-width: var(--stroke-width);
|
|
||||||
d: var(--path);
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
font-size: 1em;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding-left: 20px;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg:hover path {
|
|
||||||
d: var(--hover-path) !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,23 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { Node, NodeInput } from "@nodes/types";
|
|
||||||
import { getGraphManager } from "../graph/context";
|
|
||||||
import Input from "@nodes/input-elements";
|
|
||||||
|
|
||||||
export let node: Node;
|
|
||||||
export let input: NodeInput;
|
|
||||||
export let id: string;
|
|
||||||
export let label: string | undefined;
|
|
||||||
|
|
||||||
const graph = getGraphManager();
|
|
||||||
|
|
||||||
let value = node?.props?.[id] ?? input.value;
|
|
||||||
|
|
||||||
$: if (node?.props?.[id] !== value) {
|
|
||||||
node.props = { ...node.props, [id]: value };
|
|
||||||
graph.save();
|
|
||||||
graph.execute();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<label for="asd">{label || id}</label>
|
|
||||||
<Input {input} bind:value />
|
|
@ -1,196 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { NodeInput as NodeInputType, Socket, Node } from "@nodes/types";
|
|
||||||
import { getContext } from "svelte";
|
|
||||||
import { createNodePath } from "$lib/helpers";
|
|
||||||
import { possibleSocketIds } from "../graph/stores";
|
|
||||||
import { getGraphManager } from "../graph/context";
|
|
||||||
import NodeInput from "./NodeInput.svelte";
|
|
||||||
import Node from "./Node.svelte";
|
|
||||||
|
|
||||||
export let node: Node;
|
|
||||||
export let input: NodeInputType;
|
|
||||||
export let id: string;
|
|
||||||
export let isLast = false;
|
|
||||||
|
|
||||||
const socketId = `${node.id}-${id}`;
|
|
||||||
|
|
||||||
const graph = getGraphManager();
|
|
||||||
const graphId = graph.id;
|
|
||||||
const inputSockets = graph.inputSockets;
|
|
||||||
|
|
||||||
const setDownSocket = getContext<(socket: Socket) => void>("setDownSocket");
|
|
||||||
const getSocketPosition =
|
|
||||||
getContext<(node: Node, index: string) => [number, number]>(
|
|
||||||
"getSocketPosition",
|
|
||||||
);
|
|
||||||
|
|
||||||
function handleMouseDown(ev: MouseEvent) {
|
|
||||||
ev.preventDefault();
|
|
||||||
ev.stopPropagation();
|
|
||||||
setDownSocket({
|
|
||||||
node,
|
|
||||||
index: id,
|
|
||||||
position: getSocketPosition(node, id),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const leftBump = node.tmp?.type?.inputs?.[id].internal !== true;
|
|
||||||
const cornerBottom = isLast ? 5 : 0;
|
|
||||||
const aspectRatio = 0.5;
|
|
||||||
|
|
||||||
const path = createNodePath({
|
|
||||||
depth: 4,
|
|
||||||
height: 12,
|
|
||||||
y: 51,
|
|
||||||
cornerBottom,
|
|
||||||
leftBump,
|
|
||||||
aspectRatio,
|
|
||||||
});
|
|
||||||
const pathDisabled = createNodePath({
|
|
||||||
depth: 0,
|
|
||||||
height: 15,
|
|
||||||
y: 50,
|
|
||||||
cornerBottom,
|
|
||||||
leftBump,
|
|
||||||
aspectRatio,
|
|
||||||
});
|
|
||||||
const pathHover = createNodePath({
|
|
||||||
depth: 6,
|
|
||||||
height: 18,
|
|
||||||
y: 50.5,
|
|
||||||
cornerBottom,
|
|
||||||
leftBump,
|
|
||||||
aspectRatio,
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="wrapper"
|
|
||||||
class:disabled={$possibleSocketIds && !$possibleSocketIds.has(socketId)}
|
|
||||||
>
|
|
||||||
{#key id && graphId}
|
|
||||||
{#if node?.tmp?.type?.inputs?.[id]?.external !== true}
|
|
||||||
<div class="content" class:disabled={$inputSockets.has(socketId)}>
|
|
||||||
<NodeInput {node} {input} {id} label={input.label} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if node?.tmp?.type?.inputs?.[id]?.internal !== true}
|
|
||||||
<div
|
|
||||||
class="large target"
|
|
||||||
on:mousedown={handleMouseDown}
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="small target"
|
|
||||||
on:mousedown={handleMouseDown}
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{/key}
|
|
||||||
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 100 100"
|
|
||||||
width="100"
|
|
||||||
height="100"
|
|
||||||
preserveAspectRatio="none"
|
|
||||||
style={`
|
|
||||||
--path: path("${path}");
|
|
||||||
--hover-path: path("${pathHover}");
|
|
||||||
--hover-path-disabled: path("${pathDisabled}");
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<path vector-effect="non-scaling-stroke"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.wrapper {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 100px;
|
|
||||||
transform: translateY(-0.5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.target {
|
|
||||||
position: absolute;
|
|
||||||
border-radius: 50%;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%) translateX(-50%);
|
|
||||||
/* background: red; */
|
|
||||||
/* opacity: 0.1; */
|
|
||||||
}
|
|
||||||
|
|
||||||
.small.target {
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.large.target {
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
cursor: unset;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.hovering-sockets) .large.target {
|
|
||||||
pointer-events: all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
position: relative;
|
|
||||||
padding: 10px 20px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
justify-content: space-around;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.zoom-small) .content {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
position: absolute;
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: visible;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg path {
|
|
||||||
transition:
|
|
||||||
d 0.3s ease,
|
|
||||||
fill 0.3s ease;
|
|
||||||
fill: var(--background-color);
|
|
||||||
stroke: var(--stroke);
|
|
||||||
stroke-width: var(--stroke-width);
|
|
||||||
d: var(--path);
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.hovering-sockets) .large:hover ~ svg path {
|
|
||||||
d: var(--hover-path);
|
|
||||||
/* fill: #131313; */
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.hovering-sockets) .small:hover ~ svg path {
|
|
||||||
/* fill: #161616; */
|
|
||||||
}
|
|
||||||
|
|
||||||
.content.disabled {
|
|
||||||
opacity: 0.2;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.disabled svg path {
|
|
||||||
d: var(--hover-path-disabled) !important;
|
|
||||||
/* fill: #060606 !important; */
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -2,7 +2,7 @@
|
|||||||
import { setContext, getContext } from "svelte";
|
import { setContext, getContext } from "svelte";
|
||||||
import localStore from "$lib/helpers/localStore";
|
import localStore from "$lib/helpers/localStore";
|
||||||
|
|
||||||
const gridId = getContext<string>("grid-id");
|
const gridId = getContext<string>("grid-id") || "grid-0";
|
||||||
let sizes = localStore<string[]>(gridId, []);
|
let sizes = localStore<string[]>(gridId, []);
|
||||||
|
|
||||||
let registerIndex = 0;
|
let registerIndex = 0;
|
||||||
|
@ -1,63 +1,46 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Grid from "$lib/grid";
|
import Grid from "$lib/grid";
|
||||||
import Graph from "$lib/components/graph";
|
import Graph from "@nodes/graph-interface";
|
||||||
import { GraphManager } from "$lib/graph-manager";
|
|
||||||
import { MemoryRuntimeExecutor } from "$lib/runtime-executor";
|
import { MemoryRuntimeExecutor } from "$lib/runtime-executor";
|
||||||
import { MemoryNodeRegistry, RemoteNodeRegistry } from "$lib/node-registry";
|
import { MemoryNodeRegistry, RemoteNodeRegistry } from "$lib/node-registry";
|
||||||
import Details from "$lib/components/Details.svelte";
|
import * as templates from "$lib/graph-templates";
|
||||||
import { JsonView } from "@zerodevx/svelte-json-view";
|
|
||||||
|
|
||||||
const memNodeRegistry = new MemoryNodeRegistry();
|
const memNodeRegistry = new MemoryNodeRegistry();
|
||||||
const nodeRegistry = new RemoteNodeRegistry("http://localhost:3001");
|
const nodeRegistry = new RemoteNodeRegistry("http://localhost:3001");
|
||||||
|
|
||||||
const runtimeExecutor = new MemoryRuntimeExecutor(nodeRegistry);
|
const runtimeExecutor = new MemoryRuntimeExecutor(nodeRegistry);
|
||||||
|
|
||||||
const graphManager = new GraphManager(nodeRegistry, runtimeExecutor);
|
let res = 0;
|
||||||
|
|
||||||
let graph = localStorage.getItem("graph");
|
let graph = localStorage.getItem("graph")
|
||||||
if (graph) {
|
? JSON.parse(localStorage.getItem("graph")!)
|
||||||
graphManager.load(JSON.parse(graph));
|
: templates.grid(3, 3);
|
||||||
} else {
|
|
||||||
graphManager.load(graphManager.createTemplate("tree", 5));
|
function handleResult(event) {
|
||||||
|
console.log("Res", event);
|
||||||
|
res = runtimeExecutor.execute(event.detail);
|
||||||
|
console.log(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
graphManager.on("save", (graph) => {
|
function handleSave(event) {
|
||||||
localStorage.setItem("graph", JSON.stringify(graph));
|
localStorage.setItem("graph", JSON.stringify(event.detail));
|
||||||
});
|
}
|
||||||
|
|
||||||
let debug: undefined;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="details-wrapper">
|
|
||||||
<Details>
|
|
||||||
<button
|
|
||||||
on:click={() => graphManager.load(graphManager.createTemplate("tree", 5))}
|
|
||||||
>load tree</button
|
|
||||||
>
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<button
|
|
||||||
on:click={() =>
|
|
||||||
graphManager.load(graphManager.createTemplate("grid", 10, 10))}
|
|
||||||
>load grid</button
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
on:click={() =>
|
|
||||||
graphManager.load(graphManager.createTemplate("grid", 2, 2))}
|
|
||||||
>load small grid</button
|
|
||||||
>
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<JsonView json={debug} />
|
|
||||||
</Details>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<header>header</header>
|
<header>header</header>
|
||||||
<Grid.Row>
|
<Grid.Row>
|
||||||
<Grid.Cell></Grid.Cell>
|
|
||||||
<Grid.Cell>
|
<Grid.Cell>
|
||||||
<Graph manager={graphManager} />
|
{res}
|
||||||
|
</Grid.Cell>
|
||||||
|
<Grid.Cell>
|
||||||
|
{#key graph}
|
||||||
|
<Graph
|
||||||
|
registry={nodeRegistry}
|
||||||
|
{graph}
|
||||||
|
on:result={handleResult}
|
||||||
|
on:save={handleSave}
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
</Grid.Cell>
|
</Grid.Cell>
|
||||||
</Grid.Row>
|
</Grid.Row>
|
||||||
</div>
|
</div>
|
||||||
@ -76,10 +59,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.details-wrapper {
|
.details-wrapper {
|
||||||
position: absolute;
|
position: fixed;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
top: 10px;
|
bottom: 10px;
|
||||||
left: 10px;
|
right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(html) {
|
:global(html) {
|
||||||
|
31
packages/graph-interface/.eslintrc.cjs
Normal file
31
packages/graph-interface/.eslintrc.cjs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
/** @type { import("eslint").Linter.Config } */
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:svelte/recommended',
|
||||||
|
'prettier'
|
||||||
|
],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: ['@typescript-eslint'],
|
||||||
|
parserOptions: {
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
extraFileExtensions: ['.svelte']
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2017: true,
|
||||||
|
node: true
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['*.svelte'],
|
||||||
|
parser: 'svelte-eslint-parser',
|
||||||
|
parserOptions: {
|
||||||
|
parser: '@typescript-eslint/parser'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
4
packages/graph-interface/.prettierignore
Normal file
4
packages/graph-interface/.prettierignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Ignore files for PNPM, NPM and YARN
|
||||||
|
pnpm-lock.yaml
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
8
packages/graph-interface/.prettierrc
Normal file
8
packages/graph-interface/.prettierrc
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"useTabs": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"printWidth": 100,
|
||||||
|
"plugins": ["prettier-plugin-svelte"],
|
||||||
|
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||||
|
}
|
65
packages/graph-interface/package.json
Normal file
65
packages/graph-interface/package.json
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
{
|
||||||
|
"name": "@nodes/graph-interface",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "svelte-package --watch",
|
||||||
|
"build": "vite build && npm run package",
|
||||||
|
"postbuild": "pnpm run package",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"package": "svelte-kit sync && svelte-package && publint",
|
||||||
|
"prepublishOnly": "npm run package",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
"lint": "prettier --check . && eslint .",
|
||||||
|
"format": "prettier --write ."
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"svelte": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"!dist/**/*.test.*",
|
||||||
|
"!dist/**/*.spec.*"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@nodes/ui": "link:../ui",
|
||||||
|
"@nodes/types": "link:../types",
|
||||||
|
"@threlte/core": "^7.1.0",
|
||||||
|
"@threlte/extras": "^8.7.5",
|
||||||
|
"@threlte/flex": "^1.0.1",
|
||||||
|
"@types/three": "^0.159.0",
|
||||||
|
"jsondiffpatch": "^0.6.0",
|
||||||
|
"three": "^0.159.0",
|
||||||
|
"vite-plugin-glsl": "^1.2.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^4.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@histoire/plugin-svelte": "^0.17.9",
|
||||||
|
"@sveltejs/adapter-auto": "^3.0.0",
|
||||||
|
"@sveltejs/kit": "^2.0.0",
|
||||||
|
"@sveltejs/package": "^2.0.0",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||||
|
"@types/eslint": "^8.56.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||||
|
"@typescript-eslint/parser": "^7.0.0",
|
||||||
|
"eslint": "^8.56.0",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-svelte": "^2.35.1",
|
||||||
|
"prettier": "^3.1.1",
|
||||||
|
"prettier-plugin-svelte": "^3.1.2",
|
||||||
|
"publint": "^0.1.9",
|
||||||
|
"svelte": "^4.2.7",
|
||||||
|
"svelte-check": "^3.6.0",
|
||||||
|
"tslib": "^2.4.1",
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"vite": "^5.0.11"
|
||||||
|
},
|
||||||
|
"svelte": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"type": "module"
|
||||||
|
}
|
164
packages/graph-interface/src/lib/AddMenu.svelte
Normal file
164
packages/graph-interface/src/lib/AddMenu.svelte
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { GraphManager } from './graph-manager.js';
|
||||||
|
import { HTML } from '@threlte/extras';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
export let position: [x: number, y: number] | null;
|
||||||
|
|
||||||
|
export let graph: GraphManager;
|
||||||
|
|
||||||
|
let input: HTMLInputElement;
|
||||||
|
let value: string = '';
|
||||||
|
let activeNodeId: string = '';
|
||||||
|
|
||||||
|
const allNodes = graph.getNodeTypes();
|
||||||
|
|
||||||
|
function filterNodes() {
|
||||||
|
return allNodes.filter((node) => node.id.includes(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
$: nodes = value === '' ? allNodes : filterNodes();
|
||||||
|
$: if (nodes) {
|
||||||
|
if (activeNodeId === '') {
|
||||||
|
activeNodeId = nodes[0].id;
|
||||||
|
} else if (nodes.length) {
|
||||||
|
const node = nodes.find((node) => node.id === activeNodeId);
|
||||||
|
if (!node) {
|
||||||
|
activeNodeId = nodes[0].id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyDown(event: KeyboardEvent) {
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
const value = (event.target as HTMLInputElement).value;
|
||||||
|
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
position = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'ArrowDown') {
|
||||||
|
const index = nodes.findIndex((node) => node.id === activeNodeId);
|
||||||
|
activeNodeId = nodes[(index + 1) % nodes.length].id;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'ArrowUp') {
|
||||||
|
const index = nodes.findIndex((node) => node.id === activeNodeId);
|
||||||
|
activeNodeId = nodes[(index - 1 + nodes.length) % nodes.length].id;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
if (activeNodeId && position) {
|
||||||
|
graph.createNode({ type: activeNodeId, position });
|
||||||
|
position = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
input.disabled = false;
|
||||||
|
setTimeout(() => input.focus(), 50);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<HTML position.x={position?.[0]} position.z={position?.[1]} transform={false}>
|
||||||
|
<div class="add-menu-wrapper">
|
||||||
|
<div class="header">
|
||||||
|
<input
|
||||||
|
id="add-menu"
|
||||||
|
type="text"
|
||||||
|
aria-label="Search for a node type"
|
||||||
|
role="searchbox"
|
||||||
|
placeholder="Search..."
|
||||||
|
disabled={false}
|
||||||
|
on:keydown={handleKeyDown}
|
||||||
|
bind:value
|
||||||
|
bind:this={input}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
{#each nodes as node}
|
||||||
|
<div
|
||||||
|
class="result"
|
||||||
|
role="treeitem"
|
||||||
|
tabindex="0"
|
||||||
|
aria-selected={node.id === activeNodeId}
|
||||||
|
on:keydown={(event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
if (position) {
|
||||||
|
graph.createNode({ type: node.id, position });
|
||||||
|
position = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
on:mousedown={() => {
|
||||||
|
if (position) {
|
||||||
|
graph.createNode({ type: node.id, position });
|
||||||
|
position = null;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
on:focus={() => {
|
||||||
|
activeNodeId = node.id;
|
||||||
|
}}
|
||||||
|
class:selected={node.id === activeNodeId}
|
||||||
|
on:mouseover={() => {
|
||||||
|
activeNodeId = node.id;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{node.id}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</HTML>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
input {
|
||||||
|
background: var(--background-color-lighter);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
border: none;
|
||||||
|
color: var(--text-color);
|
||||||
|
padding: 0.8em;
|
||||||
|
width: calc(100% - 2px);
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 1em;
|
||||||
|
margin-left: 1px;
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
outline: solid 2px rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-menu-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
background: var(--background-color);
|
||||||
|
border-radius: 7px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: solid 2px var(--background-color-lighter);
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
min-height: none;
|
||||||
|
width: 100%;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result {
|
||||||
|
padding: 1em 0.9em;
|
||||||
|
border-bottom: solid 1px var(--background-color-lighter);
|
||||||
|
opacity: 0.7;
|
||||||
|
font-size: 0.9em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result[aria-selected='true'] {
|
||||||
|
background: var(--background-color-lighter);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
</style>
|
28
packages/graph-interface/src/lib/BoxSelection.svelte
Normal file
28
packages/graph-interface/src/lib/BoxSelection.svelte
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { HTML } from '@threlte/extras';
|
||||||
|
|
||||||
|
export let p1 = { x: 0, y: 0 };
|
||||||
|
export let p2 = { x: 0, y: 0 };
|
||||||
|
|
||||||
|
export let cameraPosition = [0, 1, 0];
|
||||||
|
|
||||||
|
$: width = Math.abs(p1.x - p2.x) * cameraPosition[2];
|
||||||
|
$: height = Math.abs(p1.y - p2.y) * cameraPosition[2];
|
||||||
|
|
||||||
|
$: x = Math.max(p1.x, p2.x) - width / cameraPosition[2];
|
||||||
|
$: y = Math.max(p1.y, p2.y) - height / cameraPosition[2];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<HTML position.x={x} position.z={y} transform={false}>
|
||||||
|
<div class="box-selection" style={`width: ${width}px; height: ${height}px;`}></div>
|
||||||
|
</HTML>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.box-selection {
|
||||||
|
width: 40px;
|
||||||
|
height: 20px;
|
||||||
|
border: solid 0.2px rgba(200, 200, 200, 0.8);
|
||||||
|
border-style: dashed;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,52 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { T } from '@threlte/core';
|
||||||
|
|
||||||
|
import BackgroundVert from './Background.vert';
|
||||||
|
import BackgroundFrag from './Background.frag';
|
||||||
|
import { colors } from '../graph/stores.js';
|
||||||
|
|
||||||
|
export let minZoom = 4;
|
||||||
|
export let maxZoom = 150;
|
||||||
|
|
||||||
|
export let cameraPosition: [number, number, number] = [0, 1, 0];
|
||||||
|
|
||||||
|
export let width = globalThis?.innerWidth || 100;
|
||||||
|
export let height = globalThis?.innerHeight || 100;
|
||||||
|
|
||||||
|
let bw = 2;
|
||||||
|
let bh = 2;
|
||||||
|
|
||||||
|
$: if (width && height) {
|
||||||
|
bw = width / cameraPosition[2];
|
||||||
|
bh = height / cameraPosition[2];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<T.Group position.x={cameraPosition[0]} position.z={cameraPosition[1]} position.y={-1.0}>
|
||||||
|
<T.Mesh rotation.x={-Math.PI / 2} position.y={0.2} scale.x={bw} scale.y={bh}>
|
||||||
|
<T.PlaneGeometry args={[1, 1]} />
|
||||||
|
<T.ShaderMaterial
|
||||||
|
transparent
|
||||||
|
vertexShader={BackgroundVert}
|
||||||
|
fragmentShader={BackgroundFrag}
|
||||||
|
uniforms={{
|
||||||
|
camPos: {
|
||||||
|
value: [0, 1, 0]
|
||||||
|
},
|
||||||
|
backgroundColor: {
|
||||||
|
value: [0, 0, 0]
|
||||||
|
},
|
||||||
|
zoomLimits: {
|
||||||
|
value: [2, 50]
|
||||||
|
},
|
||||||
|
dimensions: {
|
||||||
|
value: [100, 100]
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
uniforms.camPos.value={cameraPosition}
|
||||||
|
uniforms.backgroundColor.value={$colors.backgroundColorDarker}
|
||||||
|
uniforms.zoomLimits.value={[minZoom, maxZoom]}
|
||||||
|
uniforms.dimensions.value={[width, height]}
|
||||||
|
/>
|
||||||
|
</T.Mesh>
|
||||||
|
</T.Group>
|
19
packages/graph-interface/src/lib/debug/Debug.svelte
Normal file
19
packages/graph-interface/src/lib/debug/Debug.svelte
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { MeshLineGeometry, MeshLineMaterial } from '@threlte/extras';
|
||||||
|
import { points, lines } from './store.js';
|
||||||
|
import { T } from '@threlte/core';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#each $points as point}
|
||||||
|
<T.Mesh position.x={point.x} position.y={point.y} position.z={point.z} rotation.x={-Math.PI / 2}>
|
||||||
|
<T.CircleGeometry args={[0.2, 32]} />
|
||||||
|
<T.MeshBasicMaterial color="red" />
|
||||||
|
</T.Mesh>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#each $lines as line}
|
||||||
|
<T.Mesh>
|
||||||
|
<MeshLineGeometry points={line} />
|
||||||
|
<MeshLineMaterial color="red" linewidth={1} attenuate={false} />
|
||||||
|
</T.Mesh>
|
||||||
|
{/each}
|
@ -1,5 +1,5 @@
|
|||||||
import { BufferGeometry, Vector3, BufferAttribute } from 'three'
|
import { BufferGeometry, Vector3, BufferAttribute } from 'three'
|
||||||
import { setXY, setXYZ, setXYZW, setXYZXYZ } from './utils'
|
import { setXY, setXYZ, setXYZW, setXYZXYZ } from './utils.js'
|
||||||
|
|
||||||
|
|
||||||
export function createEdgeGeometry(points: Vector3[]) {
|
export function createEdgeGeometry(points: Vector3[]) {
|
@ -1,14 +1,13 @@
|
|||||||
import { writable, type Writable } from "svelte/store";
|
import { writable, type Writable } from "svelte/store";
|
||||||
import type { Graph, Node, Edge, Socket, NodeRegistry, RuntimeExecutor, } from "@nodes/types";
|
import type { Graph, Node, Edge, Socket, NodeRegistry, } from "@nodes/types";
|
||||||
import { HistoryManager } from "./history-manager";
|
import { HistoryManager } from "./history-manager.js"
|
||||||
import * as templates from "./graphs";
|
import EventEmitter from "./helpers/EventEmitter.js";
|
||||||
import EventEmitter from "./helpers/EventEmitter";
|
import throttle from "./helpers/throttle.js";
|
||||||
import throttle from "./helpers/throttle";
|
import { createLogger } from "./helpers/index.js";
|
||||||
import { createLogger } from "./helpers";
|
|
||||||
|
|
||||||
const logger = createLogger("graph-manager");
|
const logger = createLogger("graph-manager");
|
||||||
|
|
||||||
export class GraphManager extends EventEmitter<{ "save": Graph }> {
|
export class GraphManager extends EventEmitter<{ "save": Graph, "result": any }> {
|
||||||
|
|
||||||
status: Writable<"loading" | "idle" | "error"> = writable("loading");
|
status: Writable<"loading" | "idle" | "error"> = writable("loading");
|
||||||
loaded = false;
|
loaded = false;
|
||||||
@ -27,7 +26,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph }> {
|
|||||||
|
|
||||||
history: HistoryManager = new HistoryManager();
|
history: HistoryManager = new HistoryManager();
|
||||||
|
|
||||||
constructor(private nodeRegistry: NodeRegistry, private runtime: RuntimeExecutor) {
|
constructor(private nodeRegistry: NodeRegistry) {
|
||||||
super();
|
super();
|
||||||
this.nodes.subscribe((nodes) => {
|
this.nodes.subscribe((nodes) => {
|
||||||
this._nodes = nodes;
|
this._nodes = nodes;
|
||||||
@ -58,10 +57,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph }> {
|
|||||||
execute() { }
|
execute() { }
|
||||||
_execute() {
|
_execute() {
|
||||||
if (this.loaded === false) return;
|
if (this.loaded === false) return;
|
||||||
const start = performance.now();
|
this.emit("result", this.serialize());
|
||||||
const result = this.runtime.execute(this.serialize());
|
|
||||||
const end = performance.now();
|
|
||||||
logger.log(`Execution took ${end - start}ms -> ${result}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getNodeTypes() {
|
getNodeTypes() {
|
||||||
@ -525,17 +521,6 @@ export class GraphManager extends EventEmitter<{ "save": Graph }> {
|
|||||||
.filter(Boolean) as unknown as [Node, number, Node, string][];
|
.filter(Boolean) as unknown as [Node, number, Node, string][];
|
||||||
}
|
}
|
||||||
|
|
||||||
createTemplate<T extends keyof typeof templates>(template: T, ...args: Parameters<typeof templates[T]>) {
|
|
||||||
switch (template) {
|
|
||||||
case "grid":
|
|
||||||
return templates.grid(args?.[0] || 5, args?.[1] || 5);
|
|
||||||
case "tree":
|
|
||||||
return templates.tree(args?.[0] || 4);
|
|
||||||
default:
|
|
||||||
throw new Error(`Template not found: ${template}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
765
packages/graph-interface/src/lib/graph/Graph.svelte
Normal file
765
packages/graph-interface/src/lib/graph/Graph.svelte
Normal file
@ -0,0 +1,765 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { animate, lerp, snapToGrid } from '../helpers/index.js';
|
||||||
|
import { LinearSRGBColorSpace } from 'three';
|
||||||
|
import { Canvas } from '@threlte/core';
|
||||||
|
import type { OrthographicCamera } from 'three';
|
||||||
|
import Background from '../background/Background.svelte';
|
||||||
|
import type { GraphManager } from '../graph-manager.js';
|
||||||
|
import { onMount, setContext } from 'svelte';
|
||||||
|
import Camera from '../Camera.svelte';
|
||||||
|
import GraphView from './GraphView.svelte';
|
||||||
|
import type { Node, Node as NodeType, Socket } from '@nodes/types';
|
||||||
|
import FloatingEdge from '../edges/FloatingEdge.svelte';
|
||||||
|
import {
|
||||||
|
activeNodeId,
|
||||||
|
activeSocket,
|
||||||
|
hoveredSocket,
|
||||||
|
possibleSockets,
|
||||||
|
possibleSocketIds,
|
||||||
|
selectedNodes
|
||||||
|
} from './stores.js';
|
||||||
|
import BoxSelection from '../BoxSelection.svelte';
|
||||||
|
import AddMenu from '../AddMenu.svelte';
|
||||||
|
|
||||||
|
export let graph: GraphManager;
|
||||||
|
setContext('graphManager', graph);
|
||||||
|
const status = graph.status;
|
||||||
|
const nodes = graph.nodes;
|
||||||
|
const edges = graph.edges;
|
||||||
|
|
||||||
|
let wrapper: HTMLDivElement;
|
||||||
|
$: rect =
|
||||||
|
wrapper && width ? wrapper.getBoundingClientRect() : { x: 0, y: 0, width: 0, height: 0 };
|
||||||
|
|
||||||
|
let camera: OrthographicCamera;
|
||||||
|
const minZoom = 1;
|
||||||
|
const maxZoom = 40;
|
||||||
|
let mousePosition = [0, 0];
|
||||||
|
let mouseDown: null | [number, number] = null;
|
||||||
|
let mouseDownId = -1;
|
||||||
|
let boxSelection = false;
|
||||||
|
let loaded = false;
|
||||||
|
const cameraDown = [0, 0];
|
||||||
|
let cameraPosition: [number, number, number] = [0, 0, 4];
|
||||||
|
let addMenuPosition: [number, number] | null = null;
|
||||||
|
let clipboard: null | {
|
||||||
|
nodes: Node[];
|
||||||
|
edges: [number, number, number, string][];
|
||||||
|
} = null;
|
||||||
|
|
||||||
|
$: if (cameraPosition && loaded) {
|
||||||
|
localStorage.setItem('cameraPosition', JSON.stringify(cameraPosition));
|
||||||
|
}
|
||||||
|
|
||||||
|
let width = globalThis?.innerWidth ?? 100;
|
||||||
|
let height = globalThis?.innerHeight ?? 100;
|
||||||
|
|
||||||
|
let cameraBounds = [-1000, 1000, -1000, 1000];
|
||||||
|
$: cameraBounds = [
|
||||||
|
cameraPosition[0] - width / cameraPosition[2] / 2,
|
||||||
|
cameraPosition[0] + width / cameraPosition[2] / 2,
|
||||||
|
cameraPosition[1] - height / cameraPosition[2] / 2,
|
||||||
|
cameraPosition[1] + height / cameraPosition[2] / 2
|
||||||
|
];
|
||||||
|
function setCameraTransform(x: number, y: number, z: number) {
|
||||||
|
if (!camera) return;
|
||||||
|
camera.position.x = x;
|
||||||
|
camera.position.z = y;
|
||||||
|
camera.zoom = z;
|
||||||
|
cameraPosition = [x, y, z];
|
||||||
|
}
|
||||||
|
|
||||||
|
export let debug = {};
|
||||||
|
$: debug = {
|
||||||
|
activeNodeId: $activeNodeId,
|
||||||
|
activeSocket: $activeSocket ? `${$activeSocket?.node.id}-${$activeSocket?.index}` : null,
|
||||||
|
hoveredSocket: $hoveredSocket ? `${$hoveredSocket?.node.id}-${$hoveredSocket?.index}` : null,
|
||||||
|
selectedNodes: [...($selectedNodes?.values() || [])],
|
||||||
|
cameraPosition
|
||||||
|
};
|
||||||
|
|
||||||
|
function updateNodePosition(node: NodeType) {
|
||||||
|
if (node?.tmp?.ref) {
|
||||||
|
if (node.tmp['x'] !== undefined && node.tmp['y'] !== undefined) {
|
||||||
|
node.tmp.ref.style.setProperty('--nx', `${node.tmp.x * 10}px`);
|
||||||
|
node.tmp.ref.style.setProperty('--ny', `${node.tmp.y * 10}px`);
|
||||||
|
node.tmp.mesh.position.x = node.tmp.x + 10;
|
||||||
|
node.tmp.mesh.position.z = node.tmp.y + getNodeHeight(node.type) / 2;
|
||||||
|
if (node.tmp.x === node.position[0] && node.tmp.y === node.position[1]) {
|
||||||
|
delete node.tmp.x;
|
||||||
|
delete node.tmp.y;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
node.tmp.ref.style.setProperty('--nx', `${node.position[0] * 10}px`);
|
||||||
|
node.tmp.ref.style.setProperty('--ny', `${node.position[1] * 10}px`);
|
||||||
|
node.tmp.mesh.position.x = node.position[0] + 10;
|
||||||
|
node.tmp.mesh.position.z = node.position[1] + getNodeHeight(node.type) / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setContext('updateNodePosition', updateNodePosition);
|
||||||
|
|
||||||
|
const nodeHeightCache: Record<string, number> = {};
|
||||||
|
function getNodeHeight(nodeTypeId: string) {
|
||||||
|
if (nodeTypeId in nodeHeightCache) {
|
||||||
|
return nodeHeightCache[nodeTypeId];
|
||||||
|
}
|
||||||
|
const node = graph.getNodeType(nodeTypeId);
|
||||||
|
if (!node?.inputs) {
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
const height = 5 + 10 * Object.keys(node.inputs).filter((i) => i !== 'seed').length;
|
||||||
|
nodeHeightCache[nodeTypeId] = height;
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
setContext('getNodeHeight', getNodeHeight);
|
||||||
|
|
||||||
|
setContext('isNodeInView', (node: NodeType) => {
|
||||||
|
const height = getNodeHeight(node.type);
|
||||||
|
const width = 20;
|
||||||
|
return (
|
||||||
|
node.position[0] > cameraBounds[0] - width &&
|
||||||
|
node.position[0] < cameraBounds[1] &&
|
||||||
|
node.position[1] > cameraBounds[2] - height &&
|
||||||
|
node.position[1] < cameraBounds[3]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function getNodeIdFromEvent(event: MouseEvent) {
|
||||||
|
let clickedNodeId = -1;
|
||||||
|
|
||||||
|
let mx = event.clientX - rect.x;
|
||||||
|
let my = event.clientY - rect.y;
|
||||||
|
|
||||||
|
if (event.button === 0) {
|
||||||
|
// check if the clicked element is a node
|
||||||
|
if (event.target instanceof HTMLElement) {
|
||||||
|
const nodeElement = event.target.closest('.node');
|
||||||
|
const nodeId = nodeElement?.getAttribute?.('data-node-id');
|
||||||
|
if (nodeId) {
|
||||||
|
clickedNodeId = parseInt(nodeId, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we do not have an active node,
|
||||||
|
// we are going to check if we clicked on a node by coordinates
|
||||||
|
if (clickedNodeId === -1) {
|
||||||
|
const [downX, downY] = projectScreenToWorld(mx, my);
|
||||||
|
for (const node of $nodes.values()) {
|
||||||
|
const x = node.position[0];
|
||||||
|
const y = node.position[1];
|
||||||
|
const height = getNodeHeight(node.type);
|
||||||
|
if (downX > x && downX < x + 20 && downY > y && downY < y + height) {
|
||||||
|
clickedNodeId = node.id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clickedNodeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
setContext('setDownSocket', (socket: Socket) => {
|
||||||
|
$activeSocket = socket;
|
||||||
|
|
||||||
|
let { node, index, position } = socket;
|
||||||
|
|
||||||
|
// remove existing edge
|
||||||
|
if (typeof index === 'string') {
|
||||||
|
const edges = graph.getEdgesToNode(node);
|
||||||
|
for (const edge of edges) {
|
||||||
|
if (edge[3] === index) {
|
||||||
|
node = edge[0];
|
||||||
|
index = edge[1];
|
||||||
|
position = getSocketPosition(node, index);
|
||||||
|
graph.removeEdge(edge);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseDown = position;
|
||||||
|
$activeSocket = {
|
||||||
|
node,
|
||||||
|
index,
|
||||||
|
position
|
||||||
|
};
|
||||||
|
|
||||||
|
$possibleSockets = graph.getPossibleSockets($activeSocket).map(([node, index]) => {
|
||||||
|
return {
|
||||||
|
node,
|
||||||
|
index,
|
||||||
|
position: getSocketPosition(node, index)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
$possibleSocketIds = new Set($possibleSockets.map((s) => `${s.node.id}-${s.index}`));
|
||||||
|
});
|
||||||
|
|
||||||
|
function getSnapLevel() {
|
||||||
|
const z = cameraPosition[2];
|
||||||
|
if (z > 66) {
|
||||||
|
return 8;
|
||||||
|
} else if (z > 55) {
|
||||||
|
return 4;
|
||||||
|
} else if (z > 11) {
|
||||||
|
return 2;
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSocketPosition(node: NodeType, index: string | number): [number, number] {
|
||||||
|
if (typeof index === 'number') {
|
||||||
|
return [
|
||||||
|
(node?.tmp?.x ?? node.position[0]) + 20,
|
||||||
|
(node?.tmp?.y ?? node.position[1]) + 2.5 + 10 * index
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
const _index = Object.keys(node.tmp?.type?.inputs || {}).indexOf(index);
|
||||||
|
return [
|
||||||
|
node?.tmp?.x ?? node.position[0],
|
||||||
|
(node?.tmp?.y ?? node.position[1]) + 10 + 10 * _index
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setContext('getSocketPosition', getSocketPosition);
|
||||||
|
|
||||||
|
function projectScreenToWorld(x: number, y: number): [number, number] {
|
||||||
|
return [
|
||||||
|
cameraPosition[0] + (x - width / 2) / cameraPosition[2],
|
||||||
|
cameraPosition[1] + (y - height / 2) / cameraPosition[2]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseMove(event: MouseEvent) {
|
||||||
|
let mx = event.clientX - rect.x;
|
||||||
|
let my = event.clientY - rect.y;
|
||||||
|
|
||||||
|
mousePosition = projectScreenToWorld(mx, my);
|
||||||
|
|
||||||
|
if (!mouseDown) return;
|
||||||
|
|
||||||
|
// we are creating a new edge here
|
||||||
|
if ($possibleSockets?.length) {
|
||||||
|
let smallestDist = 1000;
|
||||||
|
let _socket;
|
||||||
|
for (const socket of $possibleSockets) {
|
||||||
|
const dist = Math.sqrt(
|
||||||
|
(socket.position[0] - mousePosition[0]) ** 2 +
|
||||||
|
(socket.position[1] - mousePosition[1]) ** 2
|
||||||
|
);
|
||||||
|
if (dist < smallestDist) {
|
||||||
|
smallestDist = dist;
|
||||||
|
_socket = socket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_socket && smallestDist < 0.9) {
|
||||||
|
mousePosition = _socket.position;
|
||||||
|
$hoveredSocket = _socket;
|
||||||
|
} else {
|
||||||
|
$hoveredSocket = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle box selection
|
||||||
|
if (boxSelection) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
const mouseD = projectScreenToWorld(mouseDown[0], mouseDown[1]);
|
||||||
|
const x1 = Math.min(mouseD[0], mousePosition[0]);
|
||||||
|
const x2 = Math.max(mouseD[0], mousePosition[0]);
|
||||||
|
const y1 = Math.min(mouseD[1], mousePosition[1]);
|
||||||
|
const y2 = Math.max(mouseD[1], mousePosition[1]);
|
||||||
|
for (const node of $nodes.values()) {
|
||||||
|
if (!node?.tmp) continue;
|
||||||
|
const x = node.position[0];
|
||||||
|
const y = node.position[1];
|
||||||
|
const height = getNodeHeight(node.type);
|
||||||
|
if (x > x1 - 20 && x < x2 && y > y1 - height && y < y2) {
|
||||||
|
$selectedNodes?.add(node.id);
|
||||||
|
} else {
|
||||||
|
$selectedNodes?.delete(node.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$selectedNodes = $selectedNodes;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// here we are handling dragging of nodes
|
||||||
|
if ($activeNodeId !== -1 && mouseDownId !== -1) {
|
||||||
|
const node = graph.getNode($activeNodeId);
|
||||||
|
if (!node || event.buttons !== 1) return;
|
||||||
|
|
||||||
|
node.tmp = node.tmp || {};
|
||||||
|
|
||||||
|
const oldX = node.tmp.downX || 0;
|
||||||
|
const oldY = node.tmp.downY || 0;
|
||||||
|
|
||||||
|
let newX = oldX + (mx - mouseDown[0]) / cameraPosition[2];
|
||||||
|
let newY = oldY + (my - mouseDown[1]) / cameraPosition[2];
|
||||||
|
|
||||||
|
if (event.ctrlKey) {
|
||||||
|
const snapLevel = getSnapLevel();
|
||||||
|
newX = snapToGrid(newX, 5 / snapLevel);
|
||||||
|
newY = snapToGrid(newY, 5 / snapLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!node.tmp.isMoving) {
|
||||||
|
const dist = Math.sqrt((oldX - newX) ** 2 + (oldY - newY) ** 2);
|
||||||
|
if (dist > 0.2) {
|
||||||
|
node.tmp.isMoving = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const vecX = oldX - newX;
|
||||||
|
const vecY = oldY - newY;
|
||||||
|
|
||||||
|
if ($selectedNodes?.size) {
|
||||||
|
for (const nodeId of $selectedNodes) {
|
||||||
|
const n = graph.getNode(nodeId);
|
||||||
|
if (!n?.tmp) continue;
|
||||||
|
n.tmp.x = (n?.tmp?.downX || 0) - vecX;
|
||||||
|
n.tmp.y = (n?.tmp?.downY || 0) - vecY;
|
||||||
|
updateNodePosition(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node.tmp.x = newX;
|
||||||
|
node.tmp.y = newY;
|
||||||
|
|
||||||
|
updateNodePosition(node);
|
||||||
|
|
||||||
|
$edges = $edges;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// here we are handling panning of camera
|
||||||
|
let newX = cameraDown[0] - (mx - mouseDown[0]) / cameraPosition[2];
|
||||||
|
let newY = cameraDown[1] - (my - mouseDown[1]) / cameraPosition[2];
|
||||||
|
|
||||||
|
setCameraTransform(newX, newY, cameraPosition[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const zoomSpeed = 2;
|
||||||
|
function handleMouseScroll(event: WheelEvent) {
|
||||||
|
const bodyIsFocused =
|
||||||
|
document.activeElement === document.body ||
|
||||||
|
document.activeElement === wrapper ||
|
||||||
|
document?.activeElement?.id === 'graph';
|
||||||
|
if (!bodyIsFocused) return;
|
||||||
|
|
||||||
|
// Define zoom speed and clamp it between -1 and 1
|
||||||
|
const isNegative = event.deltaY < 0;
|
||||||
|
const normalizedDelta = Math.abs(event.deltaY * 0.01);
|
||||||
|
const delta = Math.pow(0.95, zoomSpeed * normalizedDelta);
|
||||||
|
|
||||||
|
// Calculate new zoom level and clamp it between minZoom and maxZoom
|
||||||
|
const newZoom = Math.max(
|
||||||
|
minZoom,
|
||||||
|
Math.min(maxZoom, isNegative ? cameraPosition[2] / delta : cameraPosition[2] * delta)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate the ratio of the new zoom to the original zoom
|
||||||
|
const zoomRatio = newZoom / cameraPosition[2];
|
||||||
|
|
||||||
|
// Update camera position and zoom level
|
||||||
|
setCameraTransform(
|
||||||
|
mousePosition[0] - (mousePosition[0] - cameraPosition[0]) / zoomRatio,
|
||||||
|
mousePosition[1] - (mousePosition[1] - cameraPosition[1]) / zoomRatio,
|
||||||
|
newZoom
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseDown(event: MouseEvent) {
|
||||||
|
if (mouseDown) return;
|
||||||
|
|
||||||
|
if (event.target instanceof HTMLElement) {
|
||||||
|
if (
|
||||||
|
event.target.nodeName !== 'CANVAS' &&
|
||||||
|
!event.target.classList.contains('node') &&
|
||||||
|
!event.target.classList.contains('content')
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mx = event.clientX - rect.x;
|
||||||
|
let my = event.clientY - rect.y;
|
||||||
|
|
||||||
|
mouseDown = [mx, my];
|
||||||
|
cameraDown[0] = cameraPosition[0];
|
||||||
|
cameraDown[1] = cameraPosition[1];
|
||||||
|
|
||||||
|
const clickedNodeId = getNodeIdFromEvent(event);
|
||||||
|
mouseDownId = clickedNodeId;
|
||||||
|
|
||||||
|
// if we clicked on a node
|
||||||
|
if (clickedNodeId !== -1) {
|
||||||
|
if ($activeNodeId === -1) {
|
||||||
|
$activeNodeId = clickedNodeId;
|
||||||
|
// if the selected node is the same as the clicked node
|
||||||
|
} else if ($activeNodeId === clickedNodeId) {
|
||||||
|
//$activeNodeId = -1;
|
||||||
|
// if the clicked node is different from the selected node and secondary
|
||||||
|
} else if (event.ctrlKey) {
|
||||||
|
$selectedNodes = $selectedNodes || new Set();
|
||||||
|
$selectedNodes.add($activeNodeId);
|
||||||
|
$selectedNodes.delete(clickedNodeId);
|
||||||
|
$activeNodeId = clickedNodeId;
|
||||||
|
// select the node
|
||||||
|
} else if (event.shiftKey) {
|
||||||
|
const activeNode = graph.getNode($activeNodeId);
|
||||||
|
const newNode = graph.getNode(clickedNodeId);
|
||||||
|
if (activeNode && newNode) {
|
||||||
|
const edge = graph.getNodesBetween(activeNode, newNode);
|
||||||
|
if (edge) {
|
||||||
|
const selected = new Set(edge.map((n) => n.id));
|
||||||
|
selected.add(clickedNodeId);
|
||||||
|
$selectedNodes = selected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!$selectedNodes?.has(clickedNodeId)) {
|
||||||
|
$activeNodeId = clickedNodeId;
|
||||||
|
$selectedNodes?.clear();
|
||||||
|
$selectedNodes = $selectedNodes;
|
||||||
|
}
|
||||||
|
} else if (event.ctrlKey) {
|
||||||
|
boxSelection = true;
|
||||||
|
}
|
||||||
|
const node = graph.getNode($activeNodeId);
|
||||||
|
if (!node) return;
|
||||||
|
node.tmp = node.tmp || {};
|
||||||
|
node.tmp.downX = node.position[0];
|
||||||
|
node.tmp.downY = node.position[1];
|
||||||
|
if ($selectedNodes) {
|
||||||
|
for (const nodeId of $selectedNodes) {
|
||||||
|
const n = graph.getNode(nodeId);
|
||||||
|
if (!n) continue;
|
||||||
|
n.tmp = n.tmp || {};
|
||||||
|
n.tmp.downX = n.position[0];
|
||||||
|
n.tmp.downY = n.position[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyNodes() {
|
||||||
|
if ($activeNodeId === -1 && !$selectedNodes?.size) return;
|
||||||
|
let _nodes = [$activeNodeId, ...($selectedNodes?.values() || [])]
|
||||||
|
.map((id) => graph.getNode(id))
|
||||||
|
.filter(Boolean) as Node[];
|
||||||
|
|
||||||
|
const _edges = graph.getEdgesBetweenNodes(_nodes);
|
||||||
|
|
||||||
|
_nodes = _nodes.map((_node) => {
|
||||||
|
const node = globalThis.structuredClone({
|
||||||
|
..._node,
|
||||||
|
tmp: {
|
||||||
|
downX: mousePosition[0] - _node.position[0],
|
||||||
|
downY: mousePosition[1] - _node.position[1]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return node;
|
||||||
|
});
|
||||||
|
|
||||||
|
clipboard = {
|
||||||
|
nodes: _nodes,
|
||||||
|
edges: _edges
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function pasteNodes() {
|
||||||
|
if (!clipboard) return;
|
||||||
|
|
||||||
|
const _nodes = clipboard.nodes
|
||||||
|
.map((node) => {
|
||||||
|
node.tmp = node.tmp || {};
|
||||||
|
node.position[0] = mousePosition[0] - (node?.tmp?.downX || 0);
|
||||||
|
node.position[1] = mousePosition[1] - (node?.tmp?.downY || 0);
|
||||||
|
return node;
|
||||||
|
})
|
||||||
|
.filter(Boolean) as Node[];
|
||||||
|
|
||||||
|
const newNodes = graph.createGraph(_nodes, clipboard.edges);
|
||||||
|
$selectedNodes = new Set(newNodes.map((n) => n.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyDown(event: KeyboardEvent) {
|
||||||
|
const bodyIsFocused =
|
||||||
|
document.activeElement === document.body || document?.activeElement?.id === 'graph';
|
||||||
|
|
||||||
|
if (event.key === 'l') {
|
||||||
|
const activeNode = graph.getNode($activeNodeId);
|
||||||
|
if (activeNode) {
|
||||||
|
const nodes = graph.getLinkedNodes(activeNode);
|
||||||
|
$selectedNodes = new Set(nodes.map((n) => n.id));
|
||||||
|
}
|
||||||
|
console.log(activeNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
$activeNodeId = -1;
|
||||||
|
$selectedNodes?.clear();
|
||||||
|
$selectedNodes = $selectedNodes;
|
||||||
|
(document.activeElement as HTMLElement)?.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'A' && event.shiftKey) {
|
||||||
|
addMenuPosition = [mousePosition[0], mousePosition[1]];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === '.') {
|
||||||
|
const average = [0, 0];
|
||||||
|
for (const node of $nodes.values()) {
|
||||||
|
average[0] += node.position[0];
|
||||||
|
average[1] += node.position[1];
|
||||||
|
}
|
||||||
|
average[0] = average[0] ? average[0] / $nodes.size : 0;
|
||||||
|
average[1] = average[1] ? average[1] / $nodes.size : 0;
|
||||||
|
|
||||||
|
const camX = cameraPosition[0];
|
||||||
|
const camY = cameraPosition[1];
|
||||||
|
const camZ = cameraPosition[2];
|
||||||
|
|
||||||
|
const ease = (t: number) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t);
|
||||||
|
|
||||||
|
animate(500, (a: number) => {
|
||||||
|
setCameraTransform(
|
||||||
|
lerp(camX, average[0], ease(a)),
|
||||||
|
lerp(camY, average[1], ease(a)),
|
||||||
|
lerp(camZ, 2, ease(a))
|
||||||
|
);
|
||||||
|
if (mouseDown) return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'a' && event.ctrlKey) {
|
||||||
|
$selectedNodes = new Set($nodes.keys());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'c' && event.ctrlKey) {
|
||||||
|
copyNodes();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'v' && event.ctrlKey) {
|
||||||
|
pasteNodes();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'z' && event.ctrlKey) {
|
||||||
|
graph.undo();
|
||||||
|
for (const node of $nodes.values()) {
|
||||||
|
updateNodePosition(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'y' && event.ctrlKey) {
|
||||||
|
graph.redo();
|
||||||
|
for (const node of $nodes.values()) {
|
||||||
|
updateNodePosition(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(event.key === 'Delete' || event.key === 'Backspace' || event.key === 'x') &&
|
||||||
|
bodyIsFocused
|
||||||
|
) {
|
||||||
|
graph.startUndoGroup();
|
||||||
|
if ($activeNodeId !== -1) {
|
||||||
|
const node = graph.getNode($activeNodeId);
|
||||||
|
if (node) {
|
||||||
|
graph.removeNode(node, { restoreEdges: event.ctrlKey });
|
||||||
|
$activeNodeId = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($selectedNodes) {
|
||||||
|
for (const nodeId of $selectedNodes) {
|
||||||
|
const node = graph.getNode(nodeId);
|
||||||
|
if (node) {
|
||||||
|
graph.removeNode(node, { restoreEdges: event.ctrlKey });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$selectedNodes.clear();
|
||||||
|
$selectedNodes = $selectedNodes;
|
||||||
|
}
|
||||||
|
graph.saveUndoGroup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseUp(event: MouseEvent) {
|
||||||
|
const activeNode = graph.getNode($activeNodeId);
|
||||||
|
|
||||||
|
const clickedNodeId = getNodeIdFromEvent(event);
|
||||||
|
|
||||||
|
if (clickedNodeId !== -1) {
|
||||||
|
if (activeNode) {
|
||||||
|
if (!activeNode?.tmp?.isMoving && !event.ctrlKey && !event.shiftKey) {
|
||||||
|
$selectedNodes?.clear();
|
||||||
|
$selectedNodes = $selectedNodes;
|
||||||
|
$activeNodeId = clickedNodeId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeNode?.tmp?.isMoving) {
|
||||||
|
activeNode.tmp = activeNode.tmp || {};
|
||||||
|
activeNode.tmp.isMoving = false;
|
||||||
|
const snapLevel = getSnapLevel();
|
||||||
|
activeNode.position[0] = snapToGrid(
|
||||||
|
activeNode?.tmp?.x ?? activeNode.position[0],
|
||||||
|
5 / snapLevel
|
||||||
|
);
|
||||||
|
activeNode.position[1] = snapToGrid(
|
||||||
|
activeNode?.tmp?.y ?? activeNode.position[1],
|
||||||
|
5 / snapLevel
|
||||||
|
);
|
||||||
|
const nodes = [
|
||||||
|
...[...($selectedNodes?.values() || [])].map((id) => graph.getNode(id))
|
||||||
|
] as NodeType[];
|
||||||
|
|
||||||
|
const vec = [
|
||||||
|
activeNode.position[0] - (activeNode?.tmp.x || 0),
|
||||||
|
activeNode.position[1] - (activeNode?.tmp.y || 0)
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (!node) continue;
|
||||||
|
node.tmp = node.tmp || {};
|
||||||
|
const { x, y } = node.tmp;
|
||||||
|
if (x !== undefined && y !== undefined) {
|
||||||
|
node.position[0] = x + vec[0];
|
||||||
|
node.position[1] = y + vec[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes.push(activeNode);
|
||||||
|
animate(500, (a: number) => {
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (node?.tmp && node.tmp['x'] !== undefined && node.tmp['y'] !== undefined) {
|
||||||
|
node.tmp.x = lerp(node.tmp.x, node.position[0], a);
|
||||||
|
node.tmp.y = lerp(node.tmp.y, node.position[1], a);
|
||||||
|
updateNodePosition(node);
|
||||||
|
if (node?.tmp?.isMoving) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$edges = $edges;
|
||||||
|
});
|
||||||
|
graph.save();
|
||||||
|
} else if ($hoveredSocket && $activeSocket) {
|
||||||
|
if (typeof $hoveredSocket.index === 'number' && typeof $activeSocket.index === 'string') {
|
||||||
|
graph.createEdge(
|
||||||
|
$hoveredSocket.node,
|
||||||
|
$hoveredSocket.index || 0,
|
||||||
|
$activeSocket.node,
|
||||||
|
$activeSocket.index
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
typeof $activeSocket.index == 'number' &&
|
||||||
|
typeof $hoveredSocket.index === 'string'
|
||||||
|
) {
|
||||||
|
graph.createEdge(
|
||||||
|
$activeSocket.node,
|
||||||
|
$activeSocket.index || 0,
|
||||||
|
$hoveredSocket.node,
|
||||||
|
$hoveredSocket.index
|
||||||
|
);
|
||||||
|
}
|
||||||
|
graph.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if camera moved
|
||||||
|
if (
|
||||||
|
clickedNodeId === -1 &&
|
||||||
|
!boxSelection &&
|
||||||
|
cameraDown[0] === cameraPosition[0] &&
|
||||||
|
cameraDown[1] === cameraPosition[1]
|
||||||
|
) {
|
||||||
|
$activeNodeId = -1;
|
||||||
|
$selectedNodes?.clear();
|
||||||
|
$selectedNodes = $selectedNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseDown = null;
|
||||||
|
boxSelection = false;
|
||||||
|
$activeSocket = null;
|
||||||
|
$possibleSockets = [];
|
||||||
|
$possibleSocketIds = null;
|
||||||
|
$hoveredSocket = null;
|
||||||
|
addMenuPosition = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (localStorage.getItem('cameraPosition')) {
|
||||||
|
const cPosition = JSON.parse(localStorage.getItem('cameraPosition')!);
|
||||||
|
if (Array.isArray(cPosition)) {
|
||||||
|
setCameraTransform(cPosition[0], cPosition[1], cPosition[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loaded = true;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:mousemove={handleMouseMove} on:mouseup={handleMouseUp} />
|
||||||
|
|
||||||
|
<div
|
||||||
|
on:wheel={handleMouseScroll}
|
||||||
|
bind:this={wrapper}
|
||||||
|
class="wrapper"
|
||||||
|
aria-label="Graph"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
bind:clientWidth={width}
|
||||||
|
bind:clientHeight={height}
|
||||||
|
on:keydown={handleKeyDown}
|
||||||
|
on:mousedown={handleMouseDown}
|
||||||
|
>
|
||||||
|
<Canvas
|
||||||
|
shadows={false}
|
||||||
|
renderMode="on-demand"
|
||||||
|
colorManagementEnabled={false}
|
||||||
|
colorSpace={LinearSRGBColorSpace}
|
||||||
|
>
|
||||||
|
<Camera bind:camera position={cameraPosition} />
|
||||||
|
|
||||||
|
<Background {cameraPosition} {maxZoom} {minZoom} {width} {height} />
|
||||||
|
|
||||||
|
{#if boxSelection && mouseDown}
|
||||||
|
<BoxSelection
|
||||||
|
{cameraPosition}
|
||||||
|
p1={{
|
||||||
|
x: cameraPosition[0] + (mouseDown[0] - width / 2) / cameraPosition[2],
|
||||||
|
y: cameraPosition[1] + (mouseDown[1] - height / 2) / cameraPosition[2]
|
||||||
|
}}
|
||||||
|
p2={{ x: mousePosition[0], y: mousePosition[1] }}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if $status === 'idle'}
|
||||||
|
{#if addMenuPosition}
|
||||||
|
<AddMenu bind:position={addMenuPosition} {graph} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if $activeSocket}
|
||||||
|
<FloatingEdge
|
||||||
|
from={{ x: $activeSocket.position[0], y: $activeSocket.position[1] }}
|
||||||
|
to={{ x: mousePosition[0], y: mousePosition[1] }}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<GraphView {nodes} {edges} {cameraPosition} />
|
||||||
|
{:else if $status === 'loading'}
|
||||||
|
<span>Loading</span>
|
||||||
|
{:else if $status === 'error'}
|
||||||
|
<span>Error</span>
|
||||||
|
{/if}
|
||||||
|
</Canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wrapper {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
78
packages/graph-interface/src/lib/graph/GraphView.svelte
Normal file
78
packages/graph-interface/src/lib/graph/GraphView.svelte
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Edge as EdgeType, Node as NodeType } from '@nodes/types';
|
||||||
|
import { HTML } from '@threlte/extras';
|
||||||
|
import Edge from '../edges/Edge.svelte';
|
||||||
|
import Node from '../node/Node.svelte';
|
||||||
|
import { getContext, onMount } from 'svelte';
|
||||||
|
import type { Writable } from 'svelte/store';
|
||||||
|
import { activeSocket } from './stores.js';
|
||||||
|
|
||||||
|
export let nodes: Writable<Map<number, NodeType>>;
|
||||||
|
export let edges: Writable<EdgeType[]>;
|
||||||
|
|
||||||
|
export let cameraPosition = [0, 0, 4];
|
||||||
|
|
||||||
|
const isNodeInView = getContext<(n: NodeType) => boolean>('isNodeInView');
|
||||||
|
|
||||||
|
const getSocketPosition =
|
||||||
|
getContext<(node: NodeType, index: string | number) => [number, number]>('getSocketPosition');
|
||||||
|
|
||||||
|
function getEdgePosition(edge: EdgeType) {
|
||||||
|
const pos1 = getSocketPosition(edge[0], edge[1]);
|
||||||
|
const pos2 = getSocketPosition(edge[2], edge[3]);
|
||||||
|
|
||||||
|
return [pos1[0], pos1[1], pos2[0], pos2[1]];
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
for (const node of $nodes.values()) {
|
||||||
|
if (node?.tmp?.ref) {
|
||||||
|
node.tmp.ref.style.setProperty('--nx', `${node.position[0] * 10}px`);
|
||||||
|
node.tmp.ref.style.setProperty('--ny', `${node.position[1] * 10}px`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#each $edges as edge (`${edge[0].id}-${edge[1]}-${edge[2].id}-${edge[3]}`)}
|
||||||
|
{@const pos = getEdgePosition(edge)}
|
||||||
|
{@const [x1, y1, x2, y2] = pos}
|
||||||
|
<Edge
|
||||||
|
from={{
|
||||||
|
x: x1,
|
||||||
|
y: y1
|
||||||
|
}}
|
||||||
|
to={{
|
||||||
|
x: x2,
|
||||||
|
y: y2
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<HTML transform={false}>
|
||||||
|
<div
|
||||||
|
role="tree"
|
||||||
|
id="graph"
|
||||||
|
tabindex="0"
|
||||||
|
class="wrapper"
|
||||||
|
class:zoom-small={cameraPosition[2] < 2}
|
||||||
|
class:hovering-sockets={activeSocket}
|
||||||
|
style={`--cz: ${cameraPosition[2]}; --node-display: ${cameraPosition[2] < 2 ? 'none' : 'block'};`}
|
||||||
|
>
|
||||||
|
{#each $nodes.values() as node (node.id)}
|
||||||
|
<Node {node} inView={cameraPosition && isNodeInView(node)} z={cameraPosition[2]} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</HTML>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wrapper {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
|
width: 0px;
|
||||||
|
height: 0px;
|
||||||
|
transform: scale(calc(var(--cz) * 0.1));
|
||||||
|
display: var(--node-display, block);
|
||||||
|
opacity: calc((var(--cz) - 2.5) / 3.5);
|
||||||
|
}
|
||||||
|
</style>
|
25
packages/graph-interface/src/lib/graph/Wrapper.svelte
Normal file
25
packages/graph-interface/src/lib/graph/Wrapper.svelte
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Graph, NodeRegistry } from '@nodes/types';
|
||||||
|
import GraphEl from './Graph.svelte';
|
||||||
|
import { GraphManager } from '../graph-manager.js';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
export let registry: NodeRegistry;
|
||||||
|
export let graph: Graph;
|
||||||
|
|
||||||
|
const manager = new GraphManager(registry);
|
||||||
|
|
||||||
|
manager.on('result', (result) => {
|
||||||
|
dispatch('result', result);
|
||||||
|
});
|
||||||
|
|
||||||
|
manager.on('save', (save) => {
|
||||||
|
dispatch('save', save);
|
||||||
|
});
|
||||||
|
|
||||||
|
manager.load(graph);
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<GraphEl graph={manager} />
|
@ -1,4 +1,4 @@
|
|||||||
import type { GraphManager } from "$lib/graph-manager";
|
import type { GraphManager } from "../graph-manager.js";
|
||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
|
|
||||||
export function getGraphManager(): GraphManager {
|
export function getGraphManager(): GraphManager {
|
@ -1,4 +1,3 @@
|
|||||||
import { browser } from "$app/environment";
|
|
||||||
import type { Socket } from "@nodes/types";
|
import type { Socket } from "@nodes/types";
|
||||||
import { writable, type Writable } from "svelte/store";
|
import { writable, type Writable } from "svelte/store";
|
||||||
import { Color } from "three/src/math/Color.js";
|
import { Color } from "three/src/math/Color.js";
|
||||||
@ -17,7 +16,7 @@ export const colors = writable({
|
|||||||
backgroundColorLighter: new Color().setStyle("#202020")
|
backgroundColorLighter: new Color().setStyle("#202020")
|
||||||
});
|
});
|
||||||
|
|
||||||
if (browser) {
|
if ("getComputedStyle" in globalThis) {
|
||||||
|
|
||||||
const body = document.body;
|
const body = document.body;
|
||||||
|
|
@ -1,7 +1,4 @@
|
|||||||
|
import throttle from './throttle.js';
|
||||||
import throttle from './throttle';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
type EventMap = Record<string, unknown>;
|
type EventMap = Record<string, unknown>;
|
||||||
type EventKey<T extends EventMap> = string & keyof T;
|
type EventKey<T extends EventMap> = string & keyof T;
|
107
packages/graph-interface/src/lib/helpers/index.ts
Normal file
107
packages/graph-interface/src/lib/helpers/index.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
export function snapToGrid(value: number, gridSize: number = 10) {
|
||||||
|
return Math.round(value / gridSize) * gridSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lerp(a: number, b: number, t: number) {
|
||||||
|
return a + (b - a) * t;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function animate(duration: number, callback: (progress: number) => void | false) {
|
||||||
|
const start = performance.now();
|
||||||
|
const loop = (time: number) => {
|
||||||
|
const progress = (time - start) / duration;
|
||||||
|
if (progress < 1) {
|
||||||
|
const res = callback(progress);
|
||||||
|
if (res !== false) {
|
||||||
|
requestAnimationFrame(loop);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requestAnimationFrame(loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createNodePath({
|
||||||
|
depth = 8,
|
||||||
|
height = 20,
|
||||||
|
y = 50,
|
||||||
|
cornerTop = 0,
|
||||||
|
cornerBottom = 0,
|
||||||
|
leftBump = false,
|
||||||
|
rightBump = false,
|
||||||
|
aspectRatio = 1,
|
||||||
|
} = {}) {
|
||||||
|
return `M0,${cornerTop}
|
||||||
|
${cornerTop
|
||||||
|
? ` V${cornerTop}
|
||||||
|
Q0,0 ${cornerTop * aspectRatio},0
|
||||||
|
H${100 - cornerTop * aspectRatio}
|
||||||
|
Q100,0 100,${cornerTop}
|
||||||
|
`
|
||||||
|
: ` V0
|
||||||
|
H100
|
||||||
|
`
|
||||||
|
}
|
||||||
|
V${y - height / 2}
|
||||||
|
${rightBump
|
||||||
|
? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}`
|
||||||
|
: ` H100`
|
||||||
|
}
|
||||||
|
${cornerBottom
|
||||||
|
? ` V${100 - cornerBottom}
|
||||||
|
Q100,100 ${100 - cornerBottom * aspectRatio},100
|
||||||
|
H${cornerBottom * aspectRatio}
|
||||||
|
Q0,100 0,${100 - cornerBottom}
|
||||||
|
`
|
||||||
|
: `${leftBump ? `V100 H0` : `V100`}`
|
||||||
|
}
|
||||||
|
${leftBump
|
||||||
|
? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${y - height / 2}`
|
||||||
|
: ` H0`
|
||||||
|
}
|
||||||
|
Z`.replace(/\s+/g, " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const debounce = (fn: Function, ms = 300) => {
|
||||||
|
let timeoutId: ReturnType<typeof setTimeout>;
|
||||||
|
return function (this: any, ...args: any[]) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = setTimeout(() => fn.apply(this, args), ms);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clone: <T>(v: T) => T = "structedClone" in globalThis ? globalThis.structuredClone : (obj) => JSON.parse(JSON.stringify(obj));
|
||||||
|
|
||||||
|
export const createLogger = (() => {
|
||||||
|
let maxLength = 5;
|
||||||
|
return (scope: string) => {
|
||||||
|
maxLength = Math.max(maxLength, scope.length);
|
||||||
|
let muted = false;
|
||||||
|
return {
|
||||||
|
log: (...args: any[]) => !muted && console.log(`[%c${scope.padEnd(maxLength, " ")}]:`, "color: #888", ...args),
|
||||||
|
info: (...args: any[]) => !muted && console.info(`[%c${scope.padEnd(maxLength, " ")}]:`, "color: #888", ...args),
|
||||||
|
warn: (...args: any[]) => !muted && console.warn(`[%c${scope.padEnd(maxLength, " ")}]:`, "color: #888", ...args),
|
||||||
|
error: (...args: any[]) => console.error(`[%c${scope.padEnd(maxLength, " ")}]:`, "color: #f88", ...args),
|
||||||
|
mute() {
|
||||||
|
muted = true;
|
||||||
|
},
|
||||||
|
unmute() {
|
||||||
|
muted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
export function withSubComponents<A, B extends Record<string, any>>(
|
||||||
|
component: A,
|
||||||
|
subcomponents: B
|
||||||
|
): A & B {
|
||||||
|
Object.keys(subcomponents).forEach((key) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(component as any)[key] = (subcomponents as any)[key];
|
||||||
|
});
|
||||||
|
return component as A & B;
|
||||||
|
}
|
53
packages/graph-interface/src/lib/helpers/localStore.ts
Normal file
53
packages/graph-interface/src/lib/helpers/localStore.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { writable, type Writable } from "svelte/store";
|
||||||
|
|
||||||
|
function isStore(v: unknown): v is Writable<unknown> {
|
||||||
|
return v !== null && typeof v === "object" && "subscribe" in v && "set" in v;
|
||||||
|
}
|
||||||
|
|
||||||
|
const storeIds: Map<string, ReturnType<typeof createLocalStore>> = new Map();
|
||||||
|
|
||||||
|
const HAS_LOCALSTORAGE = "localStorage" in globalThis;
|
||||||
|
|
||||||
|
function createLocalStore<T>(key: string, initialValue: T | Writable<T>) {
|
||||||
|
|
||||||
|
let store: Writable<T>;
|
||||||
|
|
||||||
|
if (HAS_LOCALSTORAGE) {
|
||||||
|
const localValue = localStorage.getItem(key);
|
||||||
|
const value = localValue ? JSON.parse(localValue) : null;
|
||||||
|
if (value === null) {
|
||||||
|
if (isStore(initialValue)) {
|
||||||
|
store = initialValue;
|
||||||
|
} else {
|
||||||
|
store = writable(initialValue);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
store = writable(value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return isStore(initialValue) ? initialValue : writable(initialValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
store.subscribe((value) => {
|
||||||
|
localStorage.setItem(key, JSON.stringify(value));
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
set: store.set,
|
||||||
|
update: store.update
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default function localStore<T>(key: string, initialValue: T | Writable<T>): Writable<T> {
|
||||||
|
|
||||||
|
if (storeIds.has(key)) return storeIds.get(key) as Writable<T>;
|
||||||
|
|
||||||
|
const store = createLocalStore(key, initialValue)
|
||||||
|
|
||||||
|
storeIds.set(key, store);
|
||||||
|
|
||||||
|
return store
|
||||||
|
|
||||||
|
}
|
20
packages/graph-interface/src/lib/helpers/throttle.ts
Normal file
20
packages/graph-interface/src/lib/helpers/throttle.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
export default <R, A extends any[]>(
|
||||||
|
fn: (...args: A) => R,
|
||||||
|
delay: number
|
||||||
|
): ((...args: A) => R) => {
|
||||||
|
let wait = false;
|
||||||
|
|
||||||
|
return (...args: A) => {
|
||||||
|
if (wait) return undefined;
|
||||||
|
|
||||||
|
const val = fn(...args);
|
||||||
|
|
||||||
|
wait = true;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
wait = false;
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
};
|
@ -1,6 +1,6 @@
|
|||||||
import { create, type Delta } from "jsondiffpatch";
|
import { create, type Delta } from "jsondiffpatch";
|
||||||
import type { Graph } from "@nodes/types";
|
import type { Graph } from "@nodes/types";
|
||||||
import { createLogger, clone } from "./helpers";
|
import { createLogger, clone } from "./helpers/index.js";
|
||||||
|
|
||||||
|
|
||||||
const diff = create({
|
const diff = create({
|
2
packages/graph-interface/src/lib/index.ts
Normal file
2
packages/graph-interface/src/lib/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import Wrapper from './graph/Wrapper.svelte';
|
||||||
|
export default Wrapper;
|
127
packages/graph-interface/src/lib/node/Node.svelte
Normal file
127
packages/graph-interface/src/lib/node/Node.svelte
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Node } from '@nodes/types';
|
||||||
|
import { getContext, onMount } from 'svelte';
|
||||||
|
import NodeHeader from './NodeHeader.svelte';
|
||||||
|
import NodeParameter from './NodeParameter.svelte';
|
||||||
|
import { activeNodeId, selectedNodes } from '../graph/stores.js';
|
||||||
|
import { T } from '@threlte/core';
|
||||||
|
import { Color, type Mesh } from 'three';
|
||||||
|
import NodeFrag from './Node.frag';
|
||||||
|
import NodeVert from './Node.vert';
|
||||||
|
|
||||||
|
export let node: Node;
|
||||||
|
export let inView = true;
|
||||||
|
export let z = 2;
|
||||||
|
|
||||||
|
$: isActive = $activeNodeId === node.id;
|
||||||
|
$: isSelected = !!$selectedNodes?.has(node.id);
|
||||||
|
|
||||||
|
const updateNodePosition = getContext<(n: Node) => void>('updateNodePosition');
|
||||||
|
|
||||||
|
const getNodeHeight = getContext<(n: string) => number>('getNodeHeight');
|
||||||
|
|
||||||
|
const type = node?.tmp?.type;
|
||||||
|
|
||||||
|
const parameters = Object.entries(type?.inputs || {}).filter((p) => p[1].type !== 'seed');
|
||||||
|
|
||||||
|
let ref: HTMLDivElement;
|
||||||
|
let meshRef: Mesh;
|
||||||
|
|
||||||
|
const height = getNodeHeight(node.type);
|
||||||
|
|
||||||
|
$: if (node && ref && meshRef) {
|
||||||
|
node.tmp = node.tmp || {};
|
||||||
|
node.tmp.ref = ref;
|
||||||
|
node.tmp.mesh = meshRef;
|
||||||
|
updateNodePosition(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
node.tmp = node.tmp || {};
|
||||||
|
node.tmp.ref = ref;
|
||||||
|
node.tmp.mesh = meshRef;
|
||||||
|
updateNodePosition(node);
|
||||||
|
});
|
||||||
|
|
||||||
|
const colorDark = new Color();
|
||||||
|
colorDark.setStyle('#151515');
|
||||||
|
//colorDark.();
|
||||||
|
|
||||||
|
const colorBright = new Color('#202020');
|
||||||
|
//colorBright.convertLinearToSRGB();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<T.Mesh
|
||||||
|
position.x={node.position[0] + 10}
|
||||||
|
position.z={node.position[1] + height / 2}
|
||||||
|
position.y={0.8}
|
||||||
|
rotation.x={-Math.PI / 2}
|
||||||
|
bind:ref={meshRef}
|
||||||
|
visible={z < 7}
|
||||||
|
>
|
||||||
|
<T.PlaneGeometry args={[20, height]} radius={1} />
|
||||||
|
<T.ShaderMaterial
|
||||||
|
vertexShader={NodeVert}
|
||||||
|
fragmentShader={NodeFrag}
|
||||||
|
transparent
|
||||||
|
uniforms={{
|
||||||
|
uColorBright: { value: colorBright },
|
||||||
|
uColorDark: { value: colorDark },
|
||||||
|
uSelectedColor: { value: new Color('#9d5f28') },
|
||||||
|
uActiveColor: { value: new Color('white') },
|
||||||
|
uSelected: { value: false },
|
||||||
|
uActive: { value: false },
|
||||||
|
uStrokeWidth: { value: 1.0 },
|
||||||
|
uWidth: { value: 20 },
|
||||||
|
uHeight: { value: height }
|
||||||
|
}}
|
||||||
|
uniforms.uSelected.value={isSelected}
|
||||||
|
uniforms.uActive.value={isActive}
|
||||||
|
uniforms.uStrokeWidth.value={(7 - z) / 3}
|
||||||
|
/>
|
||||||
|
</T.Mesh>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="node"
|
||||||
|
class:active={isActive}
|
||||||
|
class:selected={isSelected}
|
||||||
|
class:out-of-view={!inView}
|
||||||
|
data-node-id={node.id}
|
||||||
|
bind:this={ref}
|
||||||
|
>
|
||||||
|
<NodeHeader {node} />
|
||||||
|
|
||||||
|
{#each parameters as [key, value], i}
|
||||||
|
<NodeParameter bind:node id={key} input={value} isLast={i == parameters.length - 1} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.node {
|
||||||
|
position: absolute;
|
||||||
|
box-sizing: border-box;
|
||||||
|
user-select: none !important;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 200px;
|
||||||
|
color: var(--text-color);
|
||||||
|
transform: translate3d(var(--nx), var(--ny), 0);
|
||||||
|
z-index: 1;
|
||||||
|
font-weight: 300;
|
||||||
|
--stroke: var(--background-color-lighter);
|
||||||
|
--stroke-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node.active {
|
||||||
|
--stroke: white;
|
||||||
|
--stroke-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node.selected {
|
||||||
|
--stroke: #9d5f28;
|
||||||
|
--stroke-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node.out-of-view {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
129
packages/graph-interface/src/lib/node/NodeHeader.svelte
Normal file
129
packages/graph-interface/src/lib/node/NodeHeader.svelte
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createNodePath } from '../helpers/index.js';
|
||||||
|
import type { Node, Socket } from '@nodes/types';
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
|
||||||
|
export let node: Node;
|
||||||
|
|
||||||
|
const setDownSocket = getContext<(socket: Socket) => void>('setDownSocket');
|
||||||
|
const getSocketPosition =
|
||||||
|
getContext<(node: Node, index: number) => [number, number]>('getSocketPosition');
|
||||||
|
|
||||||
|
function handleMouseDown(event: MouseEvent) {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
setDownSocket({
|
||||||
|
node,
|
||||||
|
index: 0,
|
||||||
|
position: getSocketPosition(node, 0)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const cornerTop = 10;
|
||||||
|
const rightBump = !!node?.tmp?.type?.outputs?.length;
|
||||||
|
const aspectRatio = 0.25;
|
||||||
|
|
||||||
|
const path = createNodePath({
|
||||||
|
depth: 4,
|
||||||
|
height: 24,
|
||||||
|
y: 50,
|
||||||
|
cornerTop,
|
||||||
|
rightBump,
|
||||||
|
aspectRatio
|
||||||
|
});
|
||||||
|
const pathDisabled = createNodePath({
|
||||||
|
depth: 0,
|
||||||
|
height: 15,
|
||||||
|
y: 50,
|
||||||
|
cornerTop,
|
||||||
|
rightBump,
|
||||||
|
aspectRatio
|
||||||
|
});
|
||||||
|
const pathHover = createNodePath({
|
||||||
|
depth: 5,
|
||||||
|
height: 30,
|
||||||
|
y: 50,
|
||||||
|
cornerTop,
|
||||||
|
rightBump,
|
||||||
|
aspectRatio
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="wrapper" data-node-id={node.id}>
|
||||||
|
<div class="content">
|
||||||
|
{node.type.split('/').pop()}
|
||||||
|
</div>
|
||||||
|
<div class="click-target" role="button" tabindex="0" on:mousedown={handleMouseDown} />
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
width="100"
|
||||||
|
height="100"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
style={`
|
||||||
|
--path: path("${path}");
|
||||||
|
--hover-path: path("${pathHover}");
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<path vector-effect="non-scaling-stroke" stroke="white" stroke-width="0.1"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.click-target {
|
||||||
|
position: absolute;
|
||||||
|
right: 0px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateX(50%) translateY(-50%);
|
||||||
|
height: 30px;
|
||||||
|
width: 30px;
|
||||||
|
z-index: 100;
|
||||||
|
border-radius: 50%;
|
||||||
|
/* background: red; */
|
||||||
|
/* opacity: 0.2; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.click-target:hover + svg path {
|
||||||
|
d: var(--hover-path);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: -1;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg path {
|
||||||
|
stroke-width: 0.2px;
|
||||||
|
transition:
|
||||||
|
d 0.3s ease,
|
||||||
|
fill 0.3s ease;
|
||||||
|
fill: var(--background-color-lighter);
|
||||||
|
stroke: var(--stroke);
|
||||||
|
stroke-width: var(--stroke-width);
|
||||||
|
d: var(--path);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
font-size: 1em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 20px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg:hover path {
|
||||||
|
d: var(--hover-path) !important;
|
||||||
|
}
|
||||||
|
</style>
|
23
packages/graph-interface/src/lib/node/NodeInput.svelte
Normal file
23
packages/graph-interface/src/lib/node/NodeInput.svelte
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Node, NodeInput } from '@nodes/types';
|
||||||
|
import { getGraphManager } from '../graph/context.js';
|
||||||
|
import { Input } from '@nodes/ui';
|
||||||
|
|
||||||
|
export let node: Node;
|
||||||
|
export let input: NodeInput;
|
||||||
|
export let id: string;
|
||||||
|
export let label: string | undefined;
|
||||||
|
|
||||||
|
const graph = getGraphManager();
|
||||||
|
|
||||||
|
let value = node?.props?.[id] ?? input.value;
|
||||||
|
|
||||||
|
$: if (node?.props?.[id] !== value) {
|
||||||
|
node.props = { ...node.props, [id]: value };
|
||||||
|
graph.save();
|
||||||
|
graph.execute();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<label for="asd">{label || id}</label>
|
||||||
|
<Input {input} bind:value />
|
174
packages/graph-interface/src/lib/node/NodeParameter.svelte
Normal file
174
packages/graph-interface/src/lib/node/NodeParameter.svelte
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { NodeInput as NodeInputType, Socket, Node as NodeType } from '@nodes/types';
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
import { createNodePath } from '../helpers/index.js';
|
||||||
|
import { possibleSocketIds } from '../graph/stores.js';
|
||||||
|
import { getGraphManager } from '../graph/context.js';
|
||||||
|
import NodeInput from './NodeInput.svelte';
|
||||||
|
|
||||||
|
export let node: NodeType;
|
||||||
|
export let input: NodeInputType;
|
||||||
|
export let id: string;
|
||||||
|
export let isLast = false;
|
||||||
|
|
||||||
|
const socketId = `${node.id}-${id}`;
|
||||||
|
|
||||||
|
const graph = getGraphManager();
|
||||||
|
const graphId = graph.id;
|
||||||
|
const inputSockets = graph.inputSockets;
|
||||||
|
|
||||||
|
const setDownSocket = getContext<(socket: Socket) => void>('setDownSocket');
|
||||||
|
const getSocketPosition =
|
||||||
|
getContext<(node: NodeType, index: string) => [number, number]>('getSocketPosition');
|
||||||
|
|
||||||
|
function handleMouseDown(ev: MouseEvent) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
setDownSocket({
|
||||||
|
node,
|
||||||
|
index: id,
|
||||||
|
position: getSocketPosition(node, id)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const leftBump = node.tmp?.type?.inputs?.[id].internal !== true;
|
||||||
|
const cornerBottom = isLast ? 5 : 0;
|
||||||
|
const aspectRatio = 0.5;
|
||||||
|
|
||||||
|
const path = createNodePath({
|
||||||
|
depth: 4,
|
||||||
|
height: 12,
|
||||||
|
y: 51,
|
||||||
|
cornerBottom,
|
||||||
|
leftBump,
|
||||||
|
aspectRatio
|
||||||
|
});
|
||||||
|
const pathDisabled = createNodePath({
|
||||||
|
depth: 0,
|
||||||
|
height: 15,
|
||||||
|
y: 50,
|
||||||
|
cornerBottom,
|
||||||
|
leftBump,
|
||||||
|
aspectRatio
|
||||||
|
});
|
||||||
|
const pathHover = createNodePath({
|
||||||
|
depth: 6,
|
||||||
|
height: 18,
|
||||||
|
y: 50.5,
|
||||||
|
cornerBottom,
|
||||||
|
leftBump,
|
||||||
|
aspectRatio
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="wrapper" class:disabled={$possibleSocketIds && !$possibleSocketIds.has(socketId)}>
|
||||||
|
{#key id && graphId}
|
||||||
|
{#if node?.tmp?.type?.inputs?.[id]?.external !== true}
|
||||||
|
<div class="content" class:disabled={$inputSockets.has(socketId)}>
|
||||||
|
<NodeInput {node} {input} {id} label={input.label} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if node?.tmp?.type?.inputs?.[id]?.internal !== true}
|
||||||
|
<div class="large target" on:mousedown={handleMouseDown} role="button" tabindex="0" />
|
||||||
|
<div class="small target" on:mousedown={handleMouseDown} role="button" tabindex="0" />
|
||||||
|
{/if}
|
||||||
|
{/key}
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
width="100"
|
||||||
|
height="100"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
style={`
|
||||||
|
--path: path("${path}");
|
||||||
|
--hover-path: path("${pathHover}");
|
||||||
|
--hover-path-disabled: path("${pathDisabled}");
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<path vector-effect="non-scaling-stroke"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100px;
|
||||||
|
transform: translateY(-0.5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.target {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%) translateX(-50%);
|
||||||
|
/* background: red; */
|
||||||
|
/* opacity: 0.1; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.small.target {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.large.target {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
cursor: unset;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.hovering-sockets) .large.target {
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
position: relative;
|
||||||
|
padding: 10px 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: space-around;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.zoom-small) .content {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
position: absolute;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: visible;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg path {
|
||||||
|
transition:
|
||||||
|
d 0.3s ease,
|
||||||
|
fill 0.3s ease;
|
||||||
|
fill: var(--background-color);
|
||||||
|
stroke: var(--stroke);
|
||||||
|
stroke-width: var(--stroke-width);
|
||||||
|
d: var(--path);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.hovering-sockets) .large:hover ~ svg path {
|
||||||
|
d: var(--hover-path);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content.disabled {
|
||||||
|
opacity: 0.2;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled svg path {
|
||||||
|
d: var(--hover-path-disabled) !important;
|
||||||
|
}
|
||||||
|
</style>
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
18
packages/graph-interface/tsconfig.json
Normal file
18
packages/graph-interface/tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"types": [
|
||||||
|
"vite-plugin-glsl/ext"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
18
packages/graph-interface/vite.config.ts
Normal file
18
packages/graph-interface/vite.config.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
import glsl from "vite-plugin-glsl";
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import { exec } from 'child_process';
|
||||||
|
const dev = import.meta.env;
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [sveltekit(), glsl(), {
|
||||||
|
name: 'postbuild-commands', // the name of your custom plugin. Could be anything.
|
||||||
|
closeBundle: async () => {
|
||||||
|
return;
|
||||||
|
// run pnpm run package
|
||||||
|
exec('pnpm run package', (err, stdout, stderr) => {
|
||||||
|
console.log(stdout);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},]
|
||||||
|
});
|
13
packages/ui/.eslintignore
Normal file
13
packages/ui/.eslintignore
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/build
|
||||||
|
/.svelte-kit
|
||||||
|
/package
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Ignore files for PNPM, NPM and YARN
|
||||||
|
pnpm-lock.yaml
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
11
packages/ui/.gitignore
vendored
Normal file
11
packages/ui/.gitignore
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/build
|
||||||
|
/dist
|
||||||
|
/.svelte-kit
|
||||||
|
/package
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
1
packages/ui/.npmrc
Normal file
1
packages/ui/.npmrc
Normal file
@ -0,0 +1 @@
|
|||||||
|
engine-strict=true
|
58
packages/ui/README.md
Normal file
58
packages/ui/README.md
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# create-svelte
|
||||||
|
|
||||||
|
Everything you need to build a Svelte library, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
|
||||||
|
|
||||||
|
Read more about creating a library [in the docs](https://kit.svelte.dev/docs/packaging).
|
||||||
|
|
||||||
|
## Creating a project
|
||||||
|
|
||||||
|
If you're seeing this, you've probably already done this step. Congrats!
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# create a new project in the current directory
|
||||||
|
npm create svelte@latest
|
||||||
|
|
||||||
|
# create a new project in my-app
|
||||||
|
npm create svelte@latest my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# or start the server and open the app in a new browser tab
|
||||||
|
npm run dev -- --open
|
||||||
|
```
|
||||||
|
|
||||||
|
Everything inside `src/lib` is part of your library, everything inside `src/routes` can be used as a showcase or preview app.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
To build your library:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run package
|
||||||
|
```
|
||||||
|
|
||||||
|
To create a production version of your showcase app:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
You can preview the production build with `npm run preview`.
|
||||||
|
|
||||||
|
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
||||||
|
|
||||||
|
## Publishing
|
||||||
|
|
||||||
|
Go into the `package.json` and give your package the desired name through the `"name"` option. Also consider adding a `"license"` field and point it to a `LICENSE` file which you can create from a template (one popular option is the [MIT license](https://opensource.org/license/mit/)).
|
||||||
|
|
||||||
|
To publish your library to [npm](https://www.npmjs.com):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm publish
|
||||||
|
```
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@nodes/input-elements",
|
"name": "@nodes/ui",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
13
packages/ui/src/app.d.ts
vendored
Normal file
13
packages/ui/src/app.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// See https://kit.svelte.dev/docs/types#app
|
||||||
|
// for information about these interfaces
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface PageState {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
12
packages/ui/src/app.html
Normal file
12
packages/ui/src/app.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div>%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -5,7 +5,8 @@ import Float from "./elements/Float.svelte";
|
|||||||
import Integer from "./elements/Integer.svelte";
|
import Integer from "./elements/Integer.svelte";
|
||||||
import Select from "./elements/Select.svelte";
|
import Select from "./elements/Select.svelte";
|
||||||
import Checkbox from "./elements/Checkbox.svelte";
|
import Checkbox from "./elements/Checkbox.svelte";
|
||||||
|
import Details from "./Details.svelte";
|
||||||
|
|
||||||
export { Float, Integer, Select, Checkbox };
|
export { Float, Integer, Select, Checkbox, Input, Details };
|
||||||
|
|
||||||
export default Input;
|
export default Input;
|
3
packages/ui/src/routes/+page.svelte
Normal file
3
packages/ui/src/routes/+page.svelte
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<h1>Welcome to your library project</h1>
|
||||||
|
<p>Create your package using @sveltejs/package and preview/showcase your work with SvelteKit</p>
|
||||||
|
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
|
BIN
packages/ui/static/favicon.png
Normal file
BIN
packages/ui/static/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
18
packages/ui/svelte.config.js
Normal file
18
packages/ui/svelte.config.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import adapter from '@sveltejs/adapter-auto';
|
||||||
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||||
|
// for more information about preprocessors
|
||||||
|
preprocess: vitePreprocess(),
|
||||||
|
|
||||||
|
kit: {
|
||||||
|
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||||
|
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
|
||||||
|
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||||
|
adapter: adapter()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
146
pnpm-lock.yaml
146
pnpm-lock.yaml
@ -10,9 +10,12 @@ importers:
|
|||||||
|
|
||||||
app:
|
app:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nodes/input-elements':
|
'@nodes/graph-interface':
|
||||||
specifier: link:../packages/input-elements
|
specifier: link:../packages/graph-interface
|
||||||
version: link:../packages/input-elements
|
version: link:../packages/graph-interface
|
||||||
|
'@nodes/ui':
|
||||||
|
specifier: link:../packages/ui
|
||||||
|
version: link:../packages/ui
|
||||||
'@sveltejs/kit':
|
'@sveltejs/kit':
|
||||||
specifier: ^2.5.0
|
specifier: ^2.5.0
|
||||||
version: 2.5.2(@sveltejs/vite-plugin-svelte@3.0.2)(svelte@4.2.12)(vite@5.1.4)
|
version: 2.5.2(@sveltejs/vite-plugin-svelte@3.0.2)(svelte@4.2.12)(vite@5.1.4)
|
||||||
@ -34,24 +37,9 @@ importers:
|
|||||||
'@types/three':
|
'@types/three':
|
||||||
specifier: ^0.159.0
|
specifier: ^0.159.0
|
||||||
version: 0.159.0
|
version: 0.159.0
|
||||||
input-elements:
|
|
||||||
specifier: link:../packages/input-elements
|
|
||||||
version: link:../packages/input-elements
|
|
||||||
jsondiffpatch:
|
|
||||||
specifier: ^0.6.0
|
|
||||||
version: 0.6.0
|
|
||||||
meshline:
|
|
||||||
specifier: ^3.2.0
|
|
||||||
version: 3.2.0(three@0.159.0)
|
|
||||||
plantarium-nodes-math:
|
|
||||||
specifier: link:../nodes/max/plantarium/math/pkg
|
|
||||||
version: link:../nodes/max/plantarium/math/pkg
|
|
||||||
three:
|
three:
|
||||||
specifier: ^0.159.0
|
specifier: ^0.159.0
|
||||||
version: 0.159.0
|
version: 0.159.0
|
||||||
three.meshline:
|
|
||||||
specifier: ^1.4.0
|
|
||||||
version: 1.4.0
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@histoire/plugin-svelte':
|
'@histoire/plugin-svelte':
|
||||||
specifier: ^0.17.9
|
specifier: ^0.17.9
|
||||||
@ -102,22 +90,55 @@ importers:
|
|||||||
specifier: ^3.3.0
|
specifier: ^3.3.0
|
||||||
version: 3.3.0(vite@5.1.4)
|
version: 3.3.0(vite@5.1.4)
|
||||||
|
|
||||||
|
nodes/max/plantarium/float: {}
|
||||||
|
|
||||||
|
nodes/max/plantarium/float/pkg: {}
|
||||||
|
|
||||||
nodes/max/plantarium/math: {}
|
nodes/max/plantarium/math: {}
|
||||||
|
|
||||||
nodes/max/plantarium/math/pkg: {}
|
nodes/max/plantarium/math/pkg: {}
|
||||||
|
|
||||||
nodes/max/plantarium/random: {}
|
nodes/max/plantarium/output: {}
|
||||||
|
|
||||||
nodes/max/plantarium/random/old-pkg: {}
|
nodes/max/plantarium/output/pkg: {}
|
||||||
|
|
||||||
|
nodes/max/plantarium/random: {}
|
||||||
|
|
||||||
nodes/max/plantarium/random/pkg: {}
|
nodes/max/plantarium/random/pkg: {}
|
||||||
|
|
||||||
packages/input-elements:
|
packages/graph-interface:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nodes/types':
|
'@nodes/types':
|
||||||
specifier: link:../types
|
specifier: link:../types
|
||||||
version: link:../types
|
version: link:../types
|
||||||
|
'@nodes/ui':
|
||||||
|
specifier: link:../ui
|
||||||
|
version: link:../ui
|
||||||
|
'@threlte/core':
|
||||||
|
specifier: ^7.1.0
|
||||||
|
version: 7.1.0(svelte@4.2.12)(three@0.159.0)
|
||||||
|
'@threlte/extras':
|
||||||
|
specifier: ^8.7.5
|
||||||
|
version: 8.8.0(svelte@4.2.12)(three@0.159.0)
|
||||||
|
'@threlte/flex':
|
||||||
|
specifier: ^1.0.1
|
||||||
|
version: 1.0.1(svelte@4.2.12)(three@0.159.0)
|
||||||
|
'@types/three':
|
||||||
|
specifier: ^0.159.0
|
||||||
|
version: 0.159.0
|
||||||
|
jsondiffpatch:
|
||||||
|
specifier: ^0.6.0
|
||||||
|
version: 0.6.0
|
||||||
|
three:
|
||||||
|
specifier: ^0.159.0
|
||||||
|
version: 0.159.0
|
||||||
|
vite-plugin-glsl:
|
||||||
|
specifier: ^1.2.1
|
||||||
|
version: 1.2.1(vite@5.1.4)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@histoire/plugin-svelte':
|
||||||
|
specifier: ^0.17.9
|
||||||
|
version: 0.17.9(histoire@0.17.9)(svelte@4.2.12)(vite@5.1.4)
|
||||||
'@sveltejs/adapter-auto':
|
'@sveltejs/adapter-auto':
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.2.0(@sveltejs/kit@2.5.2)
|
version: 3.2.0(@sveltejs/kit@2.5.2)
|
||||||
@ -142,9 +163,18 @@ importers:
|
|||||||
eslint:
|
eslint:
|
||||||
specifier: ^8.56.0
|
specifier: ^8.56.0
|
||||||
version: 8.57.0
|
version: 8.57.0
|
||||||
|
eslint-config-prettier:
|
||||||
|
specifier: ^9.1.0
|
||||||
|
version: 9.1.0(eslint@8.57.0)
|
||||||
eslint-plugin-svelte:
|
eslint-plugin-svelte:
|
||||||
specifier: ^2.35.1
|
specifier: ^2.35.1
|
||||||
version: 2.35.1(eslint@8.57.0)(svelte@4.2.12)
|
version: 2.35.1(eslint@8.57.0)(svelte@4.2.12)
|
||||||
|
prettier:
|
||||||
|
specifier: ^3.1.1
|
||||||
|
version: 3.2.5
|
||||||
|
prettier-plugin-svelte:
|
||||||
|
specifier: ^3.1.2
|
||||||
|
version: 3.2.2(prettier@3.2.5)(svelte@4.2.12)
|
||||||
publint:
|
publint:
|
||||||
specifier: ^0.1.9
|
specifier: ^0.1.9
|
||||||
version: 0.1.16
|
version: 0.1.16
|
||||||
@ -163,9 +193,6 @@ importers:
|
|||||||
vite:
|
vite:
|
||||||
specifier: ^5.0.11
|
specifier: ^5.0.11
|
||||||
version: 5.1.4
|
version: 5.1.4
|
||||||
vitest:
|
|
||||||
specifier: ^1.2.0
|
|
||||||
version: 1.4.0
|
|
||||||
|
|
||||||
packages/node-registry:
|
packages/node-registry:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -232,6 +259,61 @@ importers:
|
|||||||
|
|
||||||
packages/types: {}
|
packages/types: {}
|
||||||
|
|
||||||
|
packages/ui:
|
||||||
|
dependencies:
|
||||||
|
'@nodes/types':
|
||||||
|
specifier: link:../types
|
||||||
|
version: link:../types
|
||||||
|
devDependencies:
|
||||||
|
'@sveltejs/adapter-auto':
|
||||||
|
specifier: ^3.0.0
|
||||||
|
version: 3.2.0(@sveltejs/kit@2.5.2)
|
||||||
|
'@sveltejs/kit':
|
||||||
|
specifier: ^2.0.0
|
||||||
|
version: 2.5.2(@sveltejs/vite-plugin-svelte@3.0.2)(svelte@4.2.12)(vite@5.1.4)
|
||||||
|
'@sveltejs/package':
|
||||||
|
specifier: ^2.0.0
|
||||||
|
version: 2.3.0(svelte@4.2.12)(typescript@5.3.3)
|
||||||
|
'@sveltejs/vite-plugin-svelte':
|
||||||
|
specifier: ^3.0.0
|
||||||
|
version: 3.0.2(svelte@4.2.12)(vite@5.1.4)
|
||||||
|
'@types/eslint':
|
||||||
|
specifier: ^8.56.0
|
||||||
|
version: 8.56.7
|
||||||
|
'@typescript-eslint/eslint-plugin':
|
||||||
|
specifier: ^7.0.0
|
||||||
|
version: 7.5.0(@typescript-eslint/parser@7.5.0)(eslint@8.57.0)(typescript@5.3.3)
|
||||||
|
'@typescript-eslint/parser':
|
||||||
|
specifier: ^7.0.0
|
||||||
|
version: 7.5.0(eslint@8.57.0)(typescript@5.3.3)
|
||||||
|
eslint:
|
||||||
|
specifier: ^8.56.0
|
||||||
|
version: 8.57.0
|
||||||
|
eslint-plugin-svelte:
|
||||||
|
specifier: ^2.35.1
|
||||||
|
version: 2.35.1(eslint@8.57.0)(svelte@4.2.12)
|
||||||
|
publint:
|
||||||
|
specifier: ^0.1.9
|
||||||
|
version: 0.1.16
|
||||||
|
svelte:
|
||||||
|
specifier: ^4.2.7
|
||||||
|
version: 4.2.12
|
||||||
|
svelte-check:
|
||||||
|
specifier: ^3.6.0
|
||||||
|
version: 3.6.4(postcss@8.4.35)(svelte@4.2.12)
|
||||||
|
tslib:
|
||||||
|
specifier: ^2.4.1
|
||||||
|
version: 2.6.2
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.0.0
|
||||||
|
version: 5.3.3
|
||||||
|
vite:
|
||||||
|
specifier: ^5.0.11
|
||||||
|
version: 5.1.4
|
||||||
|
vitest:
|
||||||
|
specifier: ^1.2.0
|
||||||
|
version: 1.4.0
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
/@aashutoshrathi/word-wrap@1.2.6:
|
/@aashutoshrathi/word-wrap@1.2.6:
|
||||||
@ -706,7 +788,6 @@ packages:
|
|||||||
'@types/estree': 1.0.5
|
'@types/estree': 1.0.5
|
||||||
estree-walker: 2.0.2
|
estree-walker: 2.0.2
|
||||||
picomatch: 2.3.1
|
picomatch: 2.3.1
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@rollup/rollup-android-arm-eabi@4.12.0:
|
/@rollup/rollup-android-arm-eabi@4.12.0:
|
||||||
resolution: {integrity: sha512-+ac02NL/2TCKRrJu2wffk1kZ+RyqxVUlbjSagNgPm94frxtr+XDL12E5Ll1enWskLrtrZ2r8L3wED1orIibV/w==}
|
resolution: {integrity: sha512-+ac02NL/2TCKRrJu2wffk1kZ+RyqxVUlbjSagNgPm94frxtr+XDL12E5Ll1enWskLrtrZ2r8L3wED1orIibV/w==}
|
||||||
@ -1997,7 +2078,6 @@ packages:
|
|||||||
|
|
||||||
/estree-walker@2.0.2:
|
/estree-walker@2.0.2:
|
||||||
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/estree-walker@3.0.3:
|
/estree-walker@3.0.3:
|
||||||
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
||||||
@ -2739,14 +2819,6 @@ packages:
|
|||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/meshline@3.2.0(three@0.159.0):
|
|
||||||
resolution: {integrity: sha512-ZaJkC967GTuef7UBdO0rGPX544oIWaNo7tYedVHSoR2lje6RR16fX8IsgMxgxoYYERtjqsRWIYBSPBxG4QR84Q==}
|
|
||||||
peerDependencies:
|
|
||||||
three: '>=0.137'
|
|
||||||
dependencies:
|
|
||||||
three: 0.159.0
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/meshoptimizer@0.18.1:
|
/meshoptimizer@0.18.1:
|
||||||
resolution: {integrity: sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==}
|
resolution: {integrity: sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -3076,7 +3148,6 @@ packages:
|
|||||||
/picomatch@2.3.1:
|
/picomatch@2.3.1:
|
||||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||||
engines: {node: '>=8.6'}
|
engines: {node: '>=8.6'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/pkg-types@1.0.3:
|
/pkg-types@1.0.3:
|
||||||
resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==}
|
resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==}
|
||||||
@ -3624,10 +3695,6 @@ packages:
|
|||||||
tweakpane: 3.1.10
|
tweakpane: 3.1.10
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/three.meshline@1.4.0:
|
|
||||||
resolution: {integrity: sha512-A8IsiMrWP8zmHisGDAJ76ZD7t/dOF/oCe/FUKNE6Bu01ZYEx8N6IlU/1Plb2aOZtAuWM2A8s8qS3hvY0OFuvOw==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/three@0.159.0:
|
/three@0.159.0:
|
||||||
resolution: {integrity: sha512-eCmhlLGbBgucuo4VEA9IO3Qpc7dh8Bd4VKzr7WfW4+8hMcIfoAVi1ev0pJYN9PTTsCslbcKgBwr2wNZ1EvLInA==}
|
resolution: {integrity: sha512-eCmhlLGbBgucuo4VEA9IO3Qpc7dh8Bd4VKzr7WfW4+8hMcIfoAVi1ev0pJYN9PTTsCslbcKgBwr2wNZ1EvLInA==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -3882,7 +3949,6 @@ packages:
|
|||||||
vite: 5.1.4
|
vite: 5.1.4
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- rollup
|
- rollup
|
||||||
dev: true
|
|
||||||
|
|
||||||
/vite-plugin-wasm@3.3.0(vite@5.1.4):
|
/vite-plugin-wasm@3.3.0(vite@5.1.4):
|
||||||
resolution: {integrity: sha512-tVhz6w+W9MVsOCHzxo6SSMSswCeIw4HTrXEi6qL3IRzATl83jl09JVO1djBqPSwfjgnpVHNLYcaMbaDX5WB/pg==}
|
resolution: {integrity: sha512-tVhz6w+W9MVsOCHzxo6SSMSswCeIw4HTrXEi6qL3IRzATl83jl09JVO1djBqPSwfjgnpVHNLYcaMbaDX5WB/pg==}
|
||||||
|
Loading…
Reference in New Issue
Block a user