@elicdavis/node-flow 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.html CHANGED
@@ -8,7 +8,7 @@
8
8
  <link rel="shortcut icon" href="#" />
9
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
- <link href="https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&display=swap"
11
+ <link href="https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&family=VT323&display=swap"
12
12
  rel="stylesheet">
13
13
  <style>
14
14
  * {
@@ -192,7 +192,7 @@
192
192
  import {
193
193
  Theme, FlowNode, FlowNote, NodeFlowGraph, Publisher,
194
194
  MessageType, ContextMenuItemState, DefaultConnectionRenderer,
195
- WidgetBase, ValueWidget, drawFieldBox, widgetFont,
195
+ ValueWidget, drawFieldBox, widgetFont,
196
196
  NumberWidget, StringWidget, ToggleWidget, ButtonWidget, TextWidget, ImageWidget, RowWidget,
197
197
  setTheme, onThemeChange,
198
198
  } from "./dist/web/NodeFlow.js";
@@ -253,6 +253,7 @@
253
253
  },
254
254
  Note: {
255
255
  FontColor: "#ffffff",
256
+ CodeFontFamily: "VT323",
256
257
  CodeBlock: { BackgroundColor: "#000000", FontColor: "#c0c0c0", BorderRadius: 0 },
257
258
  },
258
259
  },
@@ -352,46 +353,6 @@
352
353
  }
353
354
  }
354
355
 
355
- class MorePortsWidget extends WidgetBase {
356
- #ports;
357
- #open = false;
358
-
359
- constructor(ports) {
360
- super();
361
- this.#ports = ports;
362
- }
363
-
364
- Size() {
365
- return { x: 150, y: 16 };
366
- }
367
-
368
- render(ctx, box, scale) {
369
- const waiting = this.#ports.filter((p) => p.connections().length === 0).length;
370
- const label = this.#open ? "▴ less" : `▾ ${waiting} more`;
371
- const y = box.Position.y + box.Size.y / 2;
372
- const cx = box.Position.x + box.Size.x / 2;
373
- ctx.font = widgetFont(scale * 0.8);
374
- const half = ctx.measureText(label).width / 2 + 6 * scale;
375
- ctx.globalAlpha = this.hovered() ? 1 : 0.55;
376
- ctx.strokeStyle = ctx.fillStyle = Theme.Widget.FontColor;
377
- ctx.lineWidth = Math.max(1, scale);
378
- ctx.beginPath();
379
- ctx.moveTo(box.Position.x, y);
380
- ctx.lineTo(cx - half, y);
381
- ctx.moveTo(cx + half, y);
382
- ctx.lineTo(box.Position.x + box.Size.x, y);
383
- ctx.stroke();
384
- ctx.textAlign = "center";
385
- ctx.textBaseline = "middle";
386
- ctx.fillText(label, cx, y);
387
- }
388
-
389
- onClick() {
390
- this.#open = !this.#open;
391
- this.#ports.forEach((p) => p.setHidden(!this.#open));
392
- }
393
- }
394
-
395
356
  const mathPublisher = new Publisher({
396
357
  name: "Math",
397
358
  description: "Nodes the live example evaluates",
@@ -745,7 +706,7 @@ inputs: [
745
706
 
746
707
  A port can start **hidden**, to keep busy nodes small. A hidden port still shows while something is wired to it, so no wire ever points at nothing.
747
708
 
748
- The "more" line is a custom widget that shows and hides them.
709
+ A node with hidden ports gets a **more** line under them. Click it to show or hide them all.
749
710
 
750
711
  \`\`\`
751
712
  inputs: [
@@ -770,15 +731,52 @@ port.setHidden(false);
770
731
  ],
771
732
  outputs: [{ name: "material", type: "material" }],
772
733
  });
773
- const hiddenPorts = [];
774
- for (let i = 0; i < material.inputs(); i++) {
775
- if (["Alpha", "Clearcoat", "Sheen", "Specular", "Unlit"].includes(material.inputPort(i).getDisplayName())) {
776
- hiddenPorts.push(material.inputPort(i));
777
- }
778
- }
779
- material.insertWidget(0, new MorePortsWidget(hiddenPorts));
780
734
  graph.connectNodes(sheen, 0, material, 5);
781
735
 
736
+ note(at(4, 0), `
737
+ # Widgets on ports
738
+
739
+ An input can hold its own value in a **widget** on its row. The widget stands in for a wire, so it steps aside while something is connected and comes back when the wire is removed.
740
+
741
+ Try wiring **Size** into **Radius**.
742
+
743
+ \`\`\`
744
+ inputs: [
745
+ { name: "Radius", type: "float32",
746
+ widget: { type: "number", config:
747
+ { property: "radius", value: 1 } } },
748
+ ]
749
+ \`\`\`
750
+
751
+ The value lives in the node's **property**, so edit listeners and \`setProperty\` work as they do for any widget.
752
+ `);
753
+ const size = add({
754
+ title: "Size", position: at(4, 0, 0, 600),
755
+ outputs: [{ name: "value", type: "float32" }],
756
+ widgets: [{ type: "number", config: { value: 2.5, label: "Value" } }],
757
+ });
758
+ const circle = add({
759
+ title: "Circle", subTitle: "Widgets on ports", position: at(4, 0, 330, 540),
760
+ inputs: [
761
+ { name: "Radius", type: "float32", widget: { type: "number", config: { property: "radius", value: 1, width: 90 } } },
762
+ { name: "Segments", type: "int", widget: { type: "number", config: { property: "segments", value: 32, integer: true, min: 3, width: 90 } } },
763
+ {
764
+ name: "Center", type: "vector2", widget: {
765
+ type: "row", config: {
766
+ widgets: ["X", "Y"].map((axis) => ({ type: "number", config: { property: axis.toLowerCase(), label: axis, value: 0, width: 64 } })),
767
+ },
768
+ },
769
+ },
770
+ { name: "Fill", type: "color", widget: { type: "color", config: { property: "fill", value: "#3fa7d6", width: 90 } } },
771
+ { name: "Filled", type: "bool", widget: { type: "toggle", config: { property: "filled", value: true, label: "", width: 60 } } },
772
+ { name: "Name", type: "string", widget: { type: "string", config: { property: "name", value: "circle", width: 120 } } },
773
+ ],
774
+ outputs: [{ name: "shape", type: "shape" }],
775
+ });
776
+ logEdits(circle, "radius", "segments", "x", "y", "fill", "filled", "name");
777
+ circle.inputPort(0).addConnectionAddedListener(() => log("Circle", "Radius wired, its widget steps aside"));
778
+ circle.inputPort(0).addConnectionRemovedListener(() => log("Circle", "Radius unwired, its widget is back"));
779
+
782
780
  note(at(0, 1), `
783
781
  # Widgets
784
782
 
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@elicdavis/node-flow",
3
3
  "description": "build node graphs",
4
4
  "author": "Eli C Davis",
5
- "version": "0.2.2",
5
+ "version": "0.2.4",
6
6
  "license": "MIT",
7
7
  "keywords": [
8
8
  "nodes",
@@ -261,11 +261,11 @@ export class ContextMenu {
261
261
 
262
262
  #bounds: Box | null = null;
263
263
 
264
- #contains(point: Vector2): boolean {
264
+ public contains(point: Vector2): boolean {
265
265
  if (this.#bounds !== null && InBox(this.#bounds, point)) {
266
266
  return true;
267
267
  }
268
- return this.#openSubMenu !== undefined && this.#openSubMenu.#contains(point);
268
+ return this.#openSubMenu !== undefined && this.#openSubMenu.contains(point);
269
269
  }
270
270
 
271
271
  /**
@@ -315,7 +315,7 @@ export class ContextMenu {
315
315
  Size: { x: scaledEntryWidth, y: totalScaledHeight },
316
316
  };
317
317
 
318
- const pointer = mousePosition !== undefined && this.#openSubMenu !== undefined && this.#openSubMenu.#contains(mousePosition)
318
+ const pointer = mousePosition !== undefined && this.#openSubMenu !== undefined && this.#openSubMenu.contains(mousePosition)
319
319
  ? undefined
320
320
  : mousePosition;
321
321
 
package/src/graph.ts CHANGED
@@ -10,7 +10,7 @@ import { Clamp01 } from "./utils/math";
10
10
  import { GraphSubsystem, RenderResults } from './graphSubsystem';
11
11
  import { FlowNote } from "./notes/note";
12
12
  import { NoteAddedCallback, NoteDragStartCallback, NoteDragStopCallback, NoteRemovedCallback, NoteSubsystem, NoteSubsystemConfig } from "./notes/subsystem";
13
- import { ConnectionRendererConfiguration, DanglingConnection, InternalConnection, NodeAddedCallback, NodeRemovedCallback, NodeSubsystem } from "./nodes/subsystem";
13
+ import { ConnectionRendererConfiguration, DanglingConnection, InternalConnection, NodeAddedCallback, NodeHoverCallback, NodeRemovedCallback, NodeSubsystem } from "./nodes/subsystem";
14
14
  import { Connection } from './connection';
15
15
  import { Publisher } from './nodes/publisher';
16
16
  import { VectorPool } from './types/pool';
@@ -21,6 +21,7 @@ import { QuickMenu } from './quickMenu';
21
21
  import { onRenderRequested, renderContinuously, requestRender } from './render_scheduler';
22
22
  import { placeEditsDuring } from './widgets/inplace';
23
23
  import { Minimap, MinimapConfig } from './minimap';
24
+ import { Cast } from './port';
24
25
 
25
26
  export type GraphRenderer = (canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, position: Vector2, scale: number) => void;
26
27
 
@@ -65,6 +66,7 @@ export interface FlowNodeGraphConfiguration {
65
66
  idleConnection?: ConnectionRendererConfiguration
66
67
  contextMenu?: ContextMenuConfig
67
68
  nodes?: NodeFactoryConfig
69
+ casts?: Array<Cast>
68
70
  board?: NoteSubsystemConfig
69
71
  minimap?: MinimapConfig
70
72
  }
@@ -164,6 +166,8 @@ export class NodeFlowGraph {
164
166
 
165
167
  #mousePosition: Vector2 | undefined;
166
168
 
169
+ #mouseOverCanvas = false;
170
+
167
171
  #camera: Camera;
168
172
 
169
173
  #openedContextMenu: OpenContextMenu | null;
@@ -185,7 +189,8 @@ export class NodeFlowGraph {
185
189
 
186
190
  this.#mainNodeSubsystem = new NodeSubsystem(postProcessPass, {
187
191
  nodes: config?.nodes,
188
- idleConnection: config?.idleConnection
192
+ idleConnection: config?.idleConnection,
193
+ casts: config?.casts,
189
194
  });
190
195
 
191
196
  this.#lastFrameCursor = CursorStyle.Default;
@@ -245,6 +250,7 @@ export class NodeFlowGraph {
245
250
  this.#mouseDragEvent.bind(this),
246
251
  (mousePosition) => {
247
252
  this.#mousePosition = mousePosition;
253
+ this.#mouseOverCanvas = true;
248
254
  },
249
255
  this.#clickStart.bind(this),
250
256
  this.#clickEnd.bind(this),
@@ -252,6 +258,9 @@ export class NodeFlowGraph {
252
258
  this.#fileDrop.bind(this),
253
259
  (factor, anchor) => {
254
260
  this.#zoomBy(factor, anchor);
261
+ },
262
+ () => {
263
+ this.#mouseOverCanvas = false;
255
264
  }
256
265
  );
257
266
 
@@ -455,6 +464,7 @@ export class NodeFlowGraph {
455
464
  this.#contextMenuEntryHovering = null;
456
465
 
457
466
  this.#mousePosition = mousePosition;
467
+ this.#mouseOverCanvas = true;
458
468
 
459
469
  if (this.#minimap.contains(mousePosition)) {
460
470
  this.#lookAtFromMinimap(mousePosition);
@@ -494,6 +504,10 @@ export class NodeFlowGraph {
494
504
  this.#mainNodeSubsystem.addPublisher(identifier, publisher);
495
505
  }
496
506
 
507
+ setCasts(casts: Array<Cast>): void {
508
+ this.#mainNodeSubsystem.setCasts(casts);
509
+ }
510
+
497
511
  getNodes(): Array<FlowNode> {
498
512
  return this.#mainNodeSubsystem.getNodes();
499
513
  }
@@ -643,6 +657,10 @@ export class NodeFlowGraph {
643
657
  this.#mainNodeSubsystem.addOnNodeRemovedListener(callback);
644
658
  }
645
659
 
660
+ public addNodeHoverListener(callback: NodeHoverCallback): void {
661
+ this.#mainNodeSubsystem.addNodeHoverListener(callback);
662
+ }
663
+
646
664
  #sceenPositionToGraphPosition(screenPosition: Vector2): Vector2 {
647
665
  const out = Zero();
648
666
  this.#camera.screenSpaceToGraphSpace(screenPosition, out);
@@ -674,6 +692,7 @@ export class NodeFlowGraph {
674
692
  {
675
693
  needs: dangling.needs,
676
694
  types: dangling.types,
695
+ casts: this.#mainNodeSubsystem.casts(),
677
696
  onCreated: dangling.connectTo,
678
697
  },
679
698
  );
@@ -767,7 +786,7 @@ export class NodeFlowGraph {
767
786
 
768
787
  TimeExecution("Render_View_" + this.#currentView, () => {
769
788
  placeEditsDuring(this.#canvas, () => {
770
- let results = this.currentView().render(this.#ctx, this.#camera, this.#mousePosition);
789
+ let results = this.currentView().render(this.#ctx, this.#camera, this.#pointerOnGraph());
771
790
  if (results?.cursorStyle) {
772
791
  this.#cursor = results?.cursorStyle;
773
792
  }
@@ -790,6 +809,21 @@ export class NodeFlowGraph {
790
809
  this.#lastFrameCursor = this.#cursor;
791
810
  }
792
811
 
812
+ #pointer(): Vector2 | undefined {
813
+ return this.#mouseOverCanvas ? this.#mousePosition : undefined;
814
+ }
815
+
816
+ #pointerOnGraph(): Vector2 | undefined {
817
+ const pointer = this.#pointer();
818
+ if (pointer === undefined) {
819
+ return undefined;
820
+ }
821
+ if (this.#openedContextMenu?.Menu.contains(pointer) || this.#openQuickMenu?.Menu.contains(pointer)) {
822
+ return undefined;
823
+ }
824
+ return pointer;
825
+ }
826
+
793
827
  #renderBackground(): void {
794
828
  this.#backgroundRenderer(this.#canvas, this.#ctx, this.#camera.position, this.#camera.zoom);
795
829
  }
@@ -799,7 +833,7 @@ export class NodeFlowGraph {
799
833
  if (this.#openedContextMenu !== null) {
800
834
  const pos = VectorPool.get();
801
835
  this.#camera.graphSpaceToScreenSpace(this.#openedContextMenu.Position, pos)
802
- this.#contextMenuEntryHovering = this.#openedContextMenu.Menu.render(this.#ctx, pos, this.#camera.zoom, this.#mousePosition, true);
836
+ this.#contextMenuEntryHovering = this.#openedContextMenu.Menu.render(this.#ctx, pos, this.#camera.zoom, this.#pointer(), true);
803
837
 
804
838
  if (this.#contextMenuEntryHovering !== null) {
805
839
  this.#cursor = CursorStyle.Pointer;
@@ -813,7 +847,7 @@ export class NodeFlowGraph {
813
847
  if (this.#openQuickMenu !== null) {
814
848
  const pos = VectorPool.get();
815
849
  this.#camera.graphSpaceToScreenSpace(this.#openQuickMenu.Position, pos)
816
- this.#contextMenuEntryHovering = this.#openQuickMenu.Menu.render(this.#ctx, pos, this.#camera.zoom, this.#mousePosition);
850
+ this.#contextMenuEntryHovering = this.#openQuickMenu.Menu.render(this.#ctx, pos, this.#camera.zoom, this.#pointer());
817
851
 
818
852
  if (this.#contextMenuEntryHovering !== null) {
819
853
  this.#cursor = CursorStyle.Pointer;
package/src/index.ts CHANGED
@@ -24,8 +24,8 @@ export {
24
24
  };
25
25
 
26
26
  // Ports
27
- import { Port, PortConfig, PortStyle, PortType, portsCompatible } from './port';
28
- export { Port, PortConfig, PortStyle, PortType, portsCompatible };
27
+ import { Cast, Port, PortConfig, PortStyle, PortType, portsCompatible, portsConnect } from './port';
28
+ export { Cast, Port, PortConfig, PortStyle, PortType, portsCompatible, portsConnect };
29
29
 
30
30
  // Rendering
31
31
  import { renderContinuously, requestRender } from './render_scheduler';
@@ -34,14 +34,14 @@ export { renderContinuously, requestRender };
34
34
  import { Minimap, MinimapConfig } from './minimap';
35
35
  export { Minimap, MinimapConfig };
36
36
 
37
- import { DanglingConnection, InternalConnection } from './nodes/subsystem';
38
- export { DanglingConnection, InternalConnection };
37
+ import { DanglingConnection, InternalConnection, NodeHoverCallback } from './nodes/subsystem';
38
+ export { DanglingConnection, InternalConnection, NodeHoverCallback };
39
39
 
40
40
  import { NodeMenuFilter } from './nodes/publisher';
41
41
  export { NodeMenuFilter };
42
42
 
43
- import { portConfigTypes, typeSetsCompatible } from './port';
44
- export { portConfigTypes, typeSetsCompatible };
43
+ import { portConfigTypes, portTypeColor, typeSetsCompatible, typeSetsConnect } from './port';
44
+ export { portConfigTypes, portTypeColor, typeSetsCompatible, typeSetsConnect };
45
45
 
46
46
  // Connections
47
47
  import {
package/src/input.ts CHANGED
@@ -19,6 +19,8 @@ export class MouseObserver {
19
19
  #clickStop: () => void;
20
20
  #contextMenu: (position: Vector2) => void;
21
21
  #zoomCallback: (factor: number, anchor: Vector2) => void;
22
+ #leaveCallback: (() => void) | undefined;
23
+ #inside = false;
22
24
 
23
25
  #longPressTimer: ReturnType<typeof setTimeout> | null = null;
24
26
  #longPressOrigin: Vector2 = Zero();
@@ -35,6 +37,7 @@ export class MouseObserver {
35
37
  contextMenu: (position: Vector2) => void,
36
38
  fileDrop: (file: File) => void,
37
39
  zoomCallback: (factor: number, anchor: Vector2) => void,
40
+ leaveCallback?: () => void,
38
41
  ) {
39
42
  this.#ele = ele;
40
43
  this.#dragCallback = dragCallback;
@@ -43,6 +46,7 @@ export class MouseObserver {
43
46
  this.#clickStop = clickStop;
44
47
  this.#contextMenu = contextMenu;
45
48
  this.#zoomCallback = zoomCallback;
49
+ this.#leaveCallback = leaveCallback;
46
50
 
47
51
  this.#clicked = false;
48
52
  this.#lastTouch = Zero();
@@ -64,6 +68,12 @@ export class MouseObserver {
64
68
  document.addEventListener("touchcancel", redraws(this.#touchEnd.bind(this)), false);
65
69
 
66
70
  ele.addEventListener("mousemove", redraws(this.#move.bind(this)), false);
71
+ ele.addEventListener("mouseleave", redraws(() => {
72
+ this.#inside = false;
73
+ if (!this.#clicked) {
74
+ this.#leaveCallback?.();
75
+ }
76
+ }), false);
67
77
  ele.addEventListener("touchmove", redraws(this.#touchMove.bind(this)), { passive: false });
68
78
 
69
79
  ele.addEventListener("wheel", redraws(this.#wheel.bind(this)), { passive: false });
@@ -144,6 +154,7 @@ export class MouseObserver {
144
154
  if (this.#fromTouch()) {
145
155
  return;
146
156
  }
157
+ this.#inside = true;
147
158
  const pos = this.#mousePos(event);
148
159
  if (this.#clicked) {
149
160
  const delta = Zero();
@@ -158,6 +169,7 @@ export class MouseObserver {
158
169
  if (event.button !== 0 || this.#fromTouch()) {
159
170
  return;
160
171
  }
172
+ this.#inside = true;
161
173
  this.#clicked = true;
162
174
  this.#clickStart(this.#mousePos(event), event.ctrlKey || event.shiftKey);
163
175
  }
@@ -166,8 +178,12 @@ export class MouseObserver {
166
178
  if (event.button !== 0 || this.#fromTouch()) {
167
179
  return;
168
180
  }
181
+ const wasClicked = this.#clicked;
169
182
  this.#clicked = false;
170
183
  this.#clickStop();
184
+ if (wasClicked && !this.#inside) {
185
+ this.#leaveCallback?.();
186
+ }
171
187
  }
172
188
 
173
189
  #touchStart(event: TouchEvent): void {
@@ -150,7 +150,7 @@ export class MarkdownSyntaxParser {
150
150
  this.#inc();
151
151
  }
152
152
 
153
- return [new Text(content, { font: "monospace" })];
153
+ return [new Text(content, { font: Theme.Note.CodeFontFamily ?? "monospace" })];
154
154
  }
155
155
 
156
156
  #text(): Array<Text> {
@@ -315,7 +315,7 @@ export class MarkdownSyntaxParser {
315
315
  this.#inc(); // puts us on the next token
316
316
 
317
317
  const codeBlockText = this.#originalText.substring(start as number, end).replace(/\r?\n$/, "");
318
- const entryText = new Text(codeBlockText);
318
+ const entryText = new Text(codeBlockText, { font: Theme.Note.CodeFontFamily ?? undefined });
319
319
  this.#assignStandardStyling([entryText])
320
320
  entryText.setColor(Theme.Note.CodeBlock.FontColor);
321
321
  return new CodeBlockEntry(entryText)
package/src/node.ts CHANGED
@@ -237,12 +237,16 @@ export class FlowNode {
237
237
 
238
238
  #selected: boolean;
239
239
 
240
+ #highlighted = false;
241
+
240
242
  #inputPortPositions: List<Box>;
241
243
 
242
244
  #outputPortPositions: List<Box>;
243
245
 
244
246
  #widgetPositions: List<Box>;
245
247
 
248
+ #portWidgetPositions: List<Box> = new List<Box>();
249
+
246
250
  #data: Metadata;
247
251
 
248
252
  #registeredAnyPropertyChangeCallbacks: Array<AnyPropertyChangeCallback>;
@@ -366,6 +370,18 @@ export class FlowNode {
366
370
  }
367
371
  }
368
372
 
373
+ public highlighted(): boolean {
374
+ return this.#highlighted;
375
+ }
376
+
377
+ public setHighlighted(highlighted: boolean): void {
378
+ if (this.#highlighted === highlighted) {
379
+ return;
380
+ }
381
+ this.#highlighted = highlighted;
382
+ requestRender();
383
+ }
384
+
369
385
  public raiseDragStoppedEvent() {
370
386
  for (let i = 0; i < this.#onDragStop.length; i++) {
371
387
  this.#onDragStop[i](this);
@@ -802,7 +818,7 @@ export class FlowNode {
802
818
  }
803
819
  this.#portTextStyle.measure(ctx, 1, port.getDisplayName(), tempMeasurement);
804
820
  size.y += this.#portRowHeight(port, tempMeasurement.y) + this.#elementSpacing;
805
- size.x = Math.max(size.x, tempMeasurement.x + doublePadding)
821
+ size.x = Math.max(size.x, this.#portRowWidth(port, tempMeasurement.x) + doublePadding)
806
822
  }
807
823
 
808
824
  const widgets = this.#hasHiddenPortsToggle() ? [this.#hiddenPortsToggle, ...this.#widgets] : this.#widgets;
@@ -829,6 +845,9 @@ export class FlowNode {
829
845
 
830
846
  addInput(config: PortConfig): Port {
831
847
  const port = new Port(this, config.array ? PortType.InputArray : PortType.Input, config);
848
+ if (config.widget !== undefined) {
849
+ port.setWidget(GlobalWidgetFactory.createFromConfig(this, config.widget));
850
+ }
832
851
  this.#input.push(port);
833
852
  return port;
834
853
  }
@@ -980,6 +999,18 @@ export class FlowNode {
980
999
  }
981
1000
  }
982
1001
 
1002
+ for (let i = 0; i < this.#portWidgetPositions.Count(); i++) {
1003
+ const port = this.#input[i];
1004
+ if (port !== undefined && port.widgetShown() && InBox(this.#portWidgetPositions.At(i), position)) {
1005
+ intersection.Node = this;
1006
+ intersection.Widget = port.widget() as Widget;
1007
+ intersection.WidgetBox = this.#portWidgetPositions.At(i);
1008
+ intersection.Port = undefined;
1009
+ intersection.PortIndex = undefined;
1010
+ intersection.PortIsInput = undefined;
1011
+ }
1012
+ }
1013
+
983
1014
  return intersection;
984
1015
  }
985
1016
 
@@ -1125,7 +1156,15 @@ export class FlowNode {
1125
1156
 
1126
1157
  #portRowHeight(port: Port, textHeight: number): number {
1127
1158
  const slots = port.slots();
1128
- return slots > 1 ? Math.max(textHeight, slots * ARRAY_SLOT_PITCH) : textHeight;
1159
+ const widget = port.widgetShown() ? (port.widget() as Widget).Size().y : 0;
1160
+ return Math.max(slots > 1 ? slots * ARRAY_SLOT_PITCH : 0, textHeight, widget);
1161
+ }
1162
+
1163
+ #portRowWidth(port: Port, textWidth: number): number {
1164
+ if (!port.widgetShown()) {
1165
+ return textWidth;
1166
+ }
1167
+ return textWidth + this.#elementSpacing + (port.widget() as Widget).Size().x;
1129
1168
  }
1130
1169
 
1131
1170
  public hasBeenLaidOut(): boolean {
@@ -1153,6 +1192,7 @@ export class FlowNode {
1153
1192
  this.#inputPortPositions.Clear();
1154
1193
  this.#outputPortPositions.Clear();
1155
1194
  this.#widgetPositions.Clear();
1195
+ this.#portWidgetPositions.Clear();
1156
1196
  const scaledPadding = this.#padding * camera.zoom;
1157
1197
  const scaledElementSpacing = this.#elementSpacing * camera.zoom;
1158
1198
 
@@ -1162,6 +1202,21 @@ export class FlowNode {
1162
1202
  const nodeStyle = this.#calculateStyle(state);
1163
1203
  nodeStyle.Draw(ctx, nodeBounds, camera.zoom);
1164
1204
 
1205
+ if (this.#highlighted) {
1206
+ const gap = 3;
1207
+ ctx.save();
1208
+ ctx.strokeStyle = Theme.Node.Border.Highlighted;
1209
+ ctx.lineWidth = 2;
1210
+ ctx.beginPath();
1211
+ ctx.roundRect(
1212
+ nodeBounds.Position.x - gap, nodeBounds.Position.y - gap,
1213
+ nodeBounds.Size.x + gap * 2, nodeBounds.Size.y + gap * 2,
1214
+ nodeStyle.radius() * camera.zoom + gap
1215
+ );
1216
+ ctx.stroke();
1217
+ ctx.restore();
1218
+ }
1219
+
1165
1220
  // Title
1166
1221
  const borderSize = nodeStyle.borderSize();
1167
1222
  ctx.textAlign = TextAlign.Center;
@@ -1293,6 +1348,7 @@ export class FlowNode {
1293
1348
  const port = this.#input[i];
1294
1349
  if (port.hidden()) {
1295
1350
  this.#inputPortPositions.Push({ Position: { x: nodeBounds.Position.x, y: startY }, Size: { x: 0, y: 0 } });
1351
+ this.#portWidgetPositions.Push({ Position: { x: nodeBounds.Position.x, y: startY }, Size: { x: 0, y: 0 } });
1296
1352
  continue;
1297
1353
  }
1298
1354
  this.#portTextStyle.measure(ctx, 1, port.getDisplayName(), tempMeasurement);
@@ -1306,6 +1362,21 @@ export class FlowNode {
1306
1362
  this.#portTextStyle.setupStyle(ctx, camera.zoom);
1307
1363
  ctx.fillText(port.getDisplayName(), leftSide, position.y);
1308
1364
 
1365
+ // Widget
1366
+ if (port.widgetShown()) {
1367
+ const widget = port.widget() as Widget;
1368
+ const widgetHeight = widget.Size().y * camera.zoom;
1369
+ const widgetLeft = leftSide + (tempMeasurement.x * camera.zoom) + scaledElementSpacing;
1370
+ const box: Box = {
1371
+ Position: { x: widgetLeft, y: startY + ((portHeight - widgetHeight) / 2) },
1372
+ Size: { x: nodeBounds.Position.x + nodeBounds.Size.x - scaledPadding - widgetLeft, y: widgetHeight },
1373
+ };
1374
+ widget.Draw(ctx, box, camera.zoom, mousePosition);
1375
+ this.#portWidgetPositions.Push(box);
1376
+ } else {
1377
+ this.#portWidgetPositions.Push({ Position: { x: nodeBounds.Position.x, y: startY }, Size: { x: 0, y: 0 } });
1378
+ }
1379
+
1309
1380
  // Port
1310
1381
  this.#inputPortPositions.Push(port.render(ctx, position, camera, mousePosition, postProcess));
1311
1382
 
@@ -1411,6 +1482,7 @@ export class FlowNode {
1411
1482
  return false;
1412
1483
  }
1413
1484
  this.#widgetPositions.Clear();
1485
+ this.#portWidgetPositions.Clear();
1414
1486
  this.#hiddenPortsTogglePosition = null;
1415
1487
  return true;
1416
1488
  }
@@ -1,6 +1,6 @@
1
1
  import { ContextMenuConfig, ContextMenuItemConfig } from "../contextMenu";
2
2
  import { FlowNode, FlowNodeConfig } from '../node';
3
- import { PortConfig, PortType, portConfigTypes, typeSetsCompatible } from "../port";
3
+ import { Cast, PortConfig, PortType, portConfigTypes, typeSetsConnect } from "../port";
4
4
  import { Vector2 } from "../types/vector2";
5
5
  import { NodeSubsystem } from "./subsystem";
6
6
 
@@ -15,18 +15,16 @@ export interface NodeMenuFilter {
15
15
  /** Types that side has to accept. An empty list takes anything. */
16
16
  types: Array<string>;
17
17
 
18
+ casts?: Array<Cast>;
19
+
18
20
  onCreated?: (node: FlowNode) => void;
19
21
  }
20
22
 
21
23
  function offersMatchingPort(config: FlowNodeConfig, filter: NodeMenuFilter): boolean {
22
- const ports: Array<PortConfig> | undefined =
23
- filter.needs === PortType.Input ? config.inputs : config.outputs;
24
-
25
- if (ports === undefined) {
26
- return false;
24
+ if (filter.needs === PortType.Input) {
25
+ return (config.inputs ?? []).some((port) => typeSetsConnect(filter.types, portConfigTypes(port), filter.casts));
27
26
  }
28
-
29
- return ports.some((port) => typeSetsCompatible(portConfigTypes(port), filter.types));
27
+ return (config.outputs ?? []).some((port) => typeSetsConnect(portConfigTypes(port), filter.types, filter.casts));
30
28
  }
31
29
 
32
30
  interface PublisherNodes {