@elicdavis/node-flow 0.1.19 → 0.2.0

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.
Files changed (55) hide show
  1. package/dist/index.d.mts +386 -227
  2. package/dist/index.d.ts +386 -227
  3. package/dist/index.js +1884 -1076
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +1867 -1075
  6. package/dist/index.mjs.map +1 -1
  7. package/index.html +1134 -329
  8. package/package.json +1 -1
  9. package/src/connection.ts +30 -5
  10. package/src/contextMenu.ts +66 -34
  11. package/src/graph.ts +12 -10
  12. package/src/index.ts +26 -12
  13. package/src/markdown/entry.ts +11 -15
  14. package/src/markdown/syntaxParser.ts +2 -1
  15. package/src/node.ts +162 -111
  16. package/src/nodes/subsystem.ts +123 -29
  17. package/src/notes/note.ts +9 -1
  18. package/src/organize.ts +540 -111
  19. package/src/popup.ts +4 -2
  20. package/src/port.ts +83 -6
  21. package/src/styles/bevel.ts +31 -0
  22. package/src/styles/cursor.ts +1 -0
  23. package/src/styles/text.ts +4 -4
  24. package/src/theme.ts +179 -92
  25. package/src/types/text.ts +5 -1
  26. package/src/utils/color.ts +1 -1
  27. package/src/widgets/button.ts +52 -107
  28. package/src/widgets/color.ts +91 -150
  29. package/src/widgets/factory.ts +89 -46
  30. package/src/widgets/field.ts +99 -0
  31. package/src/widgets/image.ts +107 -165
  32. package/src/widgets/inplace.ts +84 -0
  33. package/src/widgets/number.ts +122 -154
  34. package/src/widgets/row.ts +138 -0
  35. package/src/widgets/slider.ts +101 -216
  36. package/src/widgets/string.ts +39 -130
  37. package/src/widgets/text.ts +81 -92
  38. package/src/widgets/toggle.ts +62 -207
  39. package/src/widgets/value.ts +127 -0
  40. package/src/widgets/widget.ts +180 -34
  41. package/test/context_menu.test.ts +82 -0
  42. package/test/cursor.test.ts +17 -0
  43. package/test/hidden_port.test.ts +125 -0
  44. package/test/hover_after_removal.test.ts +72 -0
  45. package/test/markdown.test.ts +2 -2
  46. package/test/markdown_code_block.test.ts +58 -0
  47. package/test/multi_input.test.ts +124 -0
  48. package/test/organize.test.ts +117 -1
  49. package/test/property_binding.test.ts +49 -0
  50. package/test/support.ts +175 -0
  51. package/test/theme.test.ts +86 -0
  52. package/test/widget_click.test.ts +111 -97
  53. package/test/widget_construction.test.ts +5 -4
  54. package/test/widgets.test.ts +333 -0
  55. package/test/wire_follows_camera.test.ts +57 -0
@@ -6,16 +6,16 @@ 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 { Port, PortType, portsCompatible } from "../port";
9
+ import { arrayInsertIndex, Port, PortType, portsCompatible } from "../port";
10
10
  import { requestRender } from "../render_scheduler";
11
11
  import { BoxStyle } from "../styles/box";
12
12
  import { CursorStyle } from "../styles/cursor";
13
13
  import { Theme } from "../theme";
14
- import { Box, BoxIntersection } from "../types/box";
14
+ import { Box, BoxIntersection, InBox } from "../types/box";
15
15
  import { List } from "../types/list";
16
16
  import { Pool, VectorPool } from "../types/pool";
17
17
  import { AddVector2, CopyVector2, SubVector2, Vector2, Zero } from "../types/vector2";
18
- import { Widget } from "../widgets/widget";
18
+ import { Widget, WidgetPointer } from "../widgets/widget";
19
19
  import { NodeCreatedCallback, NodeFactory, NodeFactoryConfig } from "./factory";
20
20
  import { Publisher } from "./publisher";
21
21
 
@@ -108,6 +108,12 @@ export class NodeSubsystem {
108
108
 
109
109
  #widgetCurrentlyClicking: Widget | null;
110
110
 
111
+ #widgetHoveringBox: Box | null = null;
112
+
113
+ #capturedWidgetBox: Box | null = null;
114
+
115
+ #capturedPointer: Vector2 = Zero();
116
+
111
117
  #cursor: CursorStyle;
112
118
 
113
119
  #nodeFactory: NodeFactory;
@@ -120,10 +126,11 @@ export class NodeSubsystem {
120
126
 
121
127
  #boxSelectionNodes: List<number>;
122
128
 
123
- #boxSelectStyle: BoxStyle;
124
129
 
125
130
  #lastCtx: CanvasRenderingContext2D | null;
126
131
 
132
+ #lastMousePosition: Vector2 | undefined;
133
+
127
134
  // LISTENERS ==============================================================
128
135
 
129
136
  #nodeRemovedCallbacks: Array<NodeRemovedCallback>;
@@ -153,14 +160,6 @@ export class NodeSubsystem {
153
160
  this.#boxSelectEnd_graphSpace = Zero();
154
161
  this.#boxSelectionNodes = new List<number>();
155
162
  this.#lastCtx = null;
156
- this.#boxSelectStyle = new BoxStyle({
157
- border: {
158
- color: Theme.BoxSelect.Color,
159
- size: Theme.BoxSelect.Size,
160
- },
161
- color: "rgba(0,0,0,0)",
162
- radius: Theme.BoxSelect.Radius
163
- });
164
163
  }
165
164
 
166
165
  public addPublisher(identifier: string, publisher: Publisher): void {
@@ -205,6 +204,7 @@ export class NodeSubsystem {
205
204
  }
206
205
  if (intersection.Widget !== undefined) {
207
206
  this.#widgetHovering = intersection.Widget;
207
+ this.#widgetHoveringBox = intersection.WidgetBox ?? null;
208
208
  }
209
209
  if (intersection.Port !== undefined && intersection.Node !== undefined && intersection.PortIndex !== undefined && intersection.PortIsInput !== undefined) {
210
210
  this.#portHovering = {
@@ -231,8 +231,15 @@ export class NodeSubsystem {
231
231
  }
232
232
 
233
233
  if (this.#widgetHovering !== null) {
234
- this.#widgetHovering.ClickStart();
235
- this.#widgetCurrentlyClicking = this.#widgetHovering;
234
+ if (!this.#widgetHovering.Disabled?.()) {
235
+ this.#widgetCurrentlyClicking = this.#widgetHovering;
236
+ this.#capturedWidgetBox = this.#widgetHoveringBox === null ? null : {
237
+ Position: { x: this.#widgetHoveringBox.Position.x, y: this.#widgetHoveringBox.Position.y },
238
+ Size: { x: this.#widgetHoveringBox.Size.x, y: this.#widgetHoveringBox.Size.y },
239
+ };
240
+ CopyVector2(this.#capturedPointer, mousePosition);
241
+ this.#widgetCurrentlyClicking.PointerDown?.(this.#widgetPointer());
242
+ }
236
243
  hoveringSomething = true;
237
244
  }
238
245
 
@@ -252,12 +259,12 @@ export class NodeSubsystem {
252
259
  }
253
260
 
254
261
  if (this.#portHovering.InputPort) {
255
- for (let i = 0; i < this.#connections.length; i++) {
256
- if (this.#connections[i].inPort() === this.#portHovering.Port) {
257
- this.#connections[i].clearInput();
258
- this.#connectionSelected = this.#connections[i];
259
- break;
260
- }
262
+ const port = this.#portHovering.Port;
263
+ const wires = port.connections();
264
+ const picked = wires.find((wire) => wire.mouseOverPort(mousePosition) === port) ?? wires[0];
265
+ if (picked !== undefined) {
266
+ picked.clearInput();
267
+ this.#connectionSelected = picked;
261
268
  }
262
269
  } else {
263
270
  let inNode: FlowNode | null = this.#portHovering.Node
@@ -281,7 +288,21 @@ export class NodeSubsystem {
281
288
  return true;
282
289
  }
283
290
 
291
+ #widgetPointer(): WidgetPointer {
292
+ return {
293
+ position: { x: this.#capturedPointer.x, y: this.#capturedPointer.y },
294
+ inside: this.#capturedWidgetBox !== null && InBox(this.#capturedWidgetBox, this.#capturedPointer),
295
+ };
296
+ }
297
+
284
298
  mouseDragEvent(delta: Vector2, scale: number): boolean {
299
+ if (this.#widgetCurrentlyClicking !== null) {
300
+ this.#capturedPointer.x += delta.x;
301
+ this.#capturedPointer.y += delta.y;
302
+ this.#widgetCurrentlyClicking.PointerMove?.(this.#widgetPointer());
303
+ return true;
304
+ }
305
+
285
306
  let nodeMoved = false;
286
307
 
287
308
  VectorPool.run(() => {
@@ -421,13 +442,20 @@ export class NodeSubsystem {
421
442
 
422
443
  #firstCompatiblePort(node: FlowNode, side: PortType, against: Port): number {
423
444
  const count = side === PortType.Input ? node.inputs() : node.outputs();
445
+ let firstHidden = -1;
424
446
  for (let i = 0; i < count; i++) {
425
447
  const port = side === PortType.Input ? node.inputPort(i) : node.outputPort(i);
426
- if (portsCompatible(port, against)) {
448
+ if (!portsCompatible(port, against)) {
449
+ continue;
450
+ }
451
+ if (!port.hidden()) {
427
452
  return i;
428
453
  }
454
+ if (firstHidden === -1) {
455
+ firstHidden = i;
456
+ }
429
457
  }
430
- return -1;
458
+ return firstHidden;
431
459
  }
432
460
 
433
461
  clickEnd(): void {
@@ -448,10 +476,11 @@ export class NodeSubsystem {
448
476
  }
449
477
 
450
478
  if (this.#widgetCurrentlyClicking !== null) {
451
- if (this.#widgetHovering === this.#widgetCurrentlyClicking) {
452
- this.#widgetCurrentlyClicking.ClickEnd();
453
- }
479
+ const widget = this.#widgetCurrentlyClicking;
480
+ const pointer = this.#widgetPointer();
454
481
  this.#widgetCurrentlyClicking = null;
482
+ this.#capturedWidgetBox = null;
483
+ widget.PointerUp?.(pointer);
455
484
  }
456
485
 
457
486
  if (this.#connectionSelected === null) {
@@ -518,7 +547,7 @@ export class NodeSubsystem {
518
547
  // Remove connection from this subsystem
519
548
  this.#removeConnection(inputPort.connections()[0], false);
520
549
  }
521
- conn.setInput(this.#portHovering.Node, this.#portHovering.Index, replace);
550
+ conn.setInput(this.#portHovering.Node, this.#portHovering.Index, replace, this.#insertIndex());
522
551
 
523
552
  } else {
524
553
  conn.setOutput(this.#portHovering.Node, this.#portHovering.Index);
@@ -684,6 +713,16 @@ export class NodeSubsystem {
684
713
  this.#removeNodeConnections(nodeIndex);
685
714
  const node = this.#nodes[nodeIndex];
686
715
  this.#nodes.splice(nodeIndex, 1);
716
+
717
+ // Hover is only recomputed on render, which is lazy, so it must follow the splice.
718
+ if (this.#nodeHovering === nodeIndex) {
719
+ this.#nodeHovering = -1;
720
+ } else if (this.#nodeHovering > nodeIndex) {
721
+ this.#nodeHovering--;
722
+ }
723
+ if (this.#portHovering?.Node === node) {
724
+ this.#portHovering = null;
725
+ }
687
726
  requestRender();
688
727
  for (let i = 0; i < this.#nodeRemovedCallbacks.length; i++) {
689
728
  this.#nodeRemovedCallbacks[i](node);
@@ -945,9 +984,11 @@ export class NodeSubsystem {
945
984
  }
946
985
 
947
986
  if (intersection.Widget !== undefined) {
948
- // console.log(i, this.#nodes[i].title(), "widget")
949
987
  this.#widgetHovering = intersection.Widget
950
- this.#cursor = CursorStyle.Pointer;
988
+ this.#widgetHoveringBox = intersection.WidgetBox ?? null;
989
+ this.#cursor = intersection.Widget.Disabled?.()
990
+ ? CursorStyle.Default
991
+ : intersection.Widget.Cursor?.() ?? CursorStyle.Pointer;
951
992
  }
952
993
 
953
994
  if (intersection.Port !== undefined && intersection.Node !== undefined && intersection.PortIndex !== undefined && intersection.PortIsInput !== undefined) {
@@ -978,6 +1019,10 @@ export class NodeSubsystem {
978
1019
 
979
1020
  this.#nodes[i].render(ctx, camera, state, mousePosition, this.#postProcessPass);
980
1021
  }
1022
+
1023
+ if (this.#widgetCurrentlyClicking !== null) {
1024
+ this.#cursor = this.#widgetCurrentlyClicking.Cursor?.() ?? CursorStyle.Pointer;
1025
+ }
981
1026
  }
982
1027
 
983
1028
  #boxSelectionScreenspaceBox(camera: Camera): Box {
@@ -991,20 +1036,65 @@ export class NodeSubsystem {
991
1036
  return box;
992
1037
  }
993
1038
 
1039
+ /**
1040
+ * Where a wire being dropped on the hovered array input would go, or
1041
+ * undefined when it is not over one.
1042
+ */
1043
+ #insertIndex(): number | undefined {
1044
+ const hovering = this.#portHovering;
1045
+ if (hovering === null || !hovering.InputPort || this.#lastMousePosition === undefined
1046
+ || hovering.Port.getPortType() !== PortType.InputArray) {
1047
+ return undefined;
1048
+ }
1049
+ const box = hovering.Node.inputPortPosition(hovering.Index);
1050
+ return arrayInsertIndex(box, hovering.Port.connections().length, this.#lastMousePosition.y);
1051
+ }
1052
+
1053
+ #renderInsertMarker(ctx: CanvasRenderingContext2D, camera: Camera): void {
1054
+ const hovering = this.#portHovering;
1055
+ if (this.#connectionSelected === null || this.#connectionSelected.inPort() !== null || hovering === null) {
1056
+ return;
1057
+ }
1058
+ const index = this.#insertIndex();
1059
+ const count = hovering.Port.connections().length;
1060
+ if (index === undefined || count === 0) {
1061
+ return;
1062
+ }
1063
+ const box = hovering.Node.inputPortPosition(hovering.Index);
1064
+ const y = box.Position.y + (index * box.Size.y / count);
1065
+ const overhang = 6 * camera.zoom;
1066
+ ctx.save();
1067
+ ctx.strokeStyle = "#ffffff";
1068
+ ctx.lineWidth = Math.max(1, 2 * camera.zoom);
1069
+ ctx.beginPath();
1070
+ ctx.moveTo(box.Position.x - overhang, y);
1071
+ ctx.lineTo(box.Position.x + box.Size.x + overhang, y);
1072
+ ctx.stroke();
1073
+ ctx.restore();
1074
+ }
1075
+
994
1076
  render(ctx: CanvasRenderingContext2D, camera: Camera, mousePosition: Vector2 | undefined): RenderResults | undefined {
995
1077
  this.#lastCtx = ctx;
1078
+ this.#lastMousePosition = mousePosition;
996
1079
  this.#cursor = CursorStyle.Default;
997
1080
 
998
1081
  // Connections draw first but need port positions, which only exist
999
1082
  // once a node has been drawn so a new node costs one extra frame.
1000
1083
  const awaitingLayout = this.#nodes.some((node) => !node.hasBeenLaidOut());
1001
1084
 
1085
+ for (const node of this.#nodes) {
1086
+ if (node.hasBeenLaidOut()) {
1087
+ node.placePorts(node.calculateBounds(ctx, camera), camera.zoom);
1088
+ }
1089
+ }
1090
+
1002
1091
  TimeExecution("Render_Connections", () => {
1003
1092
  this.#renderConnections(ctx, camera, mousePosition);
1004
1093
  })
1005
1094
  TimeExecution("Render_Nodes", () => {
1006
1095
  this.#renderNodes(ctx, camera, mousePosition);
1007
1096
  })
1097
+ this.#renderInsertMarker(ctx, camera);
1008
1098
 
1009
1099
  if (awaitingLayout) {
1010
1100
  requestRender();
@@ -1013,7 +1103,11 @@ export class NodeSubsystem {
1013
1103
  if (this.#boxSelect) {
1014
1104
  const box = this.#boxSelectionScreenspaceBox(camera);
1015
1105
  ctx.setLineDash([Theme.BoxSelect.LineDashLength]);
1016
- this.#boxSelectStyle.Draw(ctx, box, 1);
1106
+ new BoxStyle({
1107
+ border: { color: Theme.BoxSelect.Color, size: Theme.BoxSelect.Size },
1108
+ color: "rgba(0,0,0,0)",
1109
+ radius: Theme.BoxSelect.Radius,
1110
+ }).Draw(ctx, box, 1);
1017
1111
  ctx.setLineDash([]);
1018
1112
  }
1019
1113
 
package/src/notes/note.ts CHANGED
@@ -5,7 +5,7 @@ import { BoxStyle } from "../styles/box";
5
5
  import { TextAlign } from "../styles/canvasTextAlign";
6
6
  import { TextBaseline } from "../styles/canvasTextBaseline";
7
7
  import { TextStyleConfig } from "../styles/text";
8
- import { Theme } from "../theme";
8
+ import { Theme, themeVersion } from "../theme";
9
9
  import { Box } from "../types/box";
10
10
  import { CopyVector2, Vector2, Zero } from "../types/vector2";
11
11
  import { Camera } from "../camera";
@@ -40,6 +40,8 @@ export class FlowNote {
40
40
 
41
41
  #document: Array<MarkdownEntry>;
42
42
 
43
+ #parsedForTheme = -1;
44
+
43
45
  #width: number;
44
46
 
45
47
  #edittingStyle: BoxStyle;
@@ -99,6 +101,7 @@ export class FlowNote {
99
101
  setText(text: string): void {
100
102
  this.#originalText = text;
101
103
  this.#document = BuildMarkdown(this.#originalText);
104
+ this.#parsedForTheme = themeVersion();
102
105
  for (let i = 0; i < this.#contentChangeCallbacks.length; i++) {
103
106
  this.#contentChangeCallbacks[i](this, text);
104
107
  }
@@ -210,6 +213,11 @@ export class FlowNote {
210
213
  camera.graphSpaceToScreenSpace(this.#position, this.#tempPosition);
211
214
  CopyVector2(this.#lastRenderedBox.Position, this.#tempPosition)
212
215
 
216
+ if (this.#parsedForTheme !== themeVersion()) {
217
+ this.#document = BuildMarkdown(this.#originalText);
218
+ this.#parsedForTheme = themeVersion();
219
+ }
220
+
213
221
  const startY = this.#tempPosition.y;
214
222
  const lineSpacing = Theme.Note.EntrySpacing * camera.zoom;
215
223