feat: initial auto connect nodes
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m35s
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m35s
This commit is contained in:
@@ -1,21 +1,26 @@
|
||||
<script lang="ts">
|
||||
import type { GraphManager } from "./graph-manager.svelte";
|
||||
import { HTML } from "@threlte/extras";
|
||||
import { onMount } from "svelte";
|
||||
import type { NodeType } from "@nodes/types";
|
||||
import type { Node, NodeType } from "@nodes/types";
|
||||
import { getGraphState } from "./graph/state.svelte";
|
||||
import { getGraphManager } from "./graph/context";
|
||||
|
||||
type Props = {
|
||||
position: [x: number, y: number] | null;
|
||||
graph: GraphManager;
|
||||
};
|
||||
|
||||
let { position = $bindable(), graph }: Props = $props();
|
||||
const graph = getGraphManager();
|
||||
const graphState = getGraphState();
|
||||
|
||||
let { position = $bindable() }: Props = $props();
|
||||
|
||||
let input: HTMLInputElement;
|
||||
let value = $state<string>();
|
||||
let activeNodeId = $state<NodeType>();
|
||||
|
||||
const allNodes = graph.getNodeDefinitions();
|
||||
const allNodes = graphState.activeSocket
|
||||
? graph.getPossibleNodes(graphState.activeSocket)
|
||||
: graph.getNodeDefinitions();
|
||||
|
||||
function filterNodes() {
|
||||
return allNodes.filter((node) => node.id.includes(value ?? ""));
|
||||
@@ -25,7 +30,7 @@
|
||||
$effect(() => {
|
||||
if (nodes) {
|
||||
if (activeNodeId === undefined) {
|
||||
activeNodeId = nodes[0].id;
|
||||
activeNodeId = nodes?.[0]?.id;
|
||||
} else if (nodes.length) {
|
||||
const node = nodes.find((node) => node.id === activeNodeId);
|
||||
if (!node) {
|
||||
@@ -35,6 +40,28 @@
|
||||
}
|
||||
});
|
||||
|
||||
function handleNodeCreation(nodeType: Node["type"]) {
|
||||
if (!position) return;
|
||||
|
||||
const newNode = graph.createNode({
|
||||
type: nodeType,
|
||||
position,
|
||||
props: {},
|
||||
});
|
||||
|
||||
const edgeInputSocket = graphState.activeSocket;
|
||||
if (edgeInputSocket && newNode) {
|
||||
if (typeof edgeInputSocket.index === "number") {
|
||||
graph.smartConnect(edgeInputSocket.node, newNode);
|
||||
} else {
|
||||
graph.smartConnect(newNode, edgeInputSocket.node);
|
||||
}
|
||||
}
|
||||
|
||||
graphState.activeSocket = null;
|
||||
position = null;
|
||||
}
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
@@ -57,8 +84,7 @@
|
||||
|
||||
if (event.key === "Enter") {
|
||||
if (activeNodeId && position) {
|
||||
graph.createNode({ type: activeNodeId, position, props: {} });
|
||||
position = null;
|
||||
handleNodeCreation(activeNodeId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -95,18 +121,10 @@
|
||||
aria-selected={node.id === activeNodeId}
|
||||
onkeydown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
if (position) {
|
||||
graph.createNode({ type: node.id, position, props: {} });
|
||||
position = null;
|
||||
}
|
||||
}
|
||||
}}
|
||||
onmousedown={() => {
|
||||
if (position) {
|
||||
graph.createNode({ type: node.id, position, props: {} });
|
||||
position = null;
|
||||
handleNodeCreation(node.id);
|
||||
}
|
||||
}}
|
||||
onmousedown={() => handleNodeCreation(node.id)}
|
||||
onfocus={() => {
|
||||
activeNodeId = node.id;
|
||||
}}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
Edge,
|
||||
Graph,
|
||||
Node,
|
||||
NodeDefinition,
|
||||
NodeInput,
|
||||
NodeRegistry,
|
||||
NodeType,
|
||||
@@ -15,7 +16,7 @@ import throttle from "$lib/helpers/throttle";
|
||||
import { HistoryManager } from "./history-manager";
|
||||
|
||||
const logger = createLogger("graph-manager");
|
||||
// logger.mute();
|
||||
logger.mute();
|
||||
|
||||
const clone =
|
||||
"structuredClone" in self
|
||||
@@ -361,6 +362,20 @@ export class GraphManager extends EventEmitter<{
|
||||
this.save();
|
||||
}
|
||||
|
||||
smartConnect(from: Node, to: Node): Edge | undefined {
|
||||
const inputs = Object.entries(to.tmp?.type?.inputs ?? {});
|
||||
const outputs = from.tmp?.type?.outputs ?? [];
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
const [inputName, input] = inputs[0];
|
||||
for (let o = 0; o < outputs.length; o++) {
|
||||
const output = outputs[0];
|
||||
if (input.type === output) {
|
||||
return this.createEdge(from, o, to, inputName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createNodeId() {
|
||||
const max = Math.max(0, ...this.nodes.keys());
|
||||
return max + 1;
|
||||
@@ -370,10 +385,9 @@ export class GraphManager extends EventEmitter<{
|
||||
// map old ids to new ids
|
||||
const idMap = new Map<number, number>();
|
||||
|
||||
const startId = this.createNodeId();
|
||||
|
||||
nodes = nodes.map((node, i) => {
|
||||
const id = startId + i;
|
||||
const id = this.createNodeId();
|
||||
idMap.set(node.id, id);
|
||||
const type = this.registry.getNode(node.type);
|
||||
if (!type) {
|
||||
@@ -437,6 +451,8 @@ export class GraphManager extends EventEmitter<{
|
||||
this.nodes.set(node.id, node);
|
||||
|
||||
this.save();
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
createEdge(
|
||||
@@ -445,7 +461,10 @@ export class GraphManager extends EventEmitter<{
|
||||
to: Node,
|
||||
toSocket: string,
|
||||
{ applyUpdate = true } = {},
|
||||
) {
|
||||
): Edge | undefined {
|
||||
|
||||
console.log("Create Edge", from.type, fromSocket, to.type, toSocket)
|
||||
|
||||
const existingEdges = this.getEdgesToNode(to);
|
||||
|
||||
// check if this exact edge already exists
|
||||
@@ -464,6 +483,8 @@ export class GraphManager extends EventEmitter<{
|
||||
toSocketType.push(...(to?.tmp?.type?.inputs?.[toSocket]?.accepts || []));
|
||||
}
|
||||
|
||||
console.log({ fromSocketType, toSocket, toType: to?.tmp?.type, toSocketType });
|
||||
|
||||
if (!areSocketsCompatible(fromSocketType, toSocketType)) {
|
||||
logger.error(
|
||||
`Socket types do not match: ${fromSocketType} !== ${toSocketType}`,
|
||||
@@ -478,11 +499,9 @@ export class GraphManager extends EventEmitter<{
|
||||
this.removeEdge(edgeToBeReplaced, { applyDeletion: false });
|
||||
}
|
||||
|
||||
if (applyUpdate) {
|
||||
this.edges.push([from, fromSocket, to, toSocket]);
|
||||
} else {
|
||||
this.edges.push([from, fromSocket, to, toSocket]);
|
||||
}
|
||||
const edge = [from, fromSocket, to, toSocket] as Edge;
|
||||
|
||||
this.edges.push(edge);
|
||||
|
||||
from.tmp = from.tmp || {};
|
||||
from.tmp.children = from.tmp.children || [];
|
||||
@@ -496,6 +515,8 @@ export class GraphManager extends EventEmitter<{
|
||||
this.save();
|
||||
}
|
||||
this.execute();
|
||||
|
||||
return edge;
|
||||
}
|
||||
|
||||
undo() {
|
||||
@@ -547,6 +568,33 @@ export class GraphManager extends EventEmitter<{
|
||||
return parents.reverse();
|
||||
}
|
||||
|
||||
getPossibleNodes(socket: Socket): NodeDefinition[] {
|
||||
const allDefinitions = this.getNodeDefinitions();
|
||||
|
||||
const nodeType = socket.node.tmp?.type;
|
||||
if (!nodeType) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (typeof socket.index === "string") {
|
||||
// if index is a string, we are an input looking for outputs
|
||||
return allDefinitions.filter(s => {
|
||||
return s.outputs?.find(_s => Object
|
||||
.values(nodeType?.inputs || {})
|
||||
.map(s => s.type)
|
||||
.includes(_s as NodeInput["type"])
|
||||
)
|
||||
})
|
||||
} else {
|
||||
// if index is a number, we are an output looking for inputs
|
||||
return allDefinitions.filter(s => Object
|
||||
.values(s.inputs ?? {})
|
||||
.map(s => s.type)
|
||||
.find(s => nodeType?.outputs?.includes(s))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
getPossibleSockets({ node, index }: Socket): [Node, string | number][] {
|
||||
const nodeType = node?.tmp?.type;
|
||||
if (!nodeType) return [];
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
let boxSelection = $state(false);
|
||||
const cameraDown = [0, 0];
|
||||
let cameraPosition: [number, number, number] = $state([0, 0, 4]);
|
||||
let edgeEndPosition = $state<[number, number] | null>();
|
||||
let addMenuPosition = $state<[number, number] | null>(null);
|
||||
let clipboard: null | {
|
||||
nodes: Node[];
|
||||
@@ -465,6 +466,8 @@
|
||||
n.tmp.downY = n.position[1];
|
||||
}
|
||||
}
|
||||
|
||||
edgeEndPosition = null;
|
||||
}
|
||||
|
||||
function copyNodes() {
|
||||
@@ -478,17 +481,15 @@
|
||||
.filter(Boolean) as Node[];
|
||||
|
||||
const _edges = graph.getEdgesBetweenNodes(_nodes);
|
||||
|
||||
_nodes = _nodes.map((_node) => {
|
||||
const node = globalThis.structuredClone({
|
||||
_nodes = $state.snapshot(
|
||||
_nodes.map((_node) => ({
|
||||
..._node,
|
||||
tmp: {
|
||||
downX: mousePosition[0] - _node.position[0],
|
||||
downY: mousePosition[1] - _node.position[1],
|
||||
},
|
||||
});
|
||||
return node;
|
||||
});
|
||||
})),
|
||||
);
|
||||
|
||||
clipboard = {
|
||||
nodes: _nodes,
|
||||
@@ -532,6 +533,18 @@
|
||||
},
|
||||
});
|
||||
|
||||
keymap.addShortcut({
|
||||
key: "f",
|
||||
description: "Smart Connect Nodes",
|
||||
callback: () => {
|
||||
const nodes = [...graphState.selectedNodes.values()]
|
||||
.map((g) => graph.getNode(g))
|
||||
.filter((n) => !!n);
|
||||
const edge = graph.smartConnect(nodes[0], nodes[1]);
|
||||
if (!edge) graph.smartConnect(nodes[1], nodes[0]);
|
||||
},
|
||||
});
|
||||
|
||||
keymap.addShortcut({
|
||||
key: "?",
|
||||
description: "Toggle Help",
|
||||
@@ -561,6 +574,7 @@
|
||||
callback: () => {
|
||||
graphState.activeNodeId = -1;
|
||||
graphState.clearSelection();
|
||||
edgeEndPosition = null;
|
||||
(document.activeElement as HTMLElement)?.blur();
|
||||
},
|
||||
});
|
||||
@@ -778,6 +792,16 @@
|
||||
);
|
||||
}
|
||||
graph.save();
|
||||
} else if (graphState.activeSocket && event.ctrlKey) {
|
||||
// Handle automatic adding of nodes on ctrl+mouseUp
|
||||
edgeEndPosition = [mousePosition[0], mousePosition[1]];
|
||||
|
||||
if (typeof graphState.activeSocket.index === "number") {
|
||||
addMenuPosition = [mousePosition[0], mousePosition[1] - 3];
|
||||
} else {
|
||||
addMenuPosition = [mousePosition[0] - 20, mousePosition[1] - 3];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// check if camera moved
|
||||
@@ -953,7 +977,7 @@
|
||||
|
||||
{#if graph.status === "idle"}
|
||||
{#if addMenuPosition}
|
||||
<AddMenu bind:position={addMenuPosition} {graph} />
|
||||
<AddMenu bind:position={addMenuPosition} />
|
||||
{/if}
|
||||
|
||||
{#if graphState.activeSocket}
|
||||
@@ -963,7 +987,10 @@
|
||||
x: graphState.activeSocket.position[0],
|
||||
y: graphState.activeSocket.position[1],
|
||||
}}
|
||||
to={{ x: mousePosition[0], y: mousePosition[1] }}
|
||||
to={{
|
||||
x: edgeEndPosition?.[0] ?? mousePosition[0],
|
||||
y: edgeEndPosition?.[1] ?? mousePosition[1],
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { clone } from "./helpers/index.js";
|
||||
import { createLogger } from "@nodes/utils";
|
||||
|
||||
const diff = create({
|
||||
objectHash: function (obj, index) {
|
||||
objectHash: function(obj, index) {
|
||||
if (obj === null) return obj;
|
||||
if ("id" in obj) return obj.id as string;
|
||||
if ("_id" in obj) return obj._id as string;
|
||||
@@ -16,7 +16,7 @@ const diff = create({
|
||||
});
|
||||
|
||||
const log = createLogger("history");
|
||||
// log.mute();
|
||||
log.mute();
|
||||
|
||||
export class HistoryManager {
|
||||
index: number = -1;
|
||||
|
||||
@@ -25,7 +25,10 @@
|
||||
<div class="wrapper">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr on:click={() => ($open.runtime = !$open.runtime)}>
|
||||
<tr
|
||||
style="cursor:pointer;"
|
||||
on:click={() => ($open.runtime = !$open.runtime)}
|
||||
>
|
||||
<td>{$open.runtime ? "-" : "+"} runtime </td>
|
||||
<td>{humanizeDuration(runtime || 1000)}</td>
|
||||
</tr>
|
||||
@@ -37,7 +40,7 @@
|
||||
</tr>
|
||||
{/if}
|
||||
|
||||
<tr on:click={() => ($open.fps = !$open.fps)}>
|
||||
<tr style="cursor:pointer;" on:click={() => ($open.fps = !$open.fps)}>
|
||||
<td>{$open.fps ? "-" : "+"} fps </td>
|
||||
<td>
|
||||
{Math.floor(fps[fps.length - 1])}fps
|
||||
@@ -74,9 +77,6 @@
|
||||
border: solid thin var(--outline);
|
||||
border-collapse: collapse;
|
||||
}
|
||||
tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
td {
|
||||
padding: 4px;
|
||||
padding-inline: 8px;
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from "@nodes/utils";
|
||||
|
||||
const log = createLogger("runtime-executor");
|
||||
// log.mute();
|
||||
log.mute();
|
||||
|
||||
function getValue(input: NodeInput, value?: unknown) {
|
||||
if (value === undefined && "value" in input) {
|
||||
@@ -65,7 +65,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
|
||||
constructor(
|
||||
private registry: NodeRegistry,
|
||||
private cache?: SyncCache<Int32Array>,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
private async getNodeDefinitions(graph: Graph) {
|
||||
if (this.registry.status !== "ready") {
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
{type[key].label || key}
|
||||
</button>
|
||||
{:else}
|
||||
{#if type[key]?.label !== false}
|
||||
{#if type[key].label !== ""}
|
||||
<label for={id}>{type[key].label || key}</label>
|
||||
{/if}
|
||||
<Input {id} input={type[key]} bind:value={internalValue} />
|
||||
@@ -196,6 +196,10 @@
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
hr {
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { localState } from "$lib/helpers/localState.svelte";
|
||||
import type { NodeInput } from "@nodes/types";
|
||||
import type { SettingsType } from ".";
|
||||
|
||||
const themes = [
|
||||
"dark",
|
||||
@@ -10,7 +8,7 @@ const themes = [
|
||||
"high-contrast",
|
||||
"nord",
|
||||
"dracula",
|
||||
];
|
||||
] as const;
|
||||
|
||||
export const AppSettingTypes = {
|
||||
theme: {
|
||||
@@ -49,11 +47,6 @@ export const AppSettingTypes = {
|
||||
},
|
||||
debug: {
|
||||
title: "Debug",
|
||||
amount: {
|
||||
type: "number",
|
||||
label: "Amount",
|
||||
value: 4,
|
||||
},
|
||||
wireframe: {
|
||||
type: "boolean",
|
||||
label: "Wireframe",
|
||||
@@ -89,14 +82,6 @@ export const AppSettingTypes = {
|
||||
label: "Show Stem Lines",
|
||||
value: false,
|
||||
},
|
||||
logging: {
|
||||
title: "Logging",
|
||||
logLevel: {
|
||||
type: "select",
|
||||
label: false,
|
||||
options: ["info","warning","error"]
|
||||
}
|
||||
},
|
||||
stressTest: {
|
||||
title: "Stress Test",
|
||||
amount: {
|
||||
@@ -127,32 +112,23 @@ export const AppSettingTypes = {
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const satisfies SettingsType;
|
||||
} as const;
|
||||
|
||||
type IsInputDefinition<T> = T extends NodeInput ? T : never;
|
||||
type HasTitle = { title: string };
|
||||
type SettingsToStore<T> =
|
||||
T extends { value: infer V }
|
||||
? V extends readonly string[]
|
||||
? V[number]
|
||||
: V
|
||||
: T extends any[]
|
||||
? {}
|
||||
: T extends object
|
||||
? {
|
||||
[K in keyof T as T[K] extends object ? K : never]:
|
||||
SettingsToStore<T[K]>
|
||||
}
|
||||
: never;
|
||||
|
||||
type Widen<T> = T extends boolean
|
||||
? boolean
|
||||
: T extends number
|
||||
? number
|
||||
: T extends string
|
||||
? string
|
||||
: T;
|
||||
|
||||
type ExtractSettingsValues<T> = {
|
||||
-readonly [K in keyof T]: T[K] extends HasTitle
|
||||
? ExtractSettingsValues<Omit<T[K], "title">>
|
||||
: T[K] extends IsInputDefinition<T[K]>
|
||||
? T[K] extends { value: infer V }
|
||||
? Widen<V>
|
||||
: never
|
||||
: T[K] extends Record<string, any>
|
||||
? ExtractSettingsValues<T[K]>
|
||||
: never;
|
||||
};
|
||||
|
||||
export function settingsToStore<T>(settings: T): ExtractSettingsValues<T> {
|
||||
export function settingsToStore<T>(settings: T): SettingsToStore<T> {
|
||||
const result = {} as any;
|
||||
for (const key in settings) {
|
||||
const value = settings[key];
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
align-items: center;
|
||||
border-bottom: solid thin var(--outline);
|
||||
border-left: solid thin var(--outline);
|
||||
background: var(--layer-0);
|
||||
background: var(--layer-1);
|
||||
}
|
||||
|
||||
.tabs > button > span {
|
||||
@@ -124,7 +124,7 @@
|
||||
}
|
||||
|
||||
.tabs > button.active {
|
||||
background: var(--layer-1);
|
||||
background: var(--layer-2);
|
||||
}
|
||||
|
||||
.tabs > button.active span {
|
||||
|
||||
@@ -49,6 +49,9 @@
|
||||
? JSON.parse(localStorage.getItem("graph")!)
|
||||
: templates.defaultPlant,
|
||||
);
|
||||
function handleSave(graph: Graph) {
|
||||
localStorage.setItem("graph", JSON.stringify(graph));
|
||||
}
|
||||
|
||||
let graphInterface = $state<ReturnType<typeof GraphInterface>>(null!);
|
||||
let viewerComponent = $state<ReturnType<typeof Viewer>>();
|
||||
@@ -122,32 +125,30 @@
|
||||
$effect(() => {
|
||||
//@ts-ignore
|
||||
AppSettingTypes.debug.stressTest.loadGrid.callback = () => {
|
||||
graph = templates.grid(
|
||||
appSettings.value.debug.amount.value,
|
||||
appSettings.value.debug.amount.value,
|
||||
manager.load(
|
||||
templates.grid(
|
||||
appSettings.value.debug.stressTest.amount,
|
||||
appSettings.value.debug.stressTest.amount,
|
||||
),
|
||||
);
|
||||
};
|
||||
//@ts-ignore
|
||||
AppSettingTypes.debug.stressTest.loadTree.callback = () => {
|
||||
graph = templates.tree(appSettings.value.debug.amount.value);
|
||||
manager.load(templates.tree(appSettings.value.debug.stressTest.amount));
|
||||
};
|
||||
//@ts-ignore
|
||||
AppSettingTypes.debug.stressTest.lottaFaces.callback = () => {
|
||||
graph = templates.lottaFaces;
|
||||
manager.load(templates.lottaFaces as unknown as Graph);
|
||||
};
|
||||
//@ts-ignore
|
||||
AppSettingTypes.debug.stressTest.lottaNodes.callback = () => {
|
||||
graph = templates.lottaNodes;
|
||||
manager.load(templates.lottaNodes as unknown as Graph);
|
||||
};
|
||||
//@ts-ignore
|
||||
AppSettingTypes.debug.stressTest.lottaNodesAndFaces.callback = () => {
|
||||
graph = templates.lottaNodesAndFaces;
|
||||
manager.load(templates.lottaNodesAndFaces as unknown as Graph);
|
||||
};
|
||||
});
|
||||
|
||||
function handleSave(graph: Graph) {
|
||||
localStorage.setItem("graph", JSON.stringify(graph));
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:document onkeydown={applicationKeymap.handleKeyboardEvent} />
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import { createLogger, createWasmWrapper } from "@nodes/utils";
|
||||
|
||||
const log = createLogger("node-registry");
|
||||
// log.mute();
|
||||
log.mute();
|
||||
|
||||
export class RemoteNodeRegistry implements NodeRegistry {
|
||||
status: "loading" | "ready" | "error" = "loading";
|
||||
|
||||
Reference in New Issue
Block a user