Skip to content

Commit b5b0bf9

Browse files
committed
Node system: Quick add and connect when connecting to air
1 parent 4f196b7 commit b5b0bf9

7 files changed

Lines changed: 102 additions & 202 deletions

File tree

lab/node-system/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ Node network view navigation controls:
2323
- Drag and drop socket of the same type to connect.
2424
- Most of the socket only accepts at most 1 input.
2525
- Some socket can accept more than 1 input.
26+
- Quickly create and connect to new node by dragging socket to the air.
27+
- Only socket of compatible type will be shown.
2628

2729
## Goals
2830
In order to consider this experiment a success, all required goals must be achieved.

lab/node-system/demo/nodes.base.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Node, type NodeCreateOptions, type NodePart, type Socket } from "../mod.ts";
1+
import { Node, type NodeCreateOptions, type NodePart, type Socket, SocketInfo } from "../mod.ts";
22

33
export abstract class WgslNode extends Node {
44
abstract wgslSetup(ctx: WgslContext): void;
@@ -14,6 +14,7 @@ export interface WgslContext {
1414
export interface NodeFactory<N extends Node> {
1515
readonly name: string;
1616
type: { new (options: NodeCreateOptions): N };
17+
initials?: SocketInfo[];
1718
populatePartInterface?(node: N, part: NodePart): Element;
1819
}
1920

lab/node-system/demo/nodes.shader.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,14 @@ export class VertexInputNode extends WgslNode {
1919
}
2020
}
2121

22-
static readonly factory: NodeFactory<VertexInputNode> = { name: "Vertex Input", type: VertexInputNode };
22+
static readonly factory: NodeFactory<VertexInputNode> = {
23+
name: "Vertex Input",
24+
type: VertexInputNode,
25+
initials: [
26+
{ id: "position", direction: "out", type: "vec4f", name: "Position" },
27+
{ id: "uv", direction: "out", type: "vec2f", name: "UV" },
28+
],
29+
};
2330
}
2431

2532
export class RenderOutputNode extends Node {
@@ -96,6 +103,7 @@ export class RenderOutputNode extends Node {
96103
return {
97104
name: "Render Output",
98105
type: RenderOutputNode,
106+
initials: [{ id: "color", direction: "in", type: "vec4f", name: "Color" }],
99107
populatePartInterface: (node, part) => {
100108
const div = document.createElement("div");
101109
if (part != node.uiPart) return div;

lab/node-system/demo/nodes.ts

Lines changed: 0 additions & 193 deletions
This file was deleted.

lab/node-system/demo/nodes.vector.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { NodeCreateOptions, NodePart, Socket } from "../mod.ts";
1+
import type { NodeCreateOptions, NodePart, Socket, SocketDirection } from "../mod.ts";
22
import { createNumberInputField, type NodeFactory, type WgslContext, WgslNode } from "./nodes.base.ts";
33

44
interface GeneratedNodeClass<T = object> {
@@ -26,6 +26,7 @@ export class ConstF32Node extends WgslNode {
2626
static readonly factory: NodeFactory<ConstF32Node> = {
2727
name: "Scalar",
2828
type: ConstF32Node,
29+
initials: [{ id: "constant", direction: "out", type: "f32", name: "Constant" }],
2930
populatePartInterface: (node, part) => {
3031
const div = document.createElement("div");
3132
if (part != node.uiPart) return div;
@@ -64,6 +65,7 @@ export function constantVectorNode(dim: number, dtype: string, name: string): Ge
6465
static readonly factory: NodeFactory<ConstantVectorNode> = {
6566
name,
6667
type: ConstantVectorNode,
68+
initials: [{ id: "constant", direction: "out", type: dtype, name: "Constant" }],
6769
populatePartInterface: (node, part) => {
6870
const div = document.createElement("div");
6971
if (part != node.uiPart) return div;
@@ -115,7 +117,19 @@ export function splitVectorNode(components: [string, string][], dtype: string, n
115117
return `${ctx.nameOf("input")}.${components[idx][0]}`;
116118
}
117119

118-
static readonly factory: NodeFactory<SplitVectorNode> = { name, type: SplitVectorNode };
120+
static readonly factory: NodeFactory<SplitVectorNode> = {
121+
name,
122+
type: SplitVectorNode,
123+
initials: [
124+
{ id: "input", direction: "in", type: dtype, name: "Vector" },
125+
...components.map(([id, label]) => ({
126+
id,
127+
direction: "out" as SocketDirection,
128+
type: "f32",
129+
name: label,
130+
})),
131+
],
132+
};
119133
};
120134
}
121135

@@ -155,7 +169,19 @@ export function mergeVectorNode(components: [string, string][], dtype: string, n
155169
throw new Error("Invalid socket");
156170
}
157171

158-
static readonly factory: NodeFactory<MergeVectorNode> = { name, type: MergeVectorNode };
172+
static readonly factory: NodeFactory<MergeVectorNode> = {
173+
name,
174+
type: MergeVectorNode,
175+
initials: [
176+
{ id: "output", direction: "out", type: dtype, name: "Vector" },
177+
...components.map(([id, label]) => ({
178+
id,
179+
direction: "in" as SocketDirection,
180+
type: "f32",
181+
name: label,
182+
})),
183+
],
184+
};
159185
};
160186
}
161187

@@ -204,6 +230,11 @@ export function mathNode(dtype: string, name: string): GeneratedNodeClass {
204230
static readonly factory: NodeFactory<MathNode> = {
205231
name,
206232
type: MathNode,
233+
initials: [
234+
{ id: "a", direction: "in", type: dtype, name: "A" },
235+
{ id: "b", direction: "in", type: dtype, name: "B" },
236+
{ id: "output", direction: "out", type: dtype, name: "Output" },
237+
],
207238
populatePartInterface: (node) => {
208239
const select = document.createElement("select");
209240
select.style.width = "100%";

lab/node-system/index.ts

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ view.addEventListener("pointerdown", (e) => {
6060

6161
view.addEventListener("nodeoption", (e) => {
6262
existingMenu?.remove();
63-
6463
const menu = document.createElement("div");
6564
menu.classList.add("context-menu");
6665
menu.style.left = `${e.parent.clientX}px`;
@@ -133,7 +132,6 @@ view.addEventListener("nodeoption", (e) => {
133132
view.addEventListener("contextmenu", (e) => e.preventDefault());
134133

135134
const partToElement = new Map<NodePart, Element>();
136-
137135
view.addEventListener("partshow", (e) => {
138136
const factory = factories.find((factory) => typeof factory != "string" && e.part.node instanceof factory.type);
139137
if (factory == null) return;
@@ -145,7 +143,6 @@ view.addEventListener("partshow", (e) => {
145143
partToElement.set(e.part, element);
146144
}
147145
});
148-
149146
view.addEventListener("parthide", (e) => {
150147
const element = partToElement.get(e.part);
151148

@@ -154,6 +151,52 @@ view.addEventListener("parthide", (e) => {
154151
partToElement.delete(e.part);
155152
}
156153
});
154+
view.addEventListener("airconnect", (e) => {
155+
if (e.source.direction == "in" && e.source.targets.size >= e.source.maxInputs) return;
156+
157+
existingMenu?.remove();
158+
const menu = document.createElement("div");
159+
menu.classList.add("context-menu");
160+
menu.style.left = `${e.parent.clientX}px`;
161+
menu.style.top = `${e.parent.clientY + 1}px`;
162+
existingMenu = menu;
163+
164+
factories.forEach((factory) => {
165+
if (typeof factory == "string") return;
166+
if (factory.initials == null) return;
167+
const targets = factory.initials.filter((socket) =>
168+
socket.type == e.source.type && socket.direction != e.source.direction
169+
);
170+
if (targets.length == 0) return;
171+
172+
const label = document.createElement("div");
173+
label.classList.add("label");
174+
label.textContent = factory.name;
175+
menu.append(label);
176+
177+
for (const socket of targets) {
178+
const btn = document.createElement("button");
179+
btn.textContent = socket.name ?? socket.id;
180+
181+
btn.addEventListener("click", () => {
182+
const node = new factory.type({
183+
name: factory.name,
184+
x: e.parent.clientX - view.panX - (socket.direction == "in" ? 0 : 160),
185+
y: e.parent.clientY - view.panY,
186+
});
187+
view.network?.addNode(node);
188+
node.sockets.get(socket.id)?.connect(e.source);
189+
190+
existingMenu?.remove();
191+
existingMenu = null;
192+
});
193+
194+
menu.append(btn);
195+
}
196+
});
197+
198+
document.body.append(menu);
199+
});
157200

158201
document.body.append(view);
159202
view.network = new Network();

0 commit comments

Comments
 (0)