@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.
@@ -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
@@ -11,6 +11,8 @@ import { Box, InBox } from "./types/box";
11
11
  import { Text } from "./types/text";
12
12
  import { Vector2 } from "./types/vector2";
13
13
  import { Color, HSV, HSV2RGB, RgbToHex } from './utils/color';
14
+ import type { Widget } from "./widgets/widget";
15
+ import type { WidgetConfig } from "./widgets/factory";
14
16
 
15
17
  export enum PortType {
16
18
  Input = "INPUT",
@@ -56,6 +58,12 @@ export interface PortConfig {
56
58
  */
57
59
  hidden?: boolean;
58
60
 
61
+ /**
62
+ * A widget drawn on the port's own row while nothing is connected to
63
+ * it, for a value the port holds on its own. Input ports only.
64
+ */
65
+ widget?: WidgetConfig;
66
+
59
67
  emptyStyle?: PortStyle;
60
68
  filledStyle?: PortStyle;
61
69
  onConnectionAdded?: ConnectionChangeCallback;
@@ -83,6 +91,33 @@ export function typeSetsCompatible(a: Array<string>, b: Array<string>): boolean
83
91
  return a.some((type) => b.includes(type));
84
92
  }
85
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
+
86
121
  export function portConfigTypes(config: PortConfig): Array<string> {
87
122
  if (config.anyType === true) {
88
123
  return [];
@@ -131,6 +166,10 @@ function fallbackColor(type: string, s: number): string {
131
166
  return RgbToHex(color);
132
167
  }
133
168
 
169
+ export function portTypeColor(type: string): string {
170
+ return fallbackColor(type, 1);
171
+ }
172
+
134
173
  export class Port {
135
174
 
136
175
  #node: FlowNode;
@@ -167,6 +206,8 @@ export class Port {
167
206
 
168
207
  #onConnectionRemoved: Array<ConnectionChangeCallback>;
169
208
 
209
+ #widget: Widget | null = null;
210
+
170
211
  constructor(node: FlowNode, portType: PortType, config?: PortConfig) {
171
212
  this.#node = node;
172
213
  this.#connections = new Array<Connection>();
@@ -339,11 +380,31 @@ export class Port {
339
380
  }
340
381
 
341
382
  #visibilityMayHaveChanged(was: boolean): void {
342
- if (was !== this.hidden() || this.#portType === PortType.InputArray) {
383
+ if (was !== this.hidden() || this.#portType === PortType.InputArray || this.#widget !== null) {
343
384
  this.#node?.invalidateLayout();
344
385
  }
345
386
  }
346
387
 
388
+ setWidget(widget: Widget | null): void {
389
+ this.#widget = widget;
390
+ this.#node?.invalidateLayout();
391
+ }
392
+
393
+ widget(): Widget | null {
394
+ return this.#widget;
395
+ }
396
+
397
+ /**
398
+ * The widget stands in for a wire, so it leaves once one is connected.
399
+ */
400
+ widgetShown(): boolean {
401
+ return this.#widget !== null
402
+ && this.#portType !== PortType.Output
403
+ && this.#connections.length === 0
404
+ && !this.hidden()
405
+ && !(this.#widget.Hidden?.() ?? false);
406
+ }
407
+
347
408
  /**
348
409
  * A port asked to hide still shows while anything is connected to it,
349
410
  * and while its node is showing its hidden ports.
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
  });
@@ -1,7 +1,7 @@
1
1
  import { strict as assert } from 'assert';
2
2
  import { BuildMarkdown } from '../src/markdown/markdown';
3
3
  import { CodeBlockEntry } from '../src/markdown/entry';
4
- import { Theme } from '../src/theme';
4
+ import { Theme, setTheme } from '../src/theme';
5
5
 
6
6
  interface Drawn {
7
7
  box: { y: number; height: number } | null;
@@ -49,6 +49,26 @@ describe('a markdown code block', () => {
49
49
  assert.deepEqual(drawn.lines.map((l) => l.text), ['const a = 1;', 'const b = 2;']);
50
50
  });
51
51
 
52
+ it('draws in the theme code font, apart from the note text', () => {
53
+ setTheme({ FontFamily: 'Tahoma', Note: { CodeFontFamily: 'VT323' } });
54
+ try {
55
+ const entry = BuildMarkdown('```\none\n```')[0] as CodeBlockEntry;
56
+ assert.equal(entry.text().style().getFont(), 'VT323');
57
+ } finally {
58
+ setTheme();
59
+ }
60
+ });
61
+
62
+ it('draws in the theme font when no code font is set', () => {
63
+ setTheme({ FontFamily: 'Tahoma' });
64
+ try {
65
+ const entry = BuildMarkdown('```\none\n```')[0] as CodeBlockEntry;
66
+ assert.equal(entry.text().style().getFont(), 'Tahoma');
67
+ } finally {
68
+ setTheme();
69
+ }
70
+ });
71
+
52
72
  it('reports the height it drew', () => {
53
73
  const entry = BuildMarkdown('```\none\n```')[0] as CodeBlockEntry;
54
74
  const drawn: Drawn = { box: null, lines: [] };
@@ -0,0 +1,70 @@
1
+ import { strict as assert } from 'assert';
2
+ import { MouseObserver } from '../src/input';
3
+
4
+ function observe(run: (fire: (name: string, event?: any) => void, fireDocument: (name: string, event?: any) => void, left: () => number) => void) {
5
+ const listeners = new Map<string, Array<(e: any) => void>>();
6
+ const documentListeners = new Map<string, Array<(e: any) => void>>();
7
+ const ele: any = {
8
+ style: {},
9
+ addEventListener: (name: string, fn: (e: any) => void) => {
10
+ listeners.set(name, [...(listeners.get(name) ?? []), fn]);
11
+ },
12
+ getBoundingClientRect: () => ({ left: 0, top: 0 }),
13
+ };
14
+ const dispatch = (from: Map<string, Array<(e: any) => void>>) => (name: string, event: any = {}) => {
15
+ event.preventDefault = () => { };
16
+ event.button ??= 0;
17
+ event.clientX ??= 10;
18
+ event.clientY ??= 10;
19
+ (from.get(name) ?? []).forEach((fn) => fn(event));
20
+ };
21
+
22
+ let leaves = 0;
23
+ const g = globalThis as any;
24
+ const previous = g.document;
25
+ g.document = {
26
+ addEventListener: (name: string, fn: (e: any) => void) => {
27
+ documentListeners.set(name, [...(documentListeners.get(name) ?? []), fn]);
28
+ },
29
+ };
30
+ try {
31
+ new MouseObserver(ele, () => { }, () => { }, () => { }, () => { }, () => { }, () => { }, () => { },
32
+ () => { leaves++; });
33
+ run(dispatch(listeners), dispatch(documentListeners), () => leaves);
34
+ } finally {
35
+ g.document = previous;
36
+ }
37
+ }
38
+
39
+ describe('the mouse leaving the canvas', () => {
40
+ it('is reported when nothing is held', () => {
41
+ observe((fire, _, left) => {
42
+ fire('mousemove');
43
+ fire('mouseleave');
44
+ assert.equal(left(), 1);
45
+ });
46
+ });
47
+
48
+ it('is reported once a drag that left the canvas is let go', () => {
49
+ observe((fire, fireDocument, left) => {
50
+ fire('mousemove');
51
+ fire('mousedown');
52
+ fire('mouseleave');
53
+ assert.equal(left(), 0, 'still dragging');
54
+
55
+ fireDocument('mouseup');
56
+ assert.equal(left(), 1);
57
+ });
58
+ });
59
+
60
+ it('is not reported when a drag comes back before it is let go', () => {
61
+ observe((fire, fireDocument, left) => {
62
+ fire('mousemove');
63
+ fire('mousedown');
64
+ fire('mouseleave');
65
+ fire('mousemove');
66
+ fireDocument('mouseup');
67
+ assert.equal(left(), 0);
68
+ });
69
+ });
70
+ });
@@ -0,0 +1,69 @@
1
+ import { strict as assert } from 'assert';
2
+ import { NodeSubsystem } from '../src/nodes/subsystem';
3
+ import { PassSubsystem } from '../src/pass/subsystem';
4
+ import { FlowNode } from '../src/node';
5
+ import { Camera } from '../src/camera';
6
+ import { Theme } from '../src/theme';
7
+ import { center, stubCtx } from './support';
8
+
9
+ describe('the node hover listener', () => {
10
+ it('reports the node under the mouse once, then null when the mouse leaves', () => {
11
+ const graph = new NodeSubsystem(new PassSubsystem());
12
+ const node = new FlowNode({ title: 'hovered', position: { x: 100, y: 100 } });
13
+ graph.addNode(node);
14
+ const ctx = stubCtx();
15
+ const camera = new Camera();
16
+ const seen: Array<FlowNode | null> = [];
17
+ graph.addNodeHoverListener((n) => seen.push(n));
18
+
19
+ graph.render(ctx, camera, undefined);
20
+ const over = center(node.calculateBounds(ctx, camera));
21
+ graph.render(ctx, camera, over);
22
+ graph.render(ctx, camera, over);
23
+ graph.render(ctx, camera, undefined);
24
+
25
+ assert.deepEqual(seen, [node, null]);
26
+ });
27
+ });
28
+
29
+ describe('a highlighted node', () => {
30
+ it('keeps a screen-width outline in the highlight colour when zoomed far out', () => {
31
+ const node = new FlowNode({ title: 'lit', position: { x: 100, y: 100 } });
32
+ const graph = new NodeSubsystem(new PassSubsystem());
33
+ graph.addNode(node);
34
+ const strokes: Array<{ style: string; width: number }> = [];
35
+ const ctx = stubCtx();
36
+ (ctx as any).stroke = function () { strokes.push({ style: this.strokeStyle, width: this.lineWidth }); };
37
+ const camera = new Camera();
38
+ camera.zoom = 0.1;
39
+
40
+ node.setHighlighted(true);
41
+ graph.render(ctx, camera, undefined);
42
+
43
+ assert.ok(strokes.some((s) => s.style === Theme.Node.Border.Highlighted && s.width === 2));
44
+ });
45
+
46
+ it('draws its ports with the same line width as when it is not highlighted', () => {
47
+ const widths = (highlighted: boolean) => {
48
+ const node = new FlowNode({ title: 'lit', position: { x: 100, y: 100 }, inputs: [{ name: 'In', type: 'float64' }] });
49
+ const graph = new NodeSubsystem(new PassSubsystem());
50
+ graph.addNode(node);
51
+ const strokes: Array<{ style: string; width: number }> = [];
52
+ const ctx = stubCtx();
53
+ let width = 1;
54
+ Object.defineProperty(ctx, 'lineWidth', { get: () => width, set: (w: number) => { width = w; } });
55
+ const saved: Array<number> = [];
56
+ (ctx as any).save = () => saved.push(width);
57
+ (ctx as any).restore = () => { width = saved.pop() ?? width; };
58
+ (ctx as any).stroke = function () { strokes.push({ style: this.strokeStyle, width }); };
59
+ const camera = new Camera();
60
+ camera.zoom = 0.1;
61
+
62
+ node.setHighlighted(highlighted);
63
+ graph.render(ctx, camera, undefined);
64
+ return strokes.filter((s) => s.style !== Theme.Node.Border.Highlighted).map((s) => s.width);
65
+ };
66
+
67
+ assert.deepEqual(widths(true), widths(false));
68
+ });
69
+ });
@@ -117,3 +117,27 @@ describe('filtering the new node menu to what could finish a wire', () => {
117
117
  assert.ok(!groups.includes('Math'), `Math should be gone, got ${groups}`);
118
118
  });
119
119
  });
120
+
121
+ describe('filtering the new node menu through casts', () => {
122
+ const graph = {} as any;
123
+ const at = { x: 0, y: 0 };
124
+ const casts = [{ from: 'int', to: 'float64' }];
125
+
126
+ it('dragging an int output offers nodes taking a float64', () => {
127
+ const names = itemNames(publisher().contextMenu(graph, at, {
128
+ needs: PortType.Input,
129
+ types: ['int'],
130
+ casts,
131
+ }));
132
+ assert.deepStrictEqual(names, ['Add', 'Negate', 'Select']);
133
+ });
134
+
135
+ it('dragging an int input does not offer float64 producers', () => {
136
+ const names = itemNames(publisher().contextMenu(graph, at, {
137
+ needs: PortType.Output,
138
+ types: ['int'],
139
+ casts,
140
+ }));
141
+ assert.deepStrictEqual(names, ['Select']);
142
+ });
143
+ });
package/test/port.test.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import * as assert from 'assert';
2
- import { Port, PortConfig, PortType, portsCompatible } from '../src/port';
2
+ import { Port, PortConfig, PortType, portsCompatible, portsConnect } from '../src/port';
3
3
  import { FlowNode } from '../src/node';
4
+ import { DanglingConnection, NodeSubsystem } from '../src/nodes/subsystem';
5
+ import { PassSubsystem } from '../src/pass/subsystem';
6
+ import { Camera } from '../src/camera';
7
+ import { center, stubCtx } from './support';
4
8
 
5
9
  function port(config: PortConfig): Port {
6
10
  // A port only reaches back to its node when it is drawn or connected,
@@ -123,3 +127,47 @@ describe('settling a port on a type', () => {
123
127
  assert.ok(portsCompatible(settled, port({ type: 'float64' })));
124
128
  });
125
129
  });
130
+
131
+ describe('casts', () => {
132
+ const casts = [{ from: 'int', to: 'float64' }];
133
+
134
+ it('lets an output feed an input it casts to', () => {
135
+ assert.ok(portsConnect(port({ type: 'int' }), port({ type: 'float64' }), casts));
136
+ });
137
+
138
+ it('only goes the way it was registered', () => {
139
+ assert.ok(!portsConnect(port({ type: 'float64' }), port({ type: 'int' }), casts));
140
+ });
141
+
142
+ it('reaches any of the types an input takes', () => {
143
+ const lifted = port({ type: 'float64', acceptedTypes: ['float64', '[]float64'] });
144
+ assert.ok(portsConnect(port({ type: 'int' }), lifted, casts));
145
+ });
146
+
147
+ it('refuses mismatched types without one', () => {
148
+ assert.ok(!portsConnect(port({ type: 'int' }), port({ type: 'float64' })));
149
+ });
150
+
151
+ it('wires a node made from a released wire through a cast', () => {
152
+ const graph = new NodeSubsystem(new PassSubsystem(), { casts });
153
+ const source = new FlowNode({ title: 'source', position: { x: 0, y: 0 }, outputs: [{ name: 'Out', type: 'int' }] });
154
+ graph.addNode(source);
155
+ const camera = new Camera();
156
+ const ctx = stubCtx();
157
+ graph.render(ctx, camera, undefined);
158
+
159
+ let dangling: DanglingConnection | null = null;
160
+ graph.setConnectionReleasedHandler((d) => dangling = d);
161
+ const out = center(source.outputPortPosition(0));
162
+ graph.render(ctx, camera, out);
163
+ graph.clickStart(out, camera, false);
164
+ graph.render(ctx, camera, { x: 600, y: 500 });
165
+ graph.clickEnd();
166
+
167
+ const made = new FlowNode({ title: 'made', position: { x: 300, y: 0 }, inputs: [{ name: 'In', type: 'float64' }] });
168
+ graph.addNode(made);
169
+ dangling!.connectTo(made);
170
+
171
+ assert.equal(made.inputPort(0).connections().length, 1);
172
+ });
173
+ });