@elicdavis/node-flow 0.1.20 → 0.2.1

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 +393 -226
  2. package/dist/index.d.ts +393 -226
  3. package/dist/index.js +1905 -1058
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +1888 -1057
  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 +19 -14
  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 +136 -0
  33. package/src/widgets/number.ts +115 -154
  34. package/src/widgets/row.ts +138 -0
  35. package/src/widgets/slider.ts +101 -216
  36. package/src/widgets/string.ts +32 -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 +173 -6
  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 +188 -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 +386 -0
  53. package/test/wire_follows_camera.test.ts +57 -0
package/src/node.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Port, PortConfig, PortType } from "./port";
1
+ import { ARRAY_SLOT_PITCH, Port, PortConfig, PortType } from "./port";
2
2
  import { requestRender } from "./render_scheduler";
3
3
  import { FontWeight, TextStyle, TextStyleConfig, TextStyleFallback } from "./styles/text";
4
4
  import { Box, InBox } from "./types/box";
@@ -7,8 +7,9 @@ import { Widget } from './widgets/widget';
7
7
  import { List } from './types/list';
8
8
  import { BoxStyle, BoxStyleConfig, BoxStyleWithFallback } from "./styles/box";
9
9
  import { Text } from "./types/text";
10
- import { GlobalWidgetFactory } from "./widgets/factory";
11
- import { Theme } from "./theme";
10
+ import { GlobalWidgetFactory, WidgetConfig } from "./widgets/factory";
11
+ import { Theme, themeVersion } from "./theme";
12
+ import { drawBevel } from "./styles/bevel";
12
13
  import { TextAlign } from "./styles/canvasTextAlign";
13
14
  import { TextBaseline } from "./styles/canvasTextBaseline";
14
15
  import { CombineContextMenus, ContextMenuConfig, ContextMenuItemConfig } from "./contextMenu";
@@ -32,15 +33,11 @@ const MINIMUM_NODE_WIDTH = 150;
32
33
 
33
34
  type AnyPropertyChangeCallback = (propertyName: string, oldValue: any, newValue: any) => void
34
35
  type PropertyChangeCallback = (oldValue: any, newValue: any) => void
36
+ type PropertyEditCallback = (newValue: any) => void
35
37
  type TitleChangeCallback = (node: FlowNode, oldTitle: string, newTitle: string) => void
36
38
  type InfoChangeCallback = (node: FlowNode, oldInfo: string, newInfo: string) => void
37
39
 
38
40
 
39
- export interface WidgetConfig {
40
- type?: string,
41
- config?: any
42
- }
43
-
44
41
  export interface FlowNodeTitleConfig {
45
42
  textStyle?: TextStyleConfig;
46
43
  color?: string;
@@ -138,6 +135,7 @@ export interface NodeIntersection {
138
135
  // Widget
139
136
  Widget?: Widget
140
137
  WidgetIndex?: number
138
+ WidgetBox?: Box
141
139
 
142
140
  }
143
141
 
@@ -218,15 +216,19 @@ export class FlowNode {
218
216
 
219
217
  // Styling ================================================================
220
218
 
221
- #titleColor: string;
219
+ #styleConfig: FlowNodeStyle | undefined;
220
+
221
+ #styledForTheme = -1;
222
222
 
223
- #selectedStyle: BoxStyle;
223
+ #titleColor!: string;
224
224
 
225
- #stateStyles: Map<NodeState, BoxStyle>;
225
+ #selectedStyle!: BoxStyle;
226
+
227
+ #stateStyles = new Map<NodeState, BoxStyle>();
226
228
 
227
229
  #padding: number;
228
230
 
229
- #portTextStyle: TextStyle;
231
+ #portTextStyle!: TextStyle;
230
232
 
231
233
  #elementSpacing: number;
232
234
 
@@ -246,6 +248,8 @@ export class FlowNode {
246
248
 
247
249
  #registeredPropertyChangeCallbacks: Map<string, Array<PropertyChangeCallback>>;
248
250
 
251
+ #registeredPropertyEditCallbacks = new Map<string, Array<PropertyEditCallback>>();
252
+
249
253
  constructor(config?: FlowNodeConfig) {
250
254
  this.#input = new Array<Port>();
251
255
  this.#output = new Array<Port>();
@@ -307,65 +311,21 @@ export class FlowNode {
307
311
  this.#position = config?.position === undefined ? { x: 0, y: 0 } : config.position;
308
312
  this.#title = new Text(
309
313
  config?.title === undefined ? "" : config.title,
310
- TextStyleFallback(config?.style?.title?.textStyle, {
311
- size: 16,
312
- weight: FontWeight.Bold,
313
- color: Theme.Node.FontColor
314
- })
314
+ TextStyleFallback(config?.style?.title?.textStyle, { size: 16, weight: FontWeight.Bold })
315
315
  );
316
316
 
317
317
  this.#subTitle = new Text(
318
318
  config?.subTitle === undefined ? "" : config.subTitle,
319
- TextStyleFallback(config?.style?.title?.textStyle, {
320
- size: 10,
321
- weight: FontWeight.Bold,
322
- color: Theme.Node.FontColor
323
- })
319
+ TextStyleFallback(config?.style?.title?.textStyle, { size: 10, weight: FontWeight.Bold })
324
320
  );
325
321
 
326
- this.#infoSymbol = new Text(
327
- "i",
328
- {
329
- size: 13,
330
- weight: FontWeight.Bold,
331
- color: Theme.Node.FontColor
332
- }
333
- );
322
+ this.#infoSymbol = new Text("i", { size: 13, weight: FontWeight.Bold });
334
323
  this.#infoText = config?.info ? config?.info : "";
335
324
 
336
- this.#titleColor = config?.style?.title?.color === undefined ? "#154050" : config?.style?.title?.color;
337
-
338
325
  this.#padding = config?.style?.title?.padding === undefined ? 15 : config?.style?.title?.padding;
339
326
 
340
- this.#stateStyles = new Map<NodeState, BoxStyle>();
341
- this.#stateStyles.set(NodeState.Idle, new BoxStyle(BoxStyleWithFallback(config?.style?.idle, {
342
- border: { color: Theme.Node.Border.Idle, size: 1 },
343
- radius: Theme.Node.BorderRadius,
344
- color: Theme.Node.BackgroundColor,
345
- })));
346
- this.#stateStyles.set(NodeState.MouseOver, new BoxStyle(BoxStyleWithFallback(config?.style?.mouseOver, {
347
- border: { color: Theme.Node.Border.MouseOver, size: 1.1, },
348
- radius: Theme.Node.BorderRadius,
349
- color: Theme.Node.BackgroundColor,
350
- })));
351
- this.#stateStyles.set(NodeState.Grabbed, new BoxStyle(BoxStyleWithFallback(config?.style?.grabbed, {
352
- border: { color: Theme.Node.Border.Grabbed, size: 2, },
353
- radius: Theme.Node.BorderRadius,
354
- color: Theme.Node.BackgroundColor,
355
- })));
356
-
357
- this.#selectedStyle = new BoxStyle(BoxStyleWithFallback(config?.style?.selected, {
358
- border: { color: Theme.Node.Border.Selected, size: 1, },
359
- radius: Theme.Node.BorderRadius,
360
- color: Theme.Node.BackgroundColor,
361
- }));
362
-
363
- this.#portTextStyle = new TextStyle({
364
- size: config?.style?.portText?.size === undefined ? 14 : config?.style?.portText?.size,
365
- color: config?.style?.portText?.color === undefined ? Theme.Node.Port.FontColor : config?.style?.portText?.color,
366
- font: config?.style?.portText?.font,
367
- weight: config?.style?.portText?.weight,
368
- });
327
+ this.#styleConfig = config?.style;
328
+ this.#applyStyle();
369
329
 
370
330
  if (config?.inputs !== undefined) {
371
331
  for (let i = 0; i < config.inputs.length; i++) {
@@ -380,12 +340,8 @@ export class FlowNode {
380
340
  }
381
341
 
382
342
  if (config?.widgets !== undefined) {
383
- for (let i = 0; i < config.widgets.length; i++) {
384
- const widget = config.widgets[i];
385
- if (widget.type === undefined) {
386
- continue;
387
- }
388
- this.addWidget(GlobalWidgetFactory.create(this, widget.type, widget.config))
343
+ for (const widget of config.widgets) {
344
+ this.addWidget(GlobalWidgetFactory.createFromConfig(this, widget));
389
345
  }
390
346
  }
391
347
  }
@@ -476,6 +432,34 @@ export class FlowNode {
476
432
  }
477
433
  }
478
434
 
435
+ /**
436
+ * Listens for a person changing the property through a widget, but not
437
+ * for the property being set from code.
438
+ */
439
+ public addPropertyEditListener(name: string, callback: PropertyEditCallback): void {
440
+ let callbacks = this.#registeredPropertyEditCallbacks.get(name);
441
+ if (callbacks === undefined) {
442
+ callbacks = [];
443
+ this.#registeredPropertyEditCallbacks.set(name, callbacks);
444
+ }
445
+ callbacks.push(callback);
446
+ }
447
+
448
+ /**
449
+ * Sets the property as a person's finished edit, so edit listeners hear
450
+ * it even when the value was already showing.
451
+ */
452
+ public editProperty(name: string, value: any): void {
453
+ this.setProperty(name, value);
454
+ const callbacks = this.#registeredPropertyEditCallbacks.get(name);
455
+ if (callbacks === undefined) {
456
+ return;
457
+ }
458
+ for (const callback of [...callbacks]) {
459
+ callback(value);
460
+ }
461
+ }
462
+
479
463
  public getProperty(name: string): any {
480
464
  return this.#data[name];
481
465
  }
@@ -789,6 +773,7 @@ export class FlowNode {
789
773
  // }
790
774
 
791
775
  public calculateBounds(ctx: CanvasRenderingContext2D, camera: Camera): Box {
776
+ this.#followTheme();
792
777
  const tempMeasurement = Zero();
793
778
 
794
779
  const doublePadding = this.#padding * 2;
@@ -808,29 +793,23 @@ export class FlowNode {
808
793
  }
809
794
 
810
795
  size.x += doublePadding;
811
- size.y += doublePadding + (this.#elementSpacing * this.#input.length);
796
+ size.y += doublePadding;
812
797
 
813
- for (let i = 0; i < this.#input.length; i++) {
814
- const port = this.#input[i];
815
- this.#portTextStyle.measure(ctx, 1, port.getDisplayName(), tempMeasurement);
816
- size.y += tempMeasurement.y;
817
- size.x = Math.max(size.x, tempMeasurement.x + doublePadding)
818
- }
819
-
820
- size.y += (this.#elementSpacing * this.#output.length);
821
-
822
- for (let i = 0; i < this.#output.length; i++) {
823
- const port = this.#output[i];
798
+ for (const port of [...this.#input, ...this.#output]) {
799
+ if (port.hidden()) {
800
+ continue;
801
+ }
824
802
  this.#portTextStyle.measure(ctx, 1, port.getDisplayName(), tempMeasurement);
825
- size.y += tempMeasurement.y;
803
+ size.y += this.#portRowHeight(port, tempMeasurement.y) + this.#elementSpacing;
826
804
  size.x = Math.max(size.x, tempMeasurement.x + doublePadding)
827
805
  }
828
806
 
829
- size.y += (this.#elementSpacing * this.#widgets.length);
830
- for (let i = 0; i < this.#widgets.length; i++) {
831
- const element = this.#widgets[i];
832
- const eleSize = element.Size();
833
- size.y += eleSize.y
807
+ for (const widget of this.#widgets) {
808
+ if (widget.Hidden?.()) {
809
+ continue;
810
+ }
811
+ const eleSize = widget.Size();
812
+ size.y += eleSize.y + this.#elementSpacing;
834
813
  size.x = Math.max(size.x, (eleSize.x) + doublePadding)
835
814
  }
836
815
 
@@ -934,7 +913,7 @@ export class FlowNode {
934
913
  }
935
914
 
936
915
  for (let i = 0; i < this.#inputPortPositions.Count(); i++) {
937
- if (InBox(this.#inputPortPositions.At(i), position)) {
916
+ if (!this.#input[i].hidden() && InBox(this.#inputPortPositions.At(i), position)) {
938
917
  intersection.Node = this;
939
918
  intersection.PortIndex = i;
940
919
  intersection.PortIsInput = true;
@@ -943,7 +922,7 @@ export class FlowNode {
943
922
  }
944
923
 
945
924
  for (let i = 0; i < this.#outputPortPositions.Count(); i++) {
946
- if (InBox(this.#outputPortPositions.At(i), position)) {
925
+ if (!this.#output[i].hidden() && InBox(this.#outputPortPositions.At(i), position)) {
947
926
  intersection.Node = this;
948
927
  intersection.PortIndex = i;
949
928
  intersection.PortIsInput = false;
@@ -952,10 +931,12 @@ export class FlowNode {
952
931
  }
953
932
 
954
933
  for (let i = 0; i < this.#widgetPositions.Count(); i++) {
955
- if (InBox(this.#widgetPositions.At(i), position)) {
934
+ const widget = this.#widgets[i];
935
+ if (widget !== undefined && !widget.Hidden?.() && InBox(this.#widgetPositions.At(i), position)) {
956
936
  intersection.Node = this;
957
937
  intersection.WidgetIndex = i;
958
- intersection.Widget = this.#widgets[i];
938
+ intersection.Widget = widget;
939
+ intersection.WidgetBox = this.#widgetPositions.At(i);
959
940
  }
960
941
  }
961
942
 
@@ -1018,6 +999,13 @@ export class FlowNode {
1018
999
  return this.#inputPortPositions.At(index);
1019
1000
  }
1020
1001
 
1002
+ widgetPosition(index: number): Box | undefined {
1003
+ if (index < 0 || index >= this.#widgetPositions.Count()) {
1004
+ return undefined;
1005
+ }
1006
+ return this.#widgetPositions.At(index);
1007
+ }
1008
+
1021
1009
  inputPort(index: number): Port {
1022
1010
  return this.#input[index];
1023
1011
  }
@@ -1038,6 +1026,42 @@ export class FlowNode {
1038
1026
  return this.#output.length;
1039
1027
  }
1040
1028
 
1029
+ #applyStyle(): void {
1030
+ const style = this.#styleConfig;
1031
+ const titleText = style?.title?.textStyle?.color ?? Theme.Node.Title.FontColor;
1032
+ this.#title.style().setColor(titleText);
1033
+ this.#subTitle.style().setColor(titleText);
1034
+ this.#infoSymbol.style().setColor(titleText);
1035
+
1036
+ this.#titleColor = style?.title?.color ?? Theme.Node.Title.Color;
1037
+
1038
+ const box = (config: BoxStyleConfig | undefined, border: string, size: number) =>
1039
+ new BoxStyle(BoxStyleWithFallback(config, {
1040
+ border: { color: border, size },
1041
+ radius: Theme.Node.BorderRadius,
1042
+ color: Theme.Node.BackgroundColor,
1043
+ }));
1044
+ this.#stateStyles.set(NodeState.Idle, box(style?.idle, Theme.Node.Border.Idle, 1));
1045
+ this.#stateStyles.set(NodeState.MouseOver, box(style?.mouseOver, Theme.Node.Border.MouseOver, 1.1));
1046
+ this.#stateStyles.set(NodeState.Grabbed, box(style?.grabbed, Theme.Node.Border.Grabbed, 2));
1047
+ this.#selectedStyle = box(style?.selected, Theme.Node.Border.Selected, 1);
1048
+
1049
+ this.#portTextStyle = new TextStyle({
1050
+ size: style?.portText?.size ?? 14,
1051
+ color: style?.portText?.color ?? Theme.Node.Port.FontColor,
1052
+ font: style?.portText?.font,
1053
+ weight: style?.portText?.weight,
1054
+ });
1055
+
1056
+ this.#styledForTheme = themeVersion();
1057
+ }
1058
+
1059
+ #followTheme(): void {
1060
+ if (this.#styledForTheme !== themeVersion()) {
1061
+ this.#applyStyle();
1062
+ }
1063
+ }
1064
+
1041
1065
  #calculateStyle(state: NodeState): BoxStyle {
1042
1066
  // Let the node being selected override the idle style
1043
1067
  if (this.#selected && state === NodeState.Idle) {
@@ -1059,10 +1083,25 @@ export class FlowNode {
1059
1083
  this.#messages = [];
1060
1084
  }
1061
1085
 
1086
+ #portRowHeight(port: Port, textHeight: number): number {
1087
+ const slots = port.slots();
1088
+ return slots > 1 ? Math.max(textHeight, slots * ARRAY_SLOT_PITCH) : textHeight;
1089
+ }
1090
+
1062
1091
  public hasBeenLaidOut(): boolean {
1063
1092
  return this.#laidOut;
1064
1093
  }
1065
1094
 
1095
+ /**
1096
+ * Throws away where the ports were last placed, so wires wait for the
1097
+ * next draw instead of pointing at the old spots.
1098
+ */
1099
+ public invalidateLayout(): void {
1100
+ this.#laidOut = false;
1101
+ this.#portOffsets = { input: [], output: [] };
1102
+ requestRender();
1103
+ }
1104
+
1066
1105
  #laidOut: boolean = false;
1067
1106
 
1068
1107
  render(ctx: CanvasRenderingContext2D, camera: Camera, state: NodeState, mousePosition: Vector2 | undefined, postProcess: PassSubsystem): void {
@@ -1109,7 +1148,10 @@ export class FlowNode {
1109
1148
  [nodeStyle.radius() * camera.zoom, nodeStyle.radius() * camera.zoom, 0, 0]
1110
1149
  );
1111
1150
  ctx.fill();
1112
- // ctx.stroke();
1151
+
1152
+ if (Theme.Node.Bevel !== null) {
1153
+ drawBevel(ctx, nodeBounds, camera.zoom, Theme.Node.Bevel, false);
1154
+ }
1113
1155
 
1114
1156
  const titlePosition = VectorPool.get();
1115
1157
  titlePosition.x = nodeBounds.Position.x + (nodeBounds.Size.x / 2)
@@ -1209,8 +1251,12 @@ export class FlowNode {
1209
1251
  for (let i = 0; i < this.#input.length; i++) {
1210
1252
  ctx.textAlign = TextAlign.Left;
1211
1253
  const port = this.#input[i];
1254
+ if (port.hidden()) {
1255
+ this.#inputPortPositions.Push({ Position: { x: nodeBounds.Position.x, y: startY }, Size: { x: 0, y: 0 } });
1256
+ continue;
1257
+ }
1212
1258
  this.#portTextStyle.measure(ctx, 1, port.getDisplayName(), tempMeasurement);
1213
- const portHeight = tempMeasurement.y * camera.zoom;
1259
+ const portHeight = this.#portRowHeight(port, tempMeasurement.y) * camera.zoom;
1214
1260
  const position = VectorPool.get();
1215
1261
 
1216
1262
  position.x = nodeBounds.Position.x;
@@ -1231,6 +1277,10 @@ export class FlowNode {
1231
1277
  ctx.textAlign = TextAlign.Right;
1232
1278
  for (let i = 0; i < this.#output.length; i++) {
1233
1279
  const port = this.#output[i];
1280
+ if (port.hidden()) {
1281
+ this.#outputPortPositions.Push({ Position: { x: rightSide, y: startY }, Size: { x: 0, y: 0 } });
1282
+ continue;
1283
+ }
1234
1284
  this.#portTextStyle.measure(ctx, 1, port.getDisplayName(), tempMeasurement);
1235
1285
  const portHeight = tempMeasurement.y * camera.zoom;
1236
1286
  const position = VectorPool.get();
@@ -1250,15 +1300,18 @@ export class FlowNode {
1250
1300
  // Widgets
1251
1301
  for (let i = 0; i < this.#widgets.length; i++) {
1252
1302
  const widget = this.#widgets[i];
1303
+ if (widget.Hidden?.()) {
1304
+ this.#widgetPositions.Push({ Position: { x: nodeBounds.Position.x, y: startY }, Size: { x: 0, y: 0 } });
1305
+ continue;
1306
+ }
1253
1307
  const widgetSize = widget.Size();
1254
- const scaledWidgetWidth = widgetSize.x * camera.zoom;
1255
-
1256
- const position = VectorPool.get();
1257
- position.x = nodeBounds.Position.x + ((nodeBounds.Size.x - scaledWidgetWidth) / 2);
1258
- position.y = startY;
1259
-
1260
- this.#widgetPositions.Push(widget.Draw(ctx, position, camera.zoom, mousePosition));
1261
- startY += (widgetSize.y * camera.zoom) + scaledElementSpacing;
1308
+ const box: Box = {
1309
+ Position: { x: nodeBounds.Position.x + scaledPadding, y: startY },
1310
+ Size: { x: nodeBounds.Size.x - (scaledPadding * 2), y: widgetSize.y * camera.zoom },
1311
+ };
1312
+ widget.Draw(ctx, box, camera.zoom, mousePosition);
1313
+ this.#widgetPositions.Push(box);
1314
+ startY += box.Size.y + scaledElementSpacing;
1262
1315
  }
1263
1316
 
1264
1317
  // Messages
@@ -1303,6 +1356,14 @@ export class FlowNode {
1303
1356
  }
1304
1357
 
1305
1358
  public layoutPortsWithoutDrawing(bounds: Box, zoom: number): boolean {
1359
+ if (!this.placePorts(bounds, zoom)) {
1360
+ return false;
1361
+ }
1362
+ this.#widgetPositions.Clear();
1363
+ return true;
1364
+ }
1365
+
1366
+ public placePorts(bounds: Box, zoom: number): boolean {
1306
1367
  if (this.#portOffsets.input.length === 0 && this.#portOffsets.output.length === 0) {
1307
1368
  return false;
1308
1369
  }
@@ -1327,8 +1388,6 @@ export class FlowNode {
1327
1388
  project(this.#portOffsets.input, this.#inputPortPositions);
1328
1389
  project(this.#portOffsets.output, this.#outputPortPositions);
1329
1390
 
1330
- this.#widgetPositions.Clear();
1331
-
1332
1391
  return true;
1333
1392
  }
1334
1393
  }