@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
package/src/popup.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { Theme } from "./theme";
2
+
1
3
  export interface PopupConfig {
2
4
  title: string;
3
5
  options: Array<string>;
@@ -58,11 +60,11 @@ export class Popup {
58
60
 
59
61
  Show(): void {
60
62
  this.#popup = document.createElement('div');
61
- this.#popup.style.cssText = 'position:absolute;top:0;width:100%;display:flex;height:100%;justify-content:center;align-items:center;background-color:#00000070;';
63
+ this.#popup.style.cssText = `position:absolute;top:0;width:100%;display:flex;height:100%;justify-content:center;align-items:center;background-color:${Theme.Popup.Backdrop};`;
62
64
  document.body.appendChild(this.#popup);
63
65
 
64
66
  const container = document.createElement('div');
65
- container.style.cssText = 'background-color:#000;color:white;font-family:Verdana;border-radius:8px;padding:16px;';
67
+ container.style.cssText = `background-color:${Theme.Popup.BackgroundColor};color:${Theme.Popup.FontColor};font-family:${Theme.Popup.FontFamily};border-radius:${Theme.Popup.BorderRadius}px;padding:16px;`;
66
68
  this.#popup.appendChild(container);
67
69
 
68
70
  const title = document.createElement("h2");
package/src/port.ts CHANGED
@@ -51,6 +51,11 @@ export interface PortConfig {
51
51
  */
52
52
  acceptedTypes?: Array<string>;
53
53
 
54
+ /**
55
+ * Leaves the port off the node until something is connected to it.
56
+ */
57
+ hidden?: boolean;
58
+
54
59
  emptyStyle?: PortStyle;
55
60
  filledStyle?: PortStyle;
56
61
  onConnectionAdded?: ConnectionChangeCallback;
@@ -88,6 +93,28 @@ export function portConfigTypes(config: PortConfig): Array<string> {
88
93
  return [config.type === undefined ? "" : config.type];
89
94
  }
90
95
 
96
+ export const ARRAY_SLOT_PITCH = 12;
97
+
98
+ /**
99
+ * Which gap between a port's wires a point is nearest: 0 is above the first,
100
+ * `count` is below the last.
101
+ */
102
+ export function arrayInsertIndex(box: Box, count: number, y: number): number {
103
+ if (count === 0) {
104
+ return 0;
105
+ }
106
+ const index = Math.round((y - box.Position.y) / (box.Size.y / count));
107
+ return Math.min(count, Math.max(0, index));
108
+ }
109
+
110
+ export function arraySlotCenter(box: Box, count: number, index: number): Vector2 {
111
+ const slots = Math.max(1, count);
112
+ return {
113
+ x: box.Position.x + (box.Size.x / 2),
114
+ y: box.Position.y + ((index + 0.5) * box.Size.y / slots),
115
+ };
116
+ }
117
+
91
118
  // Calculate a color hash for an arbirary type
92
119
  function fallbackColor(type: string, s: number): string {
93
120
  let value = 0;
@@ -128,6 +155,8 @@ export class Port {
128
155
 
129
156
  #array: boolean;
130
157
 
158
+ #hidden: boolean;
159
+
131
160
  #configuredEmptyFill: string | undefined;
132
161
 
133
162
  #configuredFilledFill: string | undefined;
@@ -148,6 +177,7 @@ export class Port {
148
177
  this.#acceptedTypes = config?.acceptedTypes === undefined ? [] : config.acceptedTypes;
149
178
  this.#description = config?.description === undefined ? "" : config.description;
150
179
  this.#array = config?.array === true;
180
+ this.#hidden = config?.hidden === true;
151
181
  this.#configuredEmptyFill = config?.emptyStyle?.fillColor;
152
182
  this.#configuredFilledFill = config?.filledStyle?.fillColor;
153
183
 
@@ -260,9 +290,13 @@ export class Port {
260
290
  this.#refreshDataType();
261
291
  }
262
292
 
263
- addConnection(connection: Connection): void {
264
- const c = this.#connections.length;
265
- this.#connections.push(connection);
293
+ addConnection(connection: Connection, index?: number): void {
294
+ const c = index === undefined
295
+ ? this.#connections.length
296
+ : Math.min(Math.max(index, 0), this.#connections.length);
297
+ const was = this.hidden();
298
+ this.#connections.splice(c, 0, connection);
299
+ this.#visibilityMayHaveChanged(was);
266
300
  for (let i = 0; i < this.#onConnectionAdded.length; i++) {
267
301
  this.#onConnectionAdded[i](connection, c, this, this.#portType, this.#node);
268
302
  }
@@ -287,6 +321,36 @@ export class Port {
287
321
  return this.#connections;
288
322
  }
289
323
 
324
+ /**
325
+ * How many wires the port has room for: one per connection on an array
326
+ * input, so each can be told apart and picked up on its own.
327
+ */
328
+ slots(): number {
329
+ if (this.#portType !== PortType.InputArray) {
330
+ return 1;
331
+ }
332
+ return Math.max(1, this.#connections.length);
333
+ }
334
+
335
+ setHidden(hidden: boolean): void {
336
+ const was = this.hidden();
337
+ this.#hidden = hidden;
338
+ this.#visibilityMayHaveChanged(was);
339
+ }
340
+
341
+ #visibilityMayHaveChanged(was: boolean): void {
342
+ if (was !== this.hidden() || this.#portType === PortType.InputArray) {
343
+ this.#node?.invalidateLayout();
344
+ }
345
+ }
346
+
347
+ /**
348
+ * A port asked to hide still shows while anything is connected to it.
349
+ */
350
+ hidden(): boolean {
351
+ return this.#hidden && this.#connections.length === 0;
352
+ }
353
+
290
354
  addConnectionRemovedListener(callback: ConnectionChangeCallback) {
291
355
  if (callback === undefined) {
292
356
  return;
@@ -297,7 +361,9 @@ export class Port {
297
361
  clearConnection(connection: Connection): void {
298
362
  const index = this.#connections.indexOf(connection);
299
363
  if (index > -1) {
364
+ const was = this.hidden();
300
365
  this.#connections.splice(index, 1);
366
+ this.#visibilityMayHaveChanged(was);
301
367
  for (let i = 0; i < this.#onConnectionRemoved.length; i++) {
302
368
  this.#onConnectionRemoved[i](connection, index, this, this.#portType, this.#node);
303
369
  }
@@ -373,23 +439,34 @@ export class Port {
373
439
  })
374
440
  }
375
441
 
442
+ const slots = this.slots();
443
+ const height = slots === 1 ? scaledRadius * 2 : slots * ARRAY_SLOT_PITCH * camera.zoom;
376
444
  this.#box.Position.x = position.x - scaledRadius;
377
- this.#box.Position.y = position.y - scaledRadius;
445
+ this.#box.Position.y = position.y - (height / 2);
378
446
  this.#box.Size.x = scaledRadius * 2;
379
- this.#box.Size.y = scaledRadius * 2;
447
+ this.#box.Size.y = height;
380
448
 
381
449
  ctx.strokeStyle = style.borderColor as string;
382
450
  ctx.fillStyle = style.fillColor as string;
383
451
 
384
452
  ctx.beginPath();
385
453
  if (this.#portType === PortType.InputArray) {
386
- ctx.rect(position.x - scaledRadius, position.y - scaledRadius, scaledRadius * 2, scaledRadius * 2)
454
+ ctx.roundRect(this.#box.Position.x, this.#box.Position.y, this.#box.Size.x, this.#box.Size.y, slots === 1 ? 0 : scaledRadius);
387
455
  } else {
388
456
  ctx.arc(position.x, position.y, scaledRadius, 0, 2 * Math.PI);
389
457
  }
390
458
  ctx.fill();
391
459
  ctx.stroke();
392
460
 
461
+ if (slots > 1) {
462
+ ctx.fillStyle = style.borderColor as string;
463
+ for (let i = 0; i < slots; i++) {
464
+ const slot = arraySlotCenter(this.#box, slots, i);
465
+ ctx.beginPath();
466
+ ctx.arc(slot.x, slot.y, scaledRadius * 0.4, 0, 2 * Math.PI);
467
+ ctx.fill();
468
+ }
469
+ }
393
470
 
394
471
  return this.#box;
395
472
  }
@@ -0,0 +1,31 @@
1
+ import { BevelColors } from "../theme";
2
+ import { Box } from "../types/box";
3
+
4
+ export function drawBevel(ctx: CanvasRenderingContext2D, box: Box, scale: number, colors: BevelColors, sunken: boolean): void {
5
+ const width = Math.max(1, 2 * scale);
6
+ const half = width / 2;
7
+ const left = box.Position.x + half;
8
+ const top = box.Position.y + half;
9
+ const right = box.Position.x + box.Size.x - half;
10
+ const bottom = box.Position.y + box.Size.y - half;
11
+
12
+ ctx.save();
13
+ ctx.lineWidth = width;
14
+ ctx.lineCap = "square";
15
+
16
+ ctx.strokeStyle = sunken ? colors.Dark : colors.Light;
17
+ ctx.beginPath();
18
+ ctx.moveTo(left, bottom);
19
+ ctx.lineTo(left, top);
20
+ ctx.lineTo(right, top);
21
+ ctx.stroke();
22
+
23
+ ctx.strokeStyle = sunken ? colors.Light : colors.Dark;
24
+ ctx.beginPath();
25
+ ctx.moveTo(right, top);
26
+ ctx.lineTo(right, bottom);
27
+ ctx.lineTo(left, bottom);
28
+ ctx.stroke();
29
+
30
+ ctx.restore();
31
+ }
@@ -15,4 +15,5 @@ export enum CursorStyle {
15
15
  ZoomOut = 'zoom-out',
16
16
  None = 'none',
17
17
  ContextMenu = 'context-menu',
18
+ EWResize = 'ew-resize',
18
19
  }
@@ -38,7 +38,7 @@ export class TextStyle {
38
38
 
39
39
  #color: string;
40
40
 
41
- #font: string;
41
+ #font: string | undefined;
42
42
 
43
43
  #weight: FontWeight;
44
44
 
@@ -47,14 +47,14 @@ export class TextStyle {
47
47
  constructor(config: TextStyleConfig | undefined) {
48
48
  this.#size = config?.size === undefined ? DefaultSize : config.size;
49
49
  this.#color = config?.color === undefined ? DefaultColor : config.color;
50
- this.#font = config?.font === undefined ? Theme.FontFamily : config.font;
50
+ this.#font = config?.font;
51
51
  this.#weight = config?.weight === undefined ? FontWeight.Normal : config.weight;
52
52
  this.#fontStyle = config?.style === undefined ? FontStyle.Normal : config.style;
53
53
  }
54
54
 
55
55
  setupStyle(ctx: CanvasRenderingContext2D, scale: number): void {
56
56
  ctx.fillStyle = this.#color;
57
- ctx.font = this.#fontStyle + " " + this.#weight + " " + (this.#size * scale) + "px " + this.#font;
57
+ ctx.font = this.#fontStyle + " " + this.#weight + " " + (this.#size * scale) + "px " + this.getFont();
58
58
  }
59
59
 
60
60
  getSize(): number {
@@ -62,7 +62,7 @@ export class TextStyle {
62
62
  }
63
63
 
64
64
  getFont(): string {
65
- return this.#font;
65
+ return this.#font ?? Theme.FontFamily;
66
66
  }
67
67
 
68
68
  getWeight(): FontWeight {
package/src/theme.ts CHANGED
@@ -1,92 +1,179 @@
1
- export const Theme = {
2
- FontFamily: "Courier New",
3
- Graph: {
4
- BackgroundColor: "#07212a",
5
- },
6
- Node: {
7
- FontColor: "#afb9bb",
8
- BackgroundColor: "#0c2b35",
9
- Title: {
10
- Color: "#154050",
11
- FontColor: "#afb9bb",
12
- Padding: 5
13
- },
14
- BorderRadius: 15,
15
- Border: {
16
- Idle: "#1c1c1c",
17
- MouseOver: "#8e8e8e",
18
- Grabbed: "white",
19
- Selected: "#6e6e6e"
20
- },
21
- Port: {
22
- FontColor: "#afb9bb"
23
- },
24
- Message: {
25
- InfoColor: "#afb9bb",
26
- WarnColor: "#f1f663ff",
27
- ErrorColor: "#FF0000"
28
- }
29
- },
30
- BoxSelect: {
31
- Color: "white",
32
- Size: 1,
33
- Radius: 2,
34
- LineDashLength: 5,
35
- },
36
- Widget: {
37
- FontColor: "#afb9bb",
38
- BackgroundColor: "#0f313c",
39
- Border: {
40
- Color: "#154050",
41
- Size: 2,
42
- Radius: 2
43
- },
44
- Hover: {
45
- BackgroundColor: "#195366"
46
- },
47
- Slider: {
48
- FillColor: "#07212A",
49
- },
50
- Button: {
51
- Click: {
52
- BackgroundColor: "#1f637a"
53
- },
54
- }
55
- },
56
- ContextMenu: {
57
- BackgroundColor: "#07212A",
58
- HighlightColor: "#205A6D",
59
- FontColor: "#afb9bb",
60
- DisabledFontColor: "#4a585c",
61
- },
62
- Minimap: {
63
- BackgroundColor: "rgba(10, 16, 18, 0.85)",
64
- BorderColor: "#1c1c1c",
65
- BorderRadius: 4,
66
- Padding: 6,
67
- NodeColor: "#6b7f85",
68
- ViewportColor: "rgba(175, 185, 187, 0.25)",
69
- },
70
- Note: {
71
- FontColor: "#afb9bb",
72
- FontSize: 16,
73
- EntrySpacing: 20,
74
- LineSpacing: 5,
75
- DotSize: 3,
76
- HeaderLineWidth: 2,
77
- H1: {
78
- FontSize: 32,
79
- },
80
- H2: {
81
- FontSize: 16,
82
- },
83
- H3: {
84
- FontSize: 16,
85
- },
86
- CodeBlock: {
87
- BackgroundColor: "#0c2b35",
88
- Padding: 16,
89
- BorderRadius: 4,
90
- }
91
- }
92
- }
1
+ import { requestRender } from "./render_scheduler";
2
+
3
+ export interface BevelColors {
4
+ Light: string;
5
+ Dark: string;
6
+ }
7
+
8
+ const defaults = {
9
+ FontFamily: "Courier New",
10
+ Graph: {
11
+ BackgroundColor: "#07212a",
12
+ DotColor: "rgb(41, 54, 57)",
13
+ },
14
+ Node: {
15
+ FontColor: "#afb9bb",
16
+ BackgroundColor: "#0c2b35",
17
+ Title: {
18
+ Color: "#154050",
19
+ FontColor: "#afb9bb",
20
+ Padding: 5
21
+ },
22
+ BorderRadius: 15,
23
+ Border: {
24
+ Idle: "#1c1c1c",
25
+ MouseOver: "#8e8e8e",
26
+ Grabbed: "white",
27
+ Selected: "#6e6e6e"
28
+ },
29
+ Bevel: null as BevelColors | null,
30
+ Port: {
31
+ FontColor: "#afb9bb"
32
+ },
33
+ Message: {
34
+ InfoColor: "#afb9bb",
35
+ WarnColor: "#f1f663ff",
36
+ ErrorColor: "#FF0000"
37
+ }
38
+ },
39
+ BoxSelect: {
40
+ Color: "white",
41
+ Size: 1,
42
+ Radius: 2,
43
+ LineDashLength: 5,
44
+ },
45
+ Widget: {
46
+ FontColor: "#afb9bb",
47
+ LabelColor: "#7f9497",
48
+ FontSize: 14,
49
+ Padding: 6,
50
+ Gap: 4,
51
+ BackgroundColor: "#0f313c",
52
+ Border: {
53
+ Color: "#154050",
54
+ Size: 2,
55
+ Radius: 2
56
+ },
57
+ Bevel: null as BevelColors | null,
58
+ Hover: {
59
+ BackgroundColor: "#195366"
60
+ },
61
+ Input: {
62
+ BackgroundColor: "#0f313c",
63
+ HoverBackgroundColor: "#195366",
64
+ FontColor: "#afb9bb",
65
+ },
66
+ Slider: {
67
+ FillColor: "#07212A",
68
+ },
69
+ Button: {
70
+ Click: {
71
+ BackgroundColor: "#1f637a"
72
+ },
73
+ }
74
+ },
75
+ ContextMenu: {
76
+ BackgroundColor: "#07212A",
77
+ HighlightColor: "#205A6D",
78
+ HighlightFontColor: "#afb9bb",
79
+ FontColor: "#afb9bb",
80
+ DisabledFontColor: "#4a585c",
81
+ BorderRadius: 5,
82
+ },
83
+ Minimap: {
84
+ BackgroundColor: "rgba(10, 16, 18, 0.85)",
85
+ BorderColor: "#1c1c1c",
86
+ BorderRadius: 4,
87
+ Padding: 6,
88
+ NodeColor: "#6b7f85",
89
+ ViewportColor: "rgba(175, 185, 187, 0.25)",
90
+ },
91
+ Popup: {
92
+ BackgroundColor: "#000000",
93
+ FontColor: "#ffffff",
94
+ FontFamily: "Verdana",
95
+ BorderRadius: 8,
96
+ Backdrop: "#00000070",
97
+ },
98
+ Note: {
99
+ FontColor: "#afb9bb",
100
+ FontSize: 16,
101
+ EntrySpacing: 20,
102
+ LineSpacing: 5,
103
+ DotSize: 3,
104
+ HeaderLineWidth: 2,
105
+ H1: {
106
+ FontSize: 32,
107
+ },
108
+ H2: {
109
+ FontSize: 16,
110
+ },
111
+ H3: {
112
+ FontSize: 16,
113
+ },
114
+ CodeBlock: {
115
+ BackgroundColor: "#0c2b35",
116
+ FontColor: "#afb9bb",
117
+ Padding: 16,
118
+ BorderRadius: 4,
119
+ }
120
+ }
121
+ };
122
+
123
+ export type ThemeValues = typeof defaults;
124
+
125
+ type DeepPartial<T> = {
126
+ [K in keyof T]?: T[K] extends object | null
127
+ ? (null extends T[K] ? T[K] : DeepPartial<T[K]>)
128
+ : T[K];
129
+ };
130
+
131
+ export type ThemeConfig = DeepPartial<ThemeValues>;
132
+
133
+ function clone<T>(value: T): T {
134
+ return JSON.parse(JSON.stringify(value));
135
+ }
136
+
137
+ function merge(target: any, source: any): any {
138
+ if (source === undefined) {
139
+ return target;
140
+ }
141
+ if (source === null || typeof source !== "object" || target === null || typeof target !== "object") {
142
+ return source;
143
+ }
144
+ for (const key of Object.keys(source)) {
145
+ target[key] = merge(target[key], source[key]);
146
+ }
147
+ return target;
148
+ }
149
+
150
+ export const DefaultTheme: Readonly<ThemeValues> = Object.freeze(clone(defaults));
151
+
152
+ export const Theme: ThemeValues = clone(defaults);
153
+
154
+ let version = 0;
155
+
156
+ const listeners = new Set<() => void>();
157
+
158
+ /**
159
+ * Replaces the theme: the defaults, with anything in config laid over them.
160
+ * Everything already on screen picks the change up on its next draw.
161
+ */
162
+ export function setTheme(config?: ThemeConfig): void {
163
+ const next = merge(clone(defaults), config);
164
+ for (const key of Object.keys(next) as Array<keyof ThemeValues>) {
165
+ (Theme as any)[key] = next[key];
166
+ }
167
+ version++;
168
+ listeners.forEach((listener) => listener());
169
+ requestRender();
170
+ }
171
+
172
+ export function themeVersion(): number {
173
+ return version;
174
+ }
175
+
176
+ export function onThemeChange(listener: () => void): () => void {
177
+ listeners.add(listener);
178
+ return () => listeners.delete(listener);
179
+ }
package/src/types/text.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { requestRender } from "../render_scheduler";
2
+ import { themeVersion } from "../theme";
2
3
  import { FontWeight, TextStyle, TextStyleConfig } from "../styles/text";
3
4
  import { splitString, splitStringIntoLines } from "../utils/string";
4
5
  import { CopyVector2, ScaleVector, Vector2, Zero } from "./vector2";
@@ -12,6 +13,8 @@ export class Text {
12
13
 
13
14
  #measured: boolean
14
15
 
16
+ #measuredForTheme = -1;
17
+
15
18
  #size: Vector2;
16
19
 
17
20
  #style: TextStyle;
@@ -110,9 +113,10 @@ export class Text {
110
113
  }
111
114
 
112
115
  #measure(ctx: CanvasRenderingContext2D): void {
113
- if (this.#measured) {
116
+ if (this.#measured && this.#measuredForTheme === themeVersion()) {
114
117
  return;
115
118
  }
119
+ this.#measuredForTheme = themeVersion();
116
120
 
117
121
 
118
122
  this.#style.setupStyle(ctx, 1);
@@ -22,7 +22,7 @@ export function RgbToHex(c: Color): string {
22
22
  }
23
23
 
24
24
  export function HexToColor(hex: string): Color | null {
25
- var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
25
+ var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})(?:[a-f\d]{2})?$/i.exec(hex);
26
26
  return result ? {
27
27
  r: parseInt(result[1], 16) / 255,
28
28
  g: parseInt(result[2], 16) / 255,