@elicdavis/node-flow 0.2.3 → 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/dist/index.d.mts +29 -1
- package/dist/index.d.ts +29 -1
- package/dist/index.js +146 -19
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +143 -19
- package/dist/index.mjs.map +1 -1
- package/dist/web/NodeFlow.js +8 -0
- package/dist/web/NodeFlow.js.map +7 -0
- package/index.html +2 -1
- package/package.json +1 -1
- package/src/contextMenu.ts +3 -3
- package/src/graph.ts +39 -5
- package/src/index.ts +6 -6
- package/src/input.ts +16 -0
- package/src/markdown/syntaxParser.ts +2 -2
- package/src/node.ts +29 -0
- package/src/nodes/publisher.ts +6 -8
- package/src/nodes/subsystem.ts +43 -5
- package/src/port.ts +31 -0
- package/src/quickMenu.ts +11 -0
- package/src/theme.ts +3 -1
- package/test/markdown.test.ts +11 -0
- package/test/markdown_code_block.test.ts +21 -1
- package/test/mouse_leave.test.ts +70 -0
- package/test/node_hover.test.ts +69 -0
- package/test/node_menu_filter.test.ts +24 -0
- package/test/port.test.ts +49 -1
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
package/src/contextMenu.ts
CHANGED
|
@@ -261,11 +261,11 @@ export class ContextMenu {
|
|
|
261
261
|
|
|
262
262
|
#bounds: Box | null = null;
|
|
263
263
|
|
|
264
|
-
|
|
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
|
|
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
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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;
|
package/src/nodes/publisher.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ContextMenuConfig, ContextMenuItemConfig } from "../contextMenu";
|
|
2
2
|
import { FlowNode, FlowNodeConfig } from '../node';
|
|
3
|
-
import { PortConfig, PortType, portConfigTypes,
|
|
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
|
-
|
|
23
|
-
|
|
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 {
|
package/src/nodes/subsystem.ts
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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 && !
|
|
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 && !
|
|
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 (!
|
|
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",
|
package/test/markdown.test.ts
CHANGED
|
@@ -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
|
});
|