@elicdavis/node-flow 0.1.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 (68) hide show
  1. package/.github/workflows/deploy.yml +57 -0
  2. package/README.md +159 -0
  3. package/dev.tsconfig.json +18 -0
  4. package/docs/promotional-image.png +0 -0
  5. package/esbuild.ts +32 -0
  6. package/index.html +311 -0
  7. package/package.json +50 -0
  8. package/src/camera.ts +30 -0
  9. package/src/connection.ts +249 -0
  10. package/src/contextMenu.ts +367 -0
  11. package/src/draggable.ts +13 -0
  12. package/src/elements/base.ts +192 -0
  13. package/src/elements/container.ts +119 -0
  14. package/src/elements/interfaces.ts +50 -0
  15. package/src/elements/text.ts +86 -0
  16. package/src/graph.ts +535 -0
  17. package/src/graphSubsystem.ts +31 -0
  18. package/src/index.ts +28 -0
  19. package/src/input.ts +153 -0
  20. package/src/markdown/entry.ts +323 -0
  21. package/src/markdown/lexicalParser.ts +148 -0
  22. package/src/markdown/markdown.ts +14 -0
  23. package/src/markdown/syntaxParser.ts +344 -0
  24. package/src/markdown/token.ts +43 -0
  25. package/src/metadata.ts +3 -0
  26. package/src/node.ts +1130 -0
  27. package/src/nodes/factory.ts +87 -0
  28. package/src/nodes/publisher.ts +98 -0
  29. package/src/nodes/subsystem.ts +760 -0
  30. package/src/notes/note.ts +295 -0
  31. package/src/notes/subsystem.ts +262 -0
  32. package/src/organize.ts +181 -0
  33. package/src/pass/subsystem.ts +64 -0
  34. package/src/performance.ts +9 -0
  35. package/src/popup.ts +81 -0
  36. package/src/popups/form.ts +63 -0
  37. package/src/popups/string.ts +33 -0
  38. package/src/port.ts +223 -0
  39. package/src/quickMenu.ts +330 -0
  40. package/src/styles/box.ts +104 -0
  41. package/src/styles/canvasTextAlign.ts +8 -0
  42. package/src/styles/canvasTextBaseline.ts +9 -0
  43. package/src/styles/cursor.ts +18 -0
  44. package/src/styles/stroke.ts +39 -0
  45. package/src/styles/text.ts +90 -0
  46. package/src/styles/textBox.ts +82 -0
  47. package/src/theme.ts +78 -0
  48. package/src/types/box.ts +126 -0
  49. package/src/types/list.ts +41 -0
  50. package/src/types/pool.ts +66 -0
  51. package/src/types/text.ts +136 -0
  52. package/src/types/vector2.ts +34 -0
  53. package/src/utils/color.ts +136 -0
  54. package/src/utils/html.ts +21 -0
  55. package/src/utils/math.ts +8 -0
  56. package/src/utils/string.ts +98 -0
  57. package/src/widgets/button.ts +107 -0
  58. package/src/widgets/color.ts +151 -0
  59. package/src/widgets/factory.ts +46 -0
  60. package/src/widgets/image.ts +163 -0
  61. package/src/widgets/number.ts +153 -0
  62. package/src/widgets/slider.ts +216 -0
  63. package/src/widgets/string.ts +128 -0
  64. package/src/widgets/text.ts +92 -0
  65. package/src/widgets/toggle.ts +205 -0
  66. package/src/widgets/widget.ts +12 -0
  67. package/tsconfig.json +15 -0
  68. package/tsup.config.ts +10 -0
package/src/node.ts ADDED
@@ -0,0 +1,1130 @@
1
+ import { Port, PortConfig, PortType } from "./port";
2
+ import { FontWeight, TextStyle, TextStyleConfig, TextStyleFallback } from "./styles/text";
3
+ import { Box, InBox } from "./types/box";
4
+ import { CopyVector2, Distance, ScaleVector, Vector2, Zero } from "./types/vector2";
5
+ import { Widget } from './widgets/widget';
6
+ import { List } from './types/list';
7
+ import { BoxStyle, BoxStyleConfig, BoxStyleWithFallback } from "./styles/box";
8
+ import { Text } from "./types/text";
9
+ import { GlobalWidgetFactory } from "./widgets/factory";
10
+ import { Theme } from "./theme";
11
+ import { TextAlign } from "./styles/canvasTextAlign";
12
+ import { TextBaseline } from "./styles/canvasTextBaseline";
13
+ import { CombineContextMenus, ContextMenuConfig, ContextMenuItemConfig } from "./contextMenu";
14
+ import { nodeFlowGroup } from "./nodes/subsystem";
15
+ import { SetStringPopup } from "./popups/string";
16
+ import { ButtonWidget } from "./widgets/button";
17
+ import { FormPopup } from "./popups/form";
18
+ import { NumberWidget } from "./widgets/number";
19
+ import { StringWidget } from "./widgets/string";
20
+ import { ToggleWidget } from "./widgets/toggle";
21
+ import { ColorWidget } from "./widgets/color";
22
+ import { SliderWidget } from "./widgets/slider";
23
+ import { ImageWidget } from "./widgets/image";
24
+ import { VectorPool } from "./types/pool";
25
+ import { Camera } from "./camera";
26
+ import { PassSubsystem } from "./pass/subsystem";
27
+ import { splitString, splitStringIntoLines } from "./utils/string";
28
+ import { Metadata } from './metadata';
29
+
30
+ const MINIMUM_NODE_WIDTH = 150;
31
+
32
+ type AnyPropertyChangeCallback = (propertyName: string, oldValue: any, newValue: any) => void
33
+ type PropertyChangeCallback = (oldValue: any, newValue: any) => void
34
+ type TitleChangeCallback = (node: FlowNode, oldTitle: string, newTitle: string) => void
35
+ type InfoChangeCallback = (node: FlowNode, oldInfo: string, newInfo: string) => void
36
+
37
+
38
+
39
+ export interface WidgetConfig {
40
+ type?: string,
41
+ config?: any
42
+ }
43
+
44
+ export interface FlowNodeTitleConfig {
45
+ textStyle?: TextStyleConfig;
46
+ color?: string;
47
+ padding?: number;
48
+ }
49
+
50
+ export interface FlowNodeStyle {
51
+ title?: FlowNodeTitleConfig;
52
+ idle?: BoxStyleConfig;
53
+ mouseOver?: BoxStyleConfig;
54
+ grabbed?: BoxStyleConfig;
55
+ selected?: BoxStyleConfig;
56
+ portText?: TextStyleConfig;
57
+ }
58
+
59
+ export interface FlowNodeConfig {
60
+ position?: Vector2;
61
+ title?: string;
62
+ subTitle?: string;
63
+ info?: string;
64
+ locked?: boolean;
65
+ data?: Metadata;
66
+ contextMenu?: ContextMenuConfig;
67
+ metadata?: any;
68
+
69
+ canEditTitle?: boolean;
70
+ canEditInfo?: boolean;
71
+ canEditPorts?: boolean;
72
+
73
+ // Ports
74
+ inputs?: Array<PortConfig>;
75
+ outputs?: Array<PortConfig>;
76
+
77
+ // Callbacks
78
+ onGrab?: () => void;
79
+ onRelease?: () => void;
80
+ onSelect?: () => void;
81
+ onUnselect?: () => void;
82
+ onDragStop?: (FlowNode: FlowNode) => void;
83
+ onFileDrop?: (file: File) => void;
84
+ onTitleChange?: TitleChangeCallback;
85
+ onInfoChange?: InfoChangeCallback;
86
+
87
+ // Widgets
88
+ widgets?: Array<WidgetConfig>;
89
+
90
+ // Styling
91
+ style?: FlowNodeStyle;
92
+ }
93
+
94
+ export enum NodeState {
95
+ Idle,
96
+ MouseOver,
97
+ Grabbed
98
+ }
99
+
100
+ export interface NodeIntersection {
101
+ Node?: FlowNode
102
+
103
+ // Port
104
+ Port?: Port
105
+ PortIndex?: number
106
+ PortIsInput?: boolean
107
+
108
+ // Widget
109
+ Widget?: Widget
110
+ WidgetIndex?: number
111
+
112
+ }
113
+
114
+ export class FlowNode {
115
+
116
+ #position: Vector2;
117
+
118
+ #title: Text;
119
+
120
+ #subTitle: Text;
121
+
122
+ #infoSymbol: Text;
123
+
124
+ #infoText: string;
125
+
126
+ #input: Array<Port>;
127
+
128
+ #output: Array<Port>;
129
+
130
+ #widgets: Array<Widget>;
131
+
132
+ #locked: boolean;
133
+
134
+ #canEditTitle: boolean;
135
+
136
+ #canEditInfo: boolean;
137
+
138
+ #canEditPorts: boolean;
139
+
140
+ #contextMenu: ContextMenuConfig | null;
141
+
142
+ #metadata: any;
143
+
144
+ // Callbacks
145
+
146
+ #onSelect: Array<() => void>;
147
+
148
+ #onUnselect: Array<() => void>;
149
+
150
+ #onFiledrop: Array<(file: File) => void>;
151
+
152
+ #onDragStop: Array<(node: FlowNode) => void>;
153
+
154
+ #titleChangeCallback: Array<TitleChangeCallback>;
155
+
156
+ #infoChangeCallback: Array<InfoChangeCallback>;
157
+
158
+ // Styling ================================================================
159
+
160
+ #titleColor: string;
161
+
162
+ #selectedStyle: BoxStyle;
163
+
164
+ #stateStyles: Map<NodeState, BoxStyle>;
165
+
166
+ #padding: number;
167
+
168
+ #portTextStyle: TextStyle;
169
+
170
+ #elementSpacing: number;
171
+
172
+ // Runtime ================================================================
173
+
174
+ #selected: boolean;
175
+
176
+ #inputPortPositions: List<Box>;
177
+
178
+ #outputPortPositions: List<Box>;
179
+
180
+ #widgetPositions: List<Box>;
181
+
182
+ #data: Metadata;
183
+
184
+ #registeredAnyPropertyChangeCallbacks: Array<AnyPropertyChangeCallback>;
185
+
186
+ #registeredPropertyChangeCallbacks: Map<string, Array<PropertyChangeCallback>>;
187
+
188
+ constructor(config?: FlowNodeConfig) {
189
+ this.#input = new Array<Port>();
190
+ this.#output = new Array<Port>();
191
+ this.#widgets = new Array<Widget>();
192
+ this.#inputPortPositions = new List<Box>();
193
+ this.#outputPortPositions = new List<Box>();
194
+ this.#widgetPositions = new List<Box>();
195
+ this.#elementSpacing = 15;
196
+ this.#locked = config?.locked === undefined ? false : config.locked;
197
+ this.#data = config?.data === undefined ? {} : config?.data;
198
+ this.#registeredPropertyChangeCallbacks = new Map<string, Array<PropertyChangeCallback>>();
199
+ this.#registeredAnyPropertyChangeCallbacks = new Array<AnyPropertyChangeCallback>();
200
+ this.#canEditPorts = config?.canEditPorts === undefined ? false : config.canEditPorts;
201
+ this.#canEditTitle = config?.canEditTitle === undefined ? false : config.canEditTitle;
202
+ this.#canEditInfo = config?.canEditInfo === undefined ? false : config.canEditInfo;
203
+ this.#contextMenu = config?.contextMenu === undefined ? null : config.contextMenu;
204
+ this.#metadata = config?.metadata;
205
+
206
+ this.#selected = false;
207
+ this.#onSelect = new Array<() => void>();
208
+ this.#onUnselect = new Array<() => void>();
209
+ this.#onFiledrop = new Array<(file: File) => void>();
210
+ this.#onDragStop = new Array<() => void>();
211
+ this.#titleChangeCallback = new Array<TitleChangeCallback>();
212
+ this.#infoChangeCallback = new Array<InfoChangeCallback>();
213
+
214
+ if (config?.onSelect) {
215
+ this.#onSelect.push(config?.onSelect);
216
+ }
217
+
218
+ if (config?.onUnselect) {
219
+ this.#onUnselect.push(config?.onUnselect);
220
+ }
221
+
222
+ if (config?.onFileDrop) {
223
+ this.#onFiledrop.push(config?.onFileDrop);
224
+ }
225
+
226
+ if (config?.onDragStop) {
227
+ this.#onDragStop.push(config.onDragStop);
228
+ }
229
+
230
+ if (config?.onTitleChange) {
231
+ this.#titleChangeCallback.push(config.onTitleChange);
232
+ }
233
+
234
+ if (config?.onInfoChange) {
235
+ this.#infoChangeCallback.push(config.onInfoChange);
236
+ }
237
+
238
+ this.#position = config?.position === undefined ? { x: 0, y: 0 } : config.position;
239
+ this.#title = new Text(
240
+ config?.title === undefined ? "" : config.title,
241
+ TextStyleFallback(config?.style?.title?.textStyle, {
242
+ size: 16,
243
+ weight: FontWeight.Bold,
244
+ color: Theme.Node.FontColor
245
+ })
246
+ );
247
+
248
+ this.#subTitle = new Text(
249
+ config?.subTitle === undefined ? "" : config.subTitle,
250
+ TextStyleFallback(config?.style?.title?.textStyle, {
251
+ size: 10,
252
+ weight: FontWeight.Bold,
253
+ color: Theme.Node.FontColor
254
+ })
255
+ );
256
+
257
+ this.#infoSymbol = new Text(
258
+ "i",
259
+ {
260
+ size: 13,
261
+ weight: FontWeight.Bold,
262
+ color: Theme.Node.FontColor
263
+ }
264
+ );
265
+ this.#infoText = config?.info ? config?.info : "";
266
+
267
+ this.#titleColor = config?.style?.title?.color === undefined ? "#154050" : config?.style?.title?.color;
268
+
269
+ this.#padding = config?.style?.title?.padding === undefined ? 15 : config?.style?.title?.padding;
270
+
271
+ this.#stateStyles = new Map<NodeState, BoxStyle>();
272
+ this.#stateStyles.set(NodeState.Idle, new BoxStyle(BoxStyleWithFallback(config?.style?.idle, {
273
+ border: { color: Theme.Node.Border.Idle, size: 1 },
274
+ radius: Theme.Node.BorderRadius,
275
+ color: Theme.Node.BackgroundColor,
276
+ })));
277
+ this.#stateStyles.set(NodeState.MouseOver, new BoxStyle(BoxStyleWithFallback(config?.style?.mouseOver, {
278
+ border: { color: Theme.Node.Border.MouseOver, size: 1.1, },
279
+ radius: Theme.Node.BorderRadius,
280
+ color: Theme.Node.BackgroundColor,
281
+ })));
282
+ this.#stateStyles.set(NodeState.Grabbed, new BoxStyle(BoxStyleWithFallback(config?.style?.grabbed, {
283
+ border: { color: Theme.Node.Border.Grabbed, size: 2, },
284
+ radius: Theme.Node.BorderRadius,
285
+ color: Theme.Node.BackgroundColor,
286
+ })));
287
+
288
+ this.#selectedStyle = new BoxStyle(BoxStyleWithFallback(config?.style?.selected, {
289
+ border: { color: Theme.Node.Border.Selected, size: 1, },
290
+ radius: Theme.Node.BorderRadius,
291
+ color: Theme.Node.BackgroundColor,
292
+ }));
293
+
294
+ this.#portTextStyle = new TextStyle({
295
+ size: config?.style?.portText?.size === undefined ? 14 : config?.style?.portText?.size,
296
+ color: config?.style?.portText?.color === undefined ? Theme.Node.Port.FontColor : config?.style?.portText?.color,
297
+ font: config?.style?.portText?.font,
298
+ weight: config?.style?.portText?.weight,
299
+ });
300
+
301
+ if (config?.inputs !== undefined) {
302
+ for (let i = 0; i < config.inputs.length; i++) {
303
+ this.addInput(config.inputs[i]);
304
+ }
305
+ }
306
+
307
+ if (config?.outputs !== undefined) {
308
+ for (let i = 0; i < config.outputs.length; i++) {
309
+ this.addOutput(config.outputs[i]);
310
+ }
311
+ }
312
+
313
+ if (config?.widgets !== undefined) {
314
+ for (let i = 0; i < config.widgets.length; i++) {
315
+ const widget = config.widgets[i];
316
+ if (widget.type === undefined) {
317
+ continue;
318
+ }
319
+ this.addWidget(GlobalWidgetFactory.create(this, widget.type, widget.config))
320
+ }
321
+ }
322
+ }
323
+
324
+ public metadata(): any {
325
+ return this.#metadata;
326
+ }
327
+
328
+ public selected(): boolean {
329
+ return this.#selected;
330
+ }
331
+
332
+ public select(): void {
333
+ if (this.#selected) {
334
+ return;
335
+ }
336
+ this.#selected = true;
337
+ for (let i = 0; i < this.#onSelect.length; i++) {
338
+ this.#onSelect[i]();
339
+ }
340
+ }
341
+
342
+ public raiseDragStoppedEvent() {
343
+ for (let i = 0; i < this.#onDragStop.length; i++) {
344
+ this.#onDragStop[i](this);
345
+ }
346
+ }
347
+
348
+ public addTitleChangeListener(callback: TitleChangeCallback): void {
349
+ if (callback === undefined || callback === null) {
350
+ }
351
+ this.#titleChangeCallback.push(callback);
352
+ }
353
+
354
+ public addInfoChangeListener(callback: InfoChangeCallback): void {
355
+ if (callback === undefined || callback === null) {
356
+ }
357
+ this.#infoChangeCallback.push(callback);
358
+ }
359
+
360
+
361
+ public addDragStoppedListener(callback: (node: FlowNode) => void): void {
362
+ if (callback === undefined || callback === null) {
363
+ }
364
+ this.#onDragStop.push(callback);
365
+ }
366
+
367
+ public addAnyPropertyChangeListener(callback: AnyPropertyChangeCallback): void {
368
+ if (callback === undefined || callback === null) {
369
+ }
370
+ this.#registeredAnyPropertyChangeCallbacks.push(callback);
371
+ }
372
+
373
+ public addPropertyChangeListener(name: string, callback: PropertyChangeCallback): void {
374
+ if (!this.#registeredPropertyChangeCallbacks.has(name)) {
375
+ this.#registeredPropertyChangeCallbacks.set(name, []);
376
+ }
377
+
378
+ const callbacks = this.#registeredPropertyChangeCallbacks.get(name);
379
+ if (callbacks === undefined) {
380
+ return;
381
+ }
382
+ callbacks.push(callback);
383
+ }
384
+
385
+ public setProperty(name: string, value: any): void {
386
+ const oldValue = this.#data[name];
387
+ if (oldValue === value) {
388
+ return;
389
+ }
390
+
391
+ this.#data[name] = value;
392
+
393
+ for (let i = 0; i < this.#registeredAnyPropertyChangeCallbacks.length; i++) {
394
+ this.#registeredAnyPropertyChangeCallbacks[i](name, oldValue, value);
395
+ }
396
+
397
+ const callbacks = this.#registeredPropertyChangeCallbacks.get(name);
398
+ if (callbacks === undefined) {
399
+ return;
400
+ }
401
+
402
+ for (let i = 0; i < callbacks.length; i++) {
403
+ callbacks[i](oldValue, value);
404
+ }
405
+ }
406
+
407
+ public getProperty(name: string): any {
408
+ return this.#data[name];
409
+ }
410
+
411
+ #popupNodeTitleSelection(): void {
412
+ SetStringPopup({
413
+ title: "Set Node Title",
414
+ startingValue: this.title(),
415
+ onUpdate: (value: string): void => {
416
+ this.setTitle(value);
417
+ },
418
+ }).Show();
419
+ }
420
+
421
+ #popupNodeInfoSelection(): void {
422
+ SetStringPopup({
423
+ title: "Set Node Info",
424
+ startingValue: this.#infoText,
425
+ onUpdate: (value: string): void => {
426
+ this.setInfo(value);
427
+ },
428
+ }).Show();
429
+ }
430
+
431
+ #popupNewButtonWidget(): void {
432
+ SetStringPopup({
433
+ title: "New Button Widget Text",
434
+ startingValue: "My Button",
435
+ onUpdate: (value: string): void => {
436
+ this.addWidget(new ButtonWidget({
437
+ text: value
438
+ }));
439
+ },
440
+ }).Show()
441
+ }
442
+
443
+ #widgetSubmenu(): ContextMenuConfig {
444
+ return {
445
+ name: "Widget",
446
+ items: [
447
+ {
448
+ name: "Button",
449
+ callback: this.#popupNewButtonWidget.bind(this),
450
+ },
451
+ {
452
+ name: "Number",
453
+ callback: () => {
454
+ this.addWidget(new NumberWidget(this))
455
+ }
456
+ },
457
+ {
458
+ name: "Color",
459
+ callback: () => {
460
+ this.addWidget(new ColorWidget(this))
461
+ }
462
+ },
463
+ {
464
+ name: "Slider",
465
+ callback: () => {
466
+ FormPopup({
467
+ title: "New Slider",
468
+ form: [
469
+ {
470
+ name: "min",
471
+ type: "number",
472
+ startingValue: 0
473
+ },
474
+ {
475
+ name: "max",
476
+ type: "number",
477
+ startingValue: 100
478
+ }
479
+ ],
480
+ onUpdate: (data: Array<any>) => {
481
+ this.addWidget(new SliderWidget(this, {
482
+ min: data[0],
483
+ max: data[1],
484
+ }));
485
+ }
486
+ }).Show();
487
+ }
488
+ },
489
+ {
490
+ name: "String",
491
+ callback: () => {
492
+ this.addWidget(new StringWidget(this))
493
+ }
494
+ },
495
+ {
496
+ name: "Toggle",
497
+ callback: () => {
498
+ this.addWidget(new ToggleWidget(this))
499
+ }
500
+ },
501
+ {
502
+ name: "Image",
503
+ callback: () => {
504
+ FormPopup({
505
+ title: "New Image",
506
+ form: [
507
+ {
508
+ name: "URL",
509
+ type: "text",
510
+ startingValue: "https://pbs.twimg.com/media/GYabtu6bsAA7m99?format=jpg&name=medium"
511
+ },
512
+ {
513
+ name: "Max Width",
514
+ type: "number",
515
+ startingValue: MINIMUM_NODE_WIDTH
516
+ },
517
+ {
518
+ name: "Max Height",
519
+ type: "number",
520
+ startingValue: MINIMUM_NODE_WIDTH
521
+ }
522
+ ],
523
+ onUpdate: (data: Array<any>) => {
524
+ this.addWidget(new ImageWidget({
525
+ image: data[0],
526
+ maxWidth: data[1],
527
+ maxHeight: data[2],
528
+ }));
529
+ }
530
+ }).Show();
531
+ }
532
+ }
533
+ ]
534
+ };
535
+ }
536
+
537
+ public contextMenu(): ContextMenuConfig {
538
+ let config: ContextMenuConfig = {
539
+ group: nodeFlowGroup,
540
+ items: [
541
+ // {
542
+ // name: "Debug",
543
+ // callback: () => {
544
+ // console.log(this);
545
+ // }
546
+ // }
547
+ ],
548
+ subMenus: [],
549
+ }
550
+
551
+ if (this.canEditPorts() || this.canEditTitle() || this.#canEditInfo) {
552
+
553
+ const items = new Array<ContextMenuItemConfig>();
554
+ if (this.canEditTitle()) {
555
+ items.push({
556
+ name: "Title",
557
+ callback: () => {
558
+ this.#popupNodeTitleSelection();
559
+ }
560
+ })
561
+ }
562
+
563
+ if (this.#canEditInfo) {
564
+ items.push({
565
+ name: "Info",
566
+ callback: () => {
567
+ this.#popupNodeInfoSelection();
568
+ }
569
+ })
570
+ }
571
+
572
+ const submenus = new Array<ContextMenuConfig>();
573
+ if (this.canEditPorts()) {
574
+ submenus.push({
575
+ name: "Add",
576
+ items: [
577
+ {
578
+ name: "Input",
579
+ callback: () => {
580
+ FormPopup({
581
+ title: "New Input",
582
+ form: [
583
+ {
584
+ name: "name",
585
+ type: "text",
586
+ startingValue: "input"
587
+ },
588
+ {
589
+ name: "type",
590
+ type: "text",
591
+ startingValue: "string"
592
+ }
593
+ ],
594
+ onUpdate: (data: Array<any>) => {
595
+ this.addInput({
596
+ name: data[0],
597
+ type: data[1]
598
+ })
599
+ }
600
+ }).Show();
601
+ }
602
+ },
603
+ {
604
+ name: "Output",
605
+ callback: () => {
606
+ FormPopup({
607
+ title: "New Output",
608
+ form: [
609
+ {
610
+ name: "name",
611
+ type: "text",
612
+ startingValue: "output"
613
+ },
614
+ {
615
+ name: "type",
616
+ type: "text",
617
+ startingValue: "string"
618
+ }
619
+ ],
620
+ onUpdate: (data: Array<any>) => {
621
+ this.addOutput({
622
+ name: data[0],
623
+ type: data[1]
624
+ })
625
+ }
626
+ }).Show();
627
+ }
628
+ },
629
+ ],
630
+ subMenus: [
631
+ this.#widgetSubmenu(),
632
+ ]
633
+ });
634
+ }
635
+
636
+ config.subMenus?.push({
637
+ group: nodeFlowGroup,
638
+ name: "Edit",
639
+ items: items,
640
+ subMenus: submenus,
641
+ })
642
+ }
643
+
644
+ if (this.locked()) {
645
+ config.items?.push({
646
+ name: "Unlock Node Position",
647
+ group: nodeFlowGroup,
648
+ callback: this.unlock.bind(this),
649
+ });
650
+ } else {
651
+ config.items?.push({
652
+ name: "Lock Node Position",
653
+ group: nodeFlowGroup,
654
+ callback: this.lock.bind(this),
655
+ })
656
+ }
657
+
658
+ if (this.#contextMenu !== null) {
659
+ config = CombineContextMenus(config, this.#contextMenu)
660
+ }
661
+
662
+ return config;
663
+ }
664
+
665
+ public unselect(): void {
666
+ if (!this.#selected) {
667
+ return;
668
+ }
669
+ this.#selected = false;
670
+ for (let i = 0; i < this.#onUnselect.length; i++) {
671
+ this.#onUnselect[i]();
672
+ }
673
+ }
674
+
675
+ public addUnselectListener(callback: () => void): void {
676
+ this.#onUnselect.push(callback);
677
+ }
678
+
679
+ public addSelectListener(callback: () => void): void {
680
+ this.#onSelect.push(callback);
681
+ }
682
+
683
+ public dropFile(file: File): void {
684
+ for (let i = 0; i < this.#onFiledrop.length; i++) {
685
+ this.#onFiledrop[i](file);
686
+ }
687
+ }
688
+
689
+ public addFileDropListener(callback: (file: File) => void): void {
690
+ this.#onFiledrop.push(callback);
691
+ }
692
+
693
+ public locked(): boolean {
694
+ return this.#locked;
695
+ }
696
+
697
+ public lock(): void {
698
+ this.#locked = true;
699
+ }
700
+
701
+ public unlock(): void {
702
+ this.#locked = false;
703
+ }
704
+
705
+ public setPosition(position: Vector2): void {
706
+ CopyVector2(this.#position, position);
707
+ }
708
+
709
+ public getPosition(): Vector2 {
710
+ return this.#position;
711
+ }
712
+
713
+ // #measureTitleText(ctx: CanvasRenderingContext2D, scale: number): Vector2 {
714
+ // return this.#titleTextStyle.measure(ctx, scale, this.#title);
715
+ // }
716
+
717
+ public calculateBounds(ctx: CanvasRenderingContext2D, camera: Camera): Box {
718
+ const tempMeasurement = Zero();
719
+
720
+ const doublePadding = this.#padding * 2;
721
+
722
+ const screenSpacePosition = Zero();
723
+ camera.graphSpaceToScreenSpace(this.#position, screenSpacePosition);
724
+
725
+ const size = Zero();
726
+ this.#title.size(ctx, 1, size);
727
+
728
+ const subtitleSize = Zero();
729
+ this.#subTitle.size(ctx, 1, subtitleSize);
730
+ size.x = Math.max(size.x, subtitleSize.x);
731
+
732
+ if (this.#infoText !== "") {
733
+ size.x += (size.y * 4);
734
+ }
735
+
736
+ size.x += doublePadding;
737
+ size.y += doublePadding + (this.#elementSpacing * this.#input.length);
738
+
739
+ for (let i = 0; i < this.#input.length; i++) {
740
+ const port = this.#input[i];
741
+ this.#portTextStyle.measure(ctx, 1, port.getDisplayName(), tempMeasurement);
742
+ size.y += tempMeasurement.y;
743
+ size.x = Math.max(size.x, tempMeasurement.x + doublePadding)
744
+ }
745
+
746
+ size.y += (this.#elementSpacing * this.#output.length);
747
+
748
+ for (let i = 0; i < this.#output.length; i++) {
749
+ const port = this.#output[i];
750
+ this.#portTextStyle.measure(ctx, 1, port.getDisplayName(), tempMeasurement);
751
+ size.y += tempMeasurement.y;
752
+ size.x = Math.max(size.x, tempMeasurement.x + doublePadding)
753
+ }
754
+
755
+ size.y += (this.#elementSpacing * this.#widgets.length);
756
+ for (let i = 0; i < this.#widgets.length; i++) {
757
+ const element = this.#widgets[i];
758
+ const eleSize = element.Size();
759
+ size.y += eleSize.y
760
+ size.x = Math.max(size.x, (eleSize.x) + doublePadding)
761
+ }
762
+
763
+ // Add some padding at the end!
764
+ size.y += this.#elementSpacing
765
+
766
+ size.x = Math.max(size.x, MINIMUM_NODE_WIDTH)
767
+ ScaleVector(size, camera.zoom);
768
+
769
+ return {
770
+ Position: screenSpacePosition,
771
+ Size: size,
772
+ }
773
+ }
774
+
775
+ addInput(config: PortConfig): Port {
776
+ const port = new Port(this, config.array ? PortType.InputArray : PortType.Input, config);
777
+ this.#input.push(port);
778
+ return port;
779
+ }
780
+
781
+ addOutput(config: PortConfig): Port {
782
+ const port = new Port(this, PortType.Output, config);
783
+ this.#output.push(port);
784
+ return port;
785
+ }
786
+
787
+ addWidget(widget: Widget): void {
788
+ this.#widgets.push(widget);
789
+ }
790
+
791
+ getWidget(index: number): Widget {
792
+ return this.#widgets[index];
793
+ }
794
+
795
+ widgetCount(): number {
796
+ return this.#widgets.length;
797
+ }
798
+
799
+ translate(delta: Vector2): void {
800
+ this.#position.x += delta.x;
801
+ this.#position.y += delta.y;
802
+ }
803
+
804
+ inBounds(ctx: CanvasRenderingContext2D, camera: Camera, position: Vector2): NodeIntersection {
805
+ var intersection: NodeIntersection = {}
806
+
807
+ const box = this.calculateBounds(ctx, camera);
808
+ if (InBox(box, position)) {
809
+ intersection.Node = this;
810
+ }
811
+
812
+ for (let i = 0; i < this.#inputPortPositions.Count(); i++) {
813
+ if (InBox(this.#inputPortPositions.At(i), position)) {
814
+ intersection.Node = this;
815
+ intersection.PortIndex = i;
816
+ intersection.PortIsInput = true;
817
+ intersection.Port = this.#input[i]
818
+ }
819
+ }
820
+
821
+ for (let i = 0; i < this.#outputPortPositions.Count(); i++) {
822
+ if (InBox(this.#outputPortPositions.At(i), position)) {
823
+ intersection.Node = this;
824
+ intersection.PortIndex = i;
825
+ intersection.PortIsInput = false;
826
+ intersection.Port = this.#output[i];
827
+ }
828
+ }
829
+
830
+ for (let i = 0; i < this.#widgetPositions.Count(); i++) {
831
+ if (InBox(this.#widgetPositions.At(i), position)) {
832
+ intersection.Node = this;
833
+ intersection.WidgetIndex = i;
834
+ intersection.Widget = this.#widgets[i];
835
+ }
836
+ }
837
+
838
+ return intersection;
839
+ }
840
+
841
+ canEditTitle(): boolean {
842
+ return this.#canEditTitle;
843
+ }
844
+
845
+ canEditPorts(): boolean {
846
+ return this.#canEditPorts;
847
+ }
848
+
849
+ title(): string {
850
+ return this.#title.get();
851
+ }
852
+
853
+ setTitle(newTitle: string): void {
854
+ if (!this.#canEditTitle) {
855
+ console.warn("setTitle instruction ignored, as node has been marked un-editable");
856
+ return;
857
+ }
858
+
859
+ let cleaned = newTitle;
860
+ if (cleaned === null || cleaned === undefined) {
861
+ cleaned = "";
862
+ }
863
+
864
+ const old = this.title();
865
+ if (cleaned === old) {
866
+ return;
867
+ }
868
+
869
+ this.#title.set(cleaned);
870
+
871
+ for (let i = 0; i < this.#titleChangeCallback.length; i++) {
872
+ const callback = this.#titleChangeCallback[i];
873
+ callback(this, old, cleaned);
874
+ }
875
+ }
876
+
877
+ setInfo(newInfo: string): void {
878
+ if (!this.#canEditInfo) {
879
+ console.warn("setInfo instruction ignored, as node has been marked un-editable");
880
+ }
881
+
882
+
883
+ let cleaned = newInfo;
884
+ if (cleaned === null || cleaned === undefined) {
885
+ cleaned = "";
886
+ }
887
+
888
+ if (cleaned === this.#infoText) {
889
+ return;
890
+ }
891
+
892
+ const old = this.#infoText;
893
+ this.#infoText = cleaned;
894
+
895
+ for (let i = 0; i < this.#infoChangeCallback.length; i++) {
896
+ const callback = this.#infoChangeCallback[i];
897
+ callback(this, old, cleaned);
898
+ }
899
+ }
900
+
901
+ inputPortPosition(index: number): Box {
902
+ return this.#inputPortPositions.At(index);
903
+ }
904
+
905
+ inputPort(index: number): Port {
906
+ return this.#input[index];
907
+ }
908
+
909
+ inputs(): number {
910
+ return this.#input.length;
911
+ }
912
+
913
+ outputPortPosition(index: number): Box {
914
+ return this.#outputPortPositions.At(index);
915
+ }
916
+
917
+ outputPort(index: number): Port {
918
+ return this.#output[index];
919
+ }
920
+
921
+ outputs(): number {
922
+ return this.#output.length;
923
+ }
924
+
925
+ #calculateStyle(state: NodeState): BoxStyle {
926
+ // Let the node being selected override the idle style
927
+ if (this.#selected && state === NodeState.Idle) {
928
+ return this.#selectedStyle;
929
+ }
930
+
931
+ let boxStyle = this.#stateStyles.get(state);
932
+ if (boxStyle === undefined) {
933
+ throw new Error("no registered border style for state: " + state)
934
+ }
935
+ return boxStyle;
936
+ }
937
+
938
+ render(ctx: CanvasRenderingContext2D, camera: Camera, state: NodeState, mousePosition: Vector2 | undefined, postProcess: PassSubsystem): void {
939
+ VectorPool.run(() => {
940
+ const tempMeasurement = VectorPool.get();
941
+
942
+ this.#inputPortPositions.Clear();
943
+ this.#outputPortPositions.Clear();
944
+ this.#widgetPositions.Clear();
945
+ const scaledPadding = this.#padding * camera.zoom;
946
+ const scaledElementSpacing = this.#elementSpacing * camera.zoom;
947
+
948
+ const nodeBounds = this.calculateBounds(ctx, camera);
949
+
950
+ // Background
951
+ const nodeStyle = this.#calculateStyle(state);
952
+ nodeStyle.Draw(ctx, nodeBounds, camera.zoom);
953
+
954
+ // Title
955
+ const borderSize = nodeStyle.borderSize();
956
+ ctx.textAlign = TextAlign.Center;
957
+ ctx.textBaseline = TextBaseline.Middle;
958
+
959
+ const titleSize = VectorPool.get();
960
+ this.#title.size(ctx, camera.zoom, titleSize);
961
+
962
+ const subtitleSize = VectorPool.get();
963
+ this.#subTitle.size(ctx, camera.zoom, subtitleSize);
964
+
965
+ const titleBoxSize = VectorPool.get();
966
+ titleBoxSize.x = nodeBounds.Size.x;
967
+ titleBoxSize.y = titleSize.y + (scaledPadding * 2);
968
+
969
+ ctx.fillStyle = this.#titleColor;
970
+ ctx.beginPath();
971
+ const titleX = nodeBounds.Position.x + (borderSize * camera.zoom * 0.5)
972
+ const titleY = nodeBounds.Position.y + (borderSize * camera.zoom * 0.5)
973
+ const titleWidth = titleBoxSize.x - (borderSize * camera.zoom);
974
+ const titleHeight = titleBoxSize.y - (borderSize * camera.zoom * 0.5);
975
+ ctx.roundRect(
976
+ titleX, titleY, titleWidth, titleHeight,
977
+ [nodeStyle.radius() * camera.zoom, nodeStyle.radius() * camera.zoom, 0, 0]
978
+ );
979
+ ctx.fill();
980
+ // ctx.stroke();
981
+
982
+ const titlePosition = VectorPool.get();
983
+ titlePosition.x = nodeBounds.Position.x + (nodeBounds.Size.x / 2)
984
+ titlePosition.y = nodeBounds.Position.y + scaledPadding + (titleSize.y / 2)
985
+
986
+ if (this.#subTitle.get() !== "") {
987
+ const subtitlePosition = VectorPool.get();
988
+ CopyVector2(subtitlePosition, titlePosition);
989
+
990
+ // TODO: I'm fudging a bunch of numbers ot make it fit nicely
991
+ // together.
992
+ //
993
+ // Need to come up with like, proper logic about how we ensure
994
+ // title and subtitle fit together
995
+ subtitlePosition.y -= titleSize.y / 1.5;
996
+ titlePosition.y += scaledPadding / 2
997
+
998
+ this.#subTitle.render(ctx, camera.zoom, subtitlePosition);
999
+ }
1000
+
1001
+ this.#title.render(ctx, camera.zoom, titlePosition);
1002
+
1003
+ // Info symbol
1004
+ if (this.#infoText !== "") {
1005
+ const infoRadius = titleHeight * .25;
1006
+ const centerX = titleX + titleWidth - (titleHeight * .25) - infoRadius;
1007
+ const centerY = titleY + (titleHeight * .25) + infoRadius;
1008
+
1009
+ const infoPosition = VectorPool.get();
1010
+ infoPosition.x = centerX;
1011
+ infoPosition.y = centerY + (titleHeight * .025);
1012
+
1013
+ let infoLineThickness = 1.5 * camera.zoom;
1014
+
1015
+ if (mousePosition && Distance(infoPosition, mousePosition) <= infoRadius) {
1016
+ infoLineThickness *= 1.5;
1017
+
1018
+ // This is a slightly expensive function if we're not
1019
+ // caching, but it only executes while we're mousing over
1020
+ // the info symbol, so I'm not particularly obligated to
1021
+ // optimize it.
1022
+ //
1023
+ // If you're just bored, def cache the split string method,
1024
+ // and figure out a system to recalculate as a node has its
1025
+ // title, port, or widgets change.
1026
+ //
1027
+ // Should probably break out the lambda into a proper
1028
+ // function as well. I think as it is, we're creating a new
1029
+ // lambda function per frame?
1030
+ postProcess.queue(() => {
1031
+ this.#infoSymbol.style().setupStyle(ctx, camera.zoom);
1032
+ ctx.textAlign = TextAlign.Center;
1033
+
1034
+ const infoBoxWidth = titleWidth * 1.5;
1035
+ const textEntries = splitStringIntoLines(ctx, this.#infoText, infoBoxWidth);
1036
+
1037
+ const lineHeight = (this.#infoSymbol.style().getSize() + 2);
1038
+ const infoBoxHeight = lineHeight * textEntries.length * camera.zoom;
1039
+ const scaledLineHeight = lineHeight * camera.zoom;
1040
+ const start = titleY - infoBoxHeight - (scaledLineHeight / 2);
1041
+
1042
+ // Draw box
1043
+ ctx.fillStyle = this.#titleColor;
1044
+ ctx.beginPath();
1045
+ ctx.roundRect(
1046
+ centerX - (infoBoxWidth / 2) - (scaledLineHeight / 2),
1047
+ start - scaledLineHeight,
1048
+ infoBoxWidth + scaledLineHeight,
1049
+ infoBoxHeight + scaledLineHeight,
1050
+ nodeStyle.radius() * camera.zoom
1051
+ );
1052
+ ctx.fill();
1053
+
1054
+ // Render Text
1055
+ this.#infoSymbol.style().setupStyle(ctx, camera.zoom);
1056
+ for (let i = 0; i < textEntries.length; i++) {
1057
+ const entry = textEntries[i];
1058
+ ctx.fillText(entry, centerX, start + (i * lineHeight * camera.zoom));
1059
+ }
1060
+ });
1061
+ }
1062
+
1063
+ ctx.beginPath();
1064
+ ctx.arc(centerX, centerY, infoRadius, 0, 2 * Math.PI, false);
1065
+ ctx.lineWidth = infoLineThickness;
1066
+ ctx.strokeStyle = this.#infoSymbol.getColor();
1067
+ ctx.stroke();
1068
+
1069
+ ctx.textAlign = TextAlign.Center;
1070
+ this.#infoSymbol.render(ctx, camera.zoom, infoPosition);
1071
+ }
1072
+
1073
+
1074
+ // Input Ports
1075
+ let startY = nodeBounds.Position.y + (scaledPadding * 2) + titleSize.y + scaledElementSpacing;
1076
+ const leftSide = nodeBounds.Position.x + scaledPadding;
1077
+ for (let i = 0; i < this.#input.length; i++) {
1078
+ ctx.textAlign = TextAlign.Left;
1079
+ const port = this.#input[i];
1080
+ this.#portTextStyle.measure(ctx, camera.zoom, port.getDisplayName(), tempMeasurement);
1081
+ const position = VectorPool.get();
1082
+
1083
+ position.x = nodeBounds.Position.x;
1084
+ position.y = startY + (tempMeasurement.y / 2);
1085
+
1086
+ // Text
1087
+ this.#portTextStyle.setupStyle(ctx, camera.zoom);
1088
+ ctx.fillText(port.getDisplayName(), leftSide, position.y);
1089
+
1090
+ // Port
1091
+ this.#inputPortPositions.Push(port.render(ctx, position, camera, mousePosition, postProcess));
1092
+
1093
+ startY += tempMeasurement.y + scaledElementSpacing;
1094
+ }
1095
+
1096
+ // Output Ports
1097
+ const rightSide = nodeBounds.Position.x + nodeBounds.Size.x;
1098
+ ctx.textAlign = TextAlign.Right;
1099
+ for (let i = 0; i < this.#output.length; i++) {
1100
+ const port = this.#output[i];
1101
+ this.#portTextStyle.measure(ctx, camera.zoom, port.getDisplayName(), tempMeasurement);
1102
+ const position = VectorPool.get();
1103
+ position.x = rightSide;
1104
+ position.y = startY + (tempMeasurement.y / 2);
1105
+
1106
+ // Text
1107
+ this.#portTextStyle.setupStyle(ctx, camera.zoom);
1108
+ ctx.fillText(port.getDisplayName(), rightSide - scaledPadding, position.y);
1109
+
1110
+ // Port
1111
+ this.#outputPortPositions.Push(port.render(ctx, position, camera, mousePosition, postProcess));
1112
+
1113
+ startY += tempMeasurement.y + scaledElementSpacing;
1114
+ }
1115
+
1116
+ for (let i = 0; i < this.#widgets.length; i++) {
1117
+ const widget = this.#widgets[i];
1118
+ const widgetSize = widget.Size();
1119
+ const scaledWidgetWidth = widgetSize.x * camera.zoom;
1120
+
1121
+ const position = VectorPool.get();
1122
+ position.x = nodeBounds.Position.x + ((nodeBounds.Size.x - scaledWidgetWidth) / 2);
1123
+ position.y = startY;
1124
+
1125
+ this.#widgetPositions.Push(widget.Draw(ctx, position, camera.zoom, mousePosition));
1126
+ startY += (widgetSize.y * camera.zoom) + scaledElementSpacing;
1127
+ }
1128
+ })
1129
+ }
1130
+ }