@elicdavis/node-flow 0.1.20 → 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 (53) hide show
  1. package/dist/index.d.mts +386 -227
  2. package/dist/index.d.ts +386 -227
  3. package/dist/index.js +1866 -1059
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +1849 -1058
  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 +160 -101
  16. package/src/nodes/subsystem.ts +113 -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/markdown.test.ts +2 -2
  45. package/test/markdown_code_block.test.ts +58 -0
  46. package/test/multi_input.test.ts +124 -0
  47. package/test/organize.test.ts +117 -1
  48. package/test/support.ts +175 -0
  49. package/test/theme.test.ts +86 -0
  50. package/test/widget_click.test.ts +111 -97
  51. package/test/widget_construction.test.ts +5 -4
  52. package/test/widgets.test.ts +333 -0
  53. 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);
@@ -955,9 +984,11 @@ export class NodeSubsystem {
955
984
  }
956
985
 
957
986
  if (intersection.Widget !== undefined) {
958
- // console.log(i, this.#nodes[i].title(), "widget")
959
987
  this.#widgetHovering = intersection.Widget
960
- 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;
961
992
  }
962
993
 
963
994
  if (intersection.Port !== undefined && intersection.Node !== undefined && intersection.PortIndex !== undefined && intersection.PortIsInput !== undefined) {
@@ -988,6 +1019,10 @@ export class NodeSubsystem {
988
1019
 
989
1020
  this.#nodes[i].render(ctx, camera, state, mousePosition, this.#postProcessPass);
990
1021
  }
1022
+
1023
+ if (this.#widgetCurrentlyClicking !== null) {
1024
+ this.#cursor = this.#widgetCurrentlyClicking.Cursor?.() ?? CursorStyle.Pointer;
1025
+ }
991
1026
  }
992
1027
 
993
1028
  #boxSelectionScreenspaceBox(camera: Camera): Box {
@@ -1001,20 +1036,65 @@ export class NodeSubsystem {
1001
1036
  return box;
1002
1037
  }
1003
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
+
1004
1076
  render(ctx: CanvasRenderingContext2D, camera: Camera, mousePosition: Vector2 | undefined): RenderResults | undefined {
1005
1077
  this.#lastCtx = ctx;
1078
+ this.#lastMousePosition = mousePosition;
1006
1079
  this.#cursor = CursorStyle.Default;
1007
1080
 
1008
1081
  // Connections draw first but need port positions, which only exist
1009
1082
  // once a node has been drawn so a new node costs one extra frame.
1010
1083
  const awaitingLayout = this.#nodes.some((node) => !node.hasBeenLaidOut());
1011
1084
 
1085
+ for (const node of this.#nodes) {
1086
+ if (node.hasBeenLaidOut()) {
1087
+ node.placePorts(node.calculateBounds(ctx, camera), camera.zoom);
1088
+ }
1089
+ }
1090
+
1012
1091
  TimeExecution("Render_Connections", () => {
1013
1092
  this.#renderConnections(ctx, camera, mousePosition);
1014
1093
  })
1015
1094
  TimeExecution("Render_Nodes", () => {
1016
1095
  this.#renderNodes(ctx, camera, mousePosition);
1017
1096
  })
1097
+ this.#renderInsertMarker(ctx, camera);
1018
1098
 
1019
1099
  if (awaitingLayout) {
1020
1100
  requestRender();
@@ -1023,7 +1103,11 @@ export class NodeSubsystem {
1023
1103
  if (this.#boxSelect) {
1024
1104
  const box = this.#boxSelectionScreenspaceBox(camera);
1025
1105
  ctx.setLineDash([Theme.BoxSelect.LineDashLength]);
1026
- 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);
1027
1111
  ctx.setLineDash([]);
1028
1112
  }
1029
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