@elicdavis/node-flow 0.2.3 → 0.2.5

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
  * {
@@ -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
  },
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.3",
5
+ "version": "0.2.5",
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,6 +237,8 @@ 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>;
@@ -368,6 +370,18 @@ export class FlowNode {
368
370
  }
369
371
  }
370
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
+
371
385
  public raiseDragStoppedEvent() {
372
386
  for (let i = 0; i < this.#onDragStop.length; i++) {
373
387
  this.#onDragStop[i](this);
@@ -1188,6 +1202,21 @@ export class FlowNode {
1188
1202
  const nodeStyle = this.#calculateStyle(state);
1189
1203
  nodeStyle.Draw(ctx, nodeBounds, camera.zoom);
1190
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
+
1191
1220
  // Title
1192
1221
  const borderSize = nodeStyle.borderSize();
1193
1222
  ctx.textAlign = TextAlign.Center;
@@ -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 {
@@ -6,7 +6,7 @@ import { FlowNode, NodeState } from "../node";
6
6
  import { Organize } from "../organize";
7
7
  import { PassSubsystem } from "../pass/subsystem";
8
8
  import { TimeExecution } from "../performance";
9
- import { arrayInsertIndex, Port, PortType, portsCompatible } from "../port";
9
+ import { arrayInsertIndex, Cast, Port, PortType, portsConnect } from "../port";
10
10
  import { requestRender } from "../render_scheduler";
11
11
  import { BoxStyle } from "../styles/box";
12
12
  import { CursorStyle } from "../styles/cursor";
@@ -21,6 +21,7 @@ import { Publisher } from "./publisher";
21
21
 
22
22
  export type NodeAddedCallback = (node: FlowNode) => void;
23
23
  export type NodeRemovedCallback = (node: FlowNode) => void;
24
+ export type NodeHoverCallback = (node: FlowNode | null) => void;
24
25
 
25
26
  export const nodeFlowGroup = "node-flow-graph-node-menu";
26
27
 
@@ -84,6 +85,7 @@ interface PortIntersection {
84
85
  interface NodeSubsystemConfig {
85
86
  idleConnection?: ConnectionRendererConfiguration
86
87
  nodes?: NodeFactoryConfig
88
+ casts?: Array<Cast>
87
89
  }
88
90
 
89
91
  export class NodeSubsystem {
@@ -126,6 +128,7 @@ export class NodeSubsystem {
126
128
 
127
129
  #boxSelectionNodes: List<number>;
128
130
 
131
+ #casts: Array<Cast>;
129
132
 
130
133
  #lastCtx: CanvasRenderingContext2D | null;
131
134
 
@@ -137,6 +140,10 @@ export class NodeSubsystem {
137
140
 
138
141
  #registeredNodeAddedCallbacks: Array<NodeAddedCallback>;
139
142
 
143
+ #nodeHoverCallbacks = new Array<NodeHoverCallback>();
144
+
145
+ #hoveredNode: FlowNode | null = null;
146
+
140
147
  // ========================================================================
141
148
 
142
149
  constructor(postProcessPass: PassSubsystem, config?: NodeSubsystemConfig) {
@@ -160,6 +167,15 @@ export class NodeSubsystem {
160
167
  this.#boxSelectEnd_graphSpace = Zero();
161
168
  this.#boxSelectionNodes = new List<number>();
162
169
  this.#lastCtx = null;
170
+ this.#casts = config?.casts ?? [];
171
+ }
172
+
173
+ public setCasts(casts: Array<Cast>): void {
174
+ this.#casts = casts;
175
+ }
176
+
177
+ public casts(): Array<Cast> {
178
+ return this.#casts;
163
179
  }
164
180
 
165
181
  public addPublisher(identifier: string, publisher: Publisher): void {
@@ -188,6 +204,20 @@ export class NodeSubsystem {
188
204
  this.#nodeRemovedCallbacks.push(callback);
189
205
  }
190
206
 
207
+ public addNodeHoverListener(callback: NodeHoverCallback): void {
208
+ this.#nodeHoverCallbacks.push(callback);
209
+ }
210
+
211
+ #setHoveredNode(node: FlowNode | null): void {
212
+ if (this.#hoveredNode === node) {
213
+ return;
214
+ }
215
+ this.#hoveredNode = node;
216
+ for (let i = 0; i < this.#nodeHoverCallbacks.length; i++) {
217
+ this.#nodeHoverCallbacks[i](node);
218
+ }
219
+ }
220
+
191
221
  clickStart(mousePosition: Vector2, camera: Camera, ctrlKey: boolean): boolean {
192
222
  this.#boxSelect = false;
193
223
  this.#nodesGrabbed.Clear();
@@ -445,7 +475,10 @@ export class NodeSubsystem {
445
475
  let firstHidden = -1;
446
476
  for (let i = 0; i < count; i++) {
447
477
  const port = side === PortType.Input ? node.inputPort(i) : node.outputPort(i);
448
- if (!portsCompatible(port, against)) {
478
+ const compatible = side === PortType.Input
479
+ ? portsConnect(against, port, this.#casts)
480
+ : portsConnect(port, against, this.#casts);
481
+ if (!compatible) {
449
482
  continue;
450
483
  }
451
484
  if (!port.hidden()) {
@@ -521,13 +554,13 @@ export class NodeSubsystem {
521
554
  }
522
555
 
523
556
  // Different types, can't connect
524
- if (this.#portHovering.InputPort && !portsCompatible(this.#portHovering.Port, conn.outPort())) {
557
+ if (this.#portHovering.InputPort && !portsConnect(conn.outPort(), this.#portHovering.Port, this.#casts)) {
525
558
  this.#clearCurrentlySelectedConnection();
526
559
  return;
527
560
  }
528
561
 
529
562
  // Different types, can't connect
530
- if (!this.#portHovering.InputPort && !portsCompatible(this.#portHovering.Port, conn.inPort())) {
563
+ if (!this.#portHovering.InputPort && !portsConnect(this.#portHovering.Port, conn.inPort(), this.#casts)) {
531
564
  this.#clearCurrentlySelectedConnection();
532
565
  return;
533
566
  }
@@ -559,7 +592,7 @@ export class NodeSubsystem {
559
592
  connectNodes(nodeOut: FlowNode, outPort: number, nodeIn: FlowNode, inPort: number): Connection | undefined {
560
593
  const from = nodeOut.outputPort(outPort);
561
594
  const to = nodeIn.inputPort(inPort);
562
- if (!portsCompatible(from, to)) {
595
+ if (!portsConnect(from, to, this.#casts)) {
563
596
  console.error("can't connect nodes of different types", from.getDataType(), to.getDataType());
564
597
  return;
565
598
  }
@@ -955,6 +988,7 @@ export class NodeSubsystem {
955
988
  this.#widgetHovering = null;
956
989
  this.#nodeHovering = -1;
957
990
  this.#boxSelectionNodes.Clear();
991
+ let hovered: FlowNode | null = null;
958
992
 
959
993
  const selectedBox_Screenspace = this.#boxSelectionScreenspaceBox(camera);
960
994
 
@@ -976,6 +1010,9 @@ export class NodeSubsystem {
976
1010
 
977
1011
  if (mousePosition !== undefined && !this.#boxSelect) {
978
1012
  const intersection = this.#nodes[i].inBounds(ctx, camera, mousePosition);
1013
+ if (intersection.Node !== undefined) {
1014
+ hovered = intersection.Node;
1015
+ }
979
1016
 
980
1017
  if (intersection.Node !== undefined && intersection.PortIndex === undefined && intersection.Widget === undefined) {
981
1018
  state = NodeState.MouseOver;
@@ -1019,6 +1056,7 @@ export class NodeSubsystem {
1019
1056
 
1020
1057
  this.#nodes[i].render(ctx, camera, state, mousePosition, this.#postProcessPass);
1021
1058
  }
1059
+ this.#setHoveredNode(hovered);
1022
1060
 
1023
1061
  if (this.#widgetCurrentlyClicking !== null) {
1024
1062
  this.#cursor = this.#widgetCurrentlyClicking.Cursor?.() ?? CursorStyle.Pointer;
package/src/port.ts CHANGED
@@ -91,6 +91,33 @@ export function typeSetsCompatible(a: Array<string>, b: Array<string>): boolean
91
91
  return a.some((type) => b.includes(type));
92
92
  }
93
93
 
94
+ /**
95
+ * Lets an output of type `from` feed an input of type `to`
96
+ */
97
+ export interface Cast {
98
+ from: string;
99
+ to: string;
100
+ }
101
+
102
+ /**
103
+ * Whether an output may feed an input: their types overlap, or a cast takes
104
+ * one of the output's types to one of the input's.
105
+ */
106
+ export function portsConnect(output: Port | undefined | null, input: Port | undefined | null, casts?: Array<Cast>): boolean {
107
+ if (!output || !input) {
108
+ return false;
109
+ }
110
+
111
+ return typeSetsConnect(output.acceptedTypes(), input.acceptedTypes(), casts);
112
+ }
113
+
114
+ export function typeSetsConnect(output: Array<string>, input: Array<string>, casts?: Array<Cast>): boolean {
115
+ if (typeSetsCompatible(output, input)) {
116
+ return true;
117
+ }
118
+ return (casts ?? []).some((cast) => output.includes(cast.from) && input.includes(cast.to));
119
+ }
120
+
94
121
  export function portConfigTypes(config: PortConfig): Array<string> {
95
122
  if (config.anyType === true) {
96
123
  return [];
@@ -139,6 +166,10 @@ function fallbackColor(type: string, s: number): string {
139
166
  return RgbToHex(color);
140
167
  }
141
168
 
169
+ export function portTypeColor(type: string): string {
170
+ return fallbackColor(type, 1);
171
+ }
172
+
142
173
  export class Port {
143
174
 
144
175
  #node: FlowNode;
package/src/quickMenu.ts CHANGED
@@ -6,6 +6,7 @@ import { FontWeight } from "./styles/text";
6
6
  import { Theme } from "./theme";
7
7
  import { Text } from './types/text';
8
8
  import { ScaleVector, Vector2 } from './types/vector2';
9
+ import { Box, InBox } from './types/box';
9
10
  import { Display } from './elements/base';
10
11
 
11
12
  class QuickMenuGroup {
@@ -325,6 +326,16 @@ export class QuickMenu {
325
326
  }
326
327
 
327
328
  this.#container.render(ctx, pos, 1, this.renderSize);
329
+ this.#bounds = {
330
+ Position: { x: pos.x, y: pos.y },
331
+ Size: { x: this.renderSize.x, y: this.renderSize.y },
332
+ };
328
333
  return null;
329
334
  }
335
+
336
+ #bounds: Box | null = null;
337
+
338
+ public contains(point: Vector2): boolean {
339
+ return this.#bounds !== null && InBox(this.#bounds, point);
340
+ }
330
341
  }
package/src/theme.ts CHANGED
@@ -24,7 +24,8 @@ const defaults = {
24
24
  Idle: "#1c1c1c",
25
25
  MouseOver: "#8e8e8e",
26
26
  Grabbed: "white",
27
- Selected: "#6e6e6e"
27
+ Selected: "#6e6e6e",
28
+ Highlighted: "#ffc95c"
28
29
  },
29
30
  Bevel: null as BevelColors | null,
30
31
  Port: {
@@ -111,6 +112,7 @@ const defaults = {
111
112
  H3: {
112
113
  FontSize: 16,
113
114
  },
115
+ CodeFontFamily: null as string | null,
114
116
  CodeBlock: {
115
117
  BackgroundColor: "#0c2b35",
116
118
  FontColor: "#afb9bb",
@@ -3,6 +3,7 @@ import { BuildMarkdown } from '../src/markdown/markdown';
3
3
  import { BasicMarkdownEntry, CodeBlockEntry, UnorderedListMarkdownEntry } from '../src/markdown/entry';
4
4
  import { FontStyle, FontWeight } from '../src/styles/text';
5
5
  import { Text } from '../src/types/text';
6
+ import { setTheme } from '../src/theme';
6
7
 
7
8
  /** The runs of text in an entry, as [content, weight, style] triples. */
8
9
  function runs(entry: any): Array<[string, FontWeight, FontStyle]> {
@@ -141,6 +142,16 @@ describe('markdown: code', () => {
141
142
  assert.notStrictEqual(entry.lines()[0].style().getFont(), 'monospace');
142
143
  });
143
144
 
145
+ it('sets inline code in the theme code font', () => {
146
+ setTheme({ Note: { CodeFontFamily: 'VT323' } });
147
+ try {
148
+ const entry = BuildMarkdown('a `b` c')[0] as any;
149
+ assert.strictEqual(entry.lines()[1].style().getFont(), 'VT323');
150
+ } finally {
151
+ setTheme();
152
+ }
153
+ });
154
+
144
155
  it('keeps a lone backtick as text', () => {
145
156
  assert.strictEqual(contentOf(BuildMarkdown('a ` b')[0]), 'a ` b');
146
157
  });