@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
package/dist/index.mjs CHANGED
@@ -1,8 +1,46 @@
1
+ // src/render_scheduler.ts
2
+ var subscribers = /* @__PURE__ */ new Set();
3
+ var frameQueued = false;
4
+ var continuous = 0;
5
+ var canSchedule = () => typeof requestAnimationFrame === "function";
6
+ function onRenderRequested(redraw) {
7
+ subscribers.add(redraw);
8
+ return () => {
9
+ subscribers.delete(redraw);
10
+ };
11
+ }
12
+ function requestRender() {
13
+ if (frameQueued || !canSchedule()) {
14
+ return;
15
+ }
16
+ frameQueued = true;
17
+ requestAnimationFrame(() => {
18
+ frameQueued = false;
19
+ subscribers.forEach((redraw) => redraw());
20
+ if (continuous > 0) {
21
+ requestRender();
22
+ }
23
+ });
24
+ }
25
+ function renderContinuously() {
26
+ continuous++;
27
+ requestRender();
28
+ let released = false;
29
+ return () => {
30
+ if (released) {
31
+ return;
32
+ }
33
+ released = true;
34
+ continuous--;
35
+ };
36
+ }
37
+
1
38
  // src/theme.ts
2
- var Theme = {
39
+ var defaults = {
3
40
  FontFamily: "Courier New",
4
41
  Graph: {
5
- BackgroundColor: "#07212a"
42
+ BackgroundColor: "#07212a",
43
+ DotColor: "rgb(41, 54, 57)"
6
44
  },
7
45
  Node: {
8
46
  FontColor: "#afb9bb",
@@ -19,6 +57,7 @@ var Theme = {
19
57
  Grabbed: "white",
20
58
  Selected: "#6e6e6e"
21
59
  },
60
+ Bevel: null,
22
61
  Port: {
23
62
  FontColor: "#afb9bb"
24
63
  },
@@ -36,15 +75,25 @@ var Theme = {
36
75
  },
37
76
  Widget: {
38
77
  FontColor: "#afb9bb",
78
+ LabelColor: "#7f9497",
79
+ FontSize: 14,
80
+ Padding: 6,
81
+ Gap: 4,
39
82
  BackgroundColor: "#0f313c",
40
83
  Border: {
41
84
  Color: "#154050",
42
85
  Size: 2,
43
86
  Radius: 2
44
87
  },
88
+ Bevel: null,
45
89
  Hover: {
46
90
  BackgroundColor: "#195366"
47
91
  },
92
+ Input: {
93
+ BackgroundColor: "#0f313c",
94
+ HoverBackgroundColor: "#195366",
95
+ FontColor: "#afb9bb"
96
+ },
48
97
  Slider: {
49
98
  FillColor: "#07212A"
50
99
  },
@@ -57,8 +106,10 @@ var Theme = {
57
106
  ContextMenu: {
58
107
  BackgroundColor: "#07212A",
59
108
  HighlightColor: "#205A6D",
109
+ HighlightFontColor: "#afb9bb",
60
110
  FontColor: "#afb9bb",
61
- DisabledFontColor: "#4a585c"
111
+ DisabledFontColor: "#4a585c",
112
+ BorderRadius: 5
62
113
  },
63
114
  Minimap: {
64
115
  BackgroundColor: "rgba(10, 16, 18, 0.85)",
@@ -68,6 +119,13 @@ var Theme = {
68
119
  NodeColor: "#6b7f85",
69
120
  ViewportColor: "rgba(175, 185, 187, 0.25)"
70
121
  },
122
+ Popup: {
123
+ BackgroundColor: "#000000",
124
+ FontColor: "#ffffff",
125
+ FontFamily: "Verdana",
126
+ BorderRadius: 8,
127
+ Backdrop: "#00000070"
128
+ },
71
129
  Note: {
72
130
  FontColor: "#afb9bb",
73
131
  FontSize: 16,
@@ -86,11 +144,47 @@ var Theme = {
86
144
  },
87
145
  CodeBlock: {
88
146
  BackgroundColor: "#0c2b35",
147
+ FontColor: "#afb9bb",
89
148
  Padding: 16,
90
149
  BorderRadius: 4
91
150
  }
92
151
  }
93
152
  };
153
+ function clone(value) {
154
+ return JSON.parse(JSON.stringify(value));
155
+ }
156
+ function merge(target, source) {
157
+ if (source === void 0) {
158
+ return target;
159
+ }
160
+ if (source === null || typeof source !== "object" || target === null || typeof target !== "object") {
161
+ return source;
162
+ }
163
+ for (const key of Object.keys(source)) {
164
+ target[key] = merge(target[key], source[key]);
165
+ }
166
+ return target;
167
+ }
168
+ var DefaultTheme = Object.freeze(clone(defaults));
169
+ var Theme = clone(defaults);
170
+ var version = 0;
171
+ var listeners = /* @__PURE__ */ new Set();
172
+ function setTheme(config) {
173
+ const next = merge(clone(defaults), config);
174
+ for (const key of Object.keys(next)) {
175
+ Theme[key] = next[key];
176
+ }
177
+ version++;
178
+ listeners.forEach((listener) => listener());
179
+ requestRender();
180
+ }
181
+ function themeVersion() {
182
+ return version;
183
+ }
184
+ function onThemeChange(listener) {
185
+ listeners.add(listener);
186
+ return () => listeners.delete(listener);
187
+ }
94
188
 
95
189
  // src/styles/text.ts
96
190
  var DefaultSize = 16;
@@ -113,19 +207,19 @@ var TextStyle = class {
113
207
  constructor(config) {
114
208
  this.#size = config?.size === void 0 ? DefaultSize : config.size;
115
209
  this.#color = config?.color === void 0 ? DefaultColor : config.color;
116
- this.#font = config?.font === void 0 ? Theme.FontFamily : config.font;
210
+ this.#font = config?.font;
117
211
  this.#weight = config?.weight === void 0 ? "" /* Normal */ : config.weight;
118
212
  this.#fontStyle = config?.style === void 0 ? "" /* Normal */ : config.style;
119
213
  }
120
214
  setupStyle(ctx, scale) {
121
215
  ctx.fillStyle = this.#color;
122
- ctx.font = this.#fontStyle + " " + this.#weight + " " + this.#size * scale + "px " + this.#font;
216
+ ctx.font = this.#fontStyle + " " + this.#weight + " " + this.#size * scale + "px " + this.getFont();
123
217
  }
124
218
  getSize() {
125
219
  return this.#size;
126
220
  }
127
221
  getFont() {
128
- return this.#font;
222
+ return this.#font ?? Theme.FontFamily;
129
223
  }
130
224
  getWeight() {
131
225
  return this.#weight;
@@ -496,7 +590,18 @@ var ContextMenu = class _ContextMenu {
496
590
  }
497
591
  #tempBox = { Position: Zero(), Size: Zero() };
498
592
  #openSubMenu;
499
- render(ctx, pppp, graphScale, mousePosition, openRight) {
593
+ #bounds = null;
594
+ #contains(point) {
595
+ if (this.#bounds !== null && InBox(this.#bounds, point)) {
596
+ return true;
597
+ }
598
+ return this.#openSubMenu !== void 0 && this.#openSubMenu.#contains(point);
599
+ }
600
+ /**
601
+ * A submenu is anchored to its parent's left edge and told the parent's
602
+ * width, so it can open on whichever side has room.
603
+ */
604
+ render(ctx, anchor, graphScale, mousePosition, openRight, parentWidth = 0) {
500
605
  const menuScale = 1.25;
501
606
  const scaledEntryHeight = menuScale * contextEntryHeight;
502
607
  const scaledEntryWidth = menuScale * 40 + this.#getMaxWidthForText(ctx, menuScale);
@@ -505,19 +610,35 @@ var ContextMenu = class _ContextMenu {
505
610
  totalScaledHeight += this.#groups.At(i).height();
506
611
  }
507
612
  totalScaledHeight *= menuScale;
508
- const position = { x: 0, y: 0 };
509
- CopyVector2(position, pppp);
510
- if (!openRight) {
511
- position.x -= scaledEntryWidth;
613
+ const canvasWidth = ctx.canvas.clientWidth;
614
+ const position = { x: anchor.x, y: anchor.y };
615
+ let opensRight = openRight;
616
+ if (parentWidth === 0) {
617
+ if (!openRight) {
618
+ position.x -= scaledEntryWidth;
619
+ }
620
+ if (openRight && position.x + scaledEntryWidth > canvasWidth) {
621
+ position.x = canvasWidth - scaledEntryWidth;
622
+ opensRight = false;
623
+ }
624
+ } else {
625
+ const rightOfParent = anchor.x + parentWidth;
626
+ const leftOfParent = anchor.x - scaledEntryWidth;
627
+ const fitsRight = rightOfParent + scaledEntryWidth <= canvasWidth;
628
+ const fitsLeft = leftOfParent >= 0;
629
+ opensRight = openRight ? fitsRight || !fitsLeft : !fitsLeft && fitsRight;
630
+ position.x = opensRight ? rightOfParent : leftOfParent;
631
+ position.x = Math.max(0, Math.min(position.x, canvasWidth - scaledEntryWidth));
512
632
  }
513
633
  if (position.y + totalScaledHeight > ctx.canvas.clientHeight) {
514
634
  position.y = ctx.canvas.clientHeight - totalScaledHeight;
515
635
  }
516
- let submenuOpenRight = openRight;
517
- if (openRight && position.x + scaledEntryWidth > ctx.canvas.clientWidth) {
518
- position.x = ctx.canvas.clientWidth - scaledEntryWidth;
519
- submenuOpenRight = !submenuOpenRight;
520
- }
636
+ position.y = Math.max(0, position.y);
637
+ this.#bounds = {
638
+ Position: { x: position.x, y: position.y },
639
+ Size: { x: scaledEntryWidth, y: totalScaledHeight }
640
+ };
641
+ const pointer = mousePosition !== void 0 && this.#openSubMenu !== void 0 && this.#openSubMenu.#contains(mousePosition) ? void 0 : mousePosition;
521
642
  this.#tempBox.Size.x = scaledEntryWidth;
522
643
  this.#tempBox.Size.y = scaledEntryHeight;
523
644
  CopyVector2(this.#tempBox.Position, position);
@@ -531,36 +652,36 @@ var ContextMenu = class _ContextMenu {
531
652
  this.#tempBox.Position.y,
532
653
  scaledEntryWidth,
533
654
  totalScaledHeight,
534
- 5 * menuScale
655
+ Theme.ContextMenu.BorderRadius * menuScale
535
656
  );
536
657
  ctx.fill();
537
658
  ctx.shadowBlur = 0;
538
659
  let mouseIsOver = null;
539
- let subOpenedThisFrame = false;
660
+ if (pointer !== void 0) {
661
+ let row = 0;
662
+ for (let groupIndex = 0; groupIndex < this.#groups.Count(); groupIndex++) {
663
+ for (const entry of this.#groups.At(groupIndex).entries) {
664
+ this.#tempBox.Position.y = position.y + scaledEntryHeight * row;
665
+ if (InBox(this.#tempBox, pointer)) {
666
+ mouseIsOver = entry;
667
+ this.#openSubMenu = entry.subMenu;
668
+ this.#submenuPosition.x = position.x;
669
+ this.#submenuPosition.y = this.#tempBox.Position.y;
670
+ }
671
+ row++;
672
+ }
673
+ }
674
+ }
540
675
  let optionsRendered = 0;
541
676
  for (let groupIndex = 0; groupIndex < this.#groups.Count(); groupIndex++) {
542
677
  const group = this.#groups.At(groupIndex);
543
678
  for (let entryIndex = 0; entryIndex < group.entries.length; entryIndex++) {
544
679
  const entry = group.entries[entryIndex];
545
680
  this.#tempBox.Position.y = position.y + scaledEntryHeight * optionsRendered;
546
- let entryMousedOver = false;
547
- if (mousePosition !== void 0 && InBox(this.#tempBox, mousePosition)) {
548
- mouseIsOver = entry;
549
- entryMousedOver = true;
550
- if (entry.subMenu !== void 0) {
551
- this.#openSubMenu = entry.subMenu;
552
- this.#submenuPosition.x = position.x;
553
- this.#submenuPosition.y = this.#tempBox.Position.y;
554
- if (submenuOpenRight) {
555
- this.#submenuPosition.x += scaledEntryWidth;
556
- }
557
- subOpenedThisFrame = true;
558
- } else {
559
- this.#openSubMenu = void 0;
560
- }
561
- }
681
+ const entryMousedOver = entry === mouseIsOver;
562
682
  const itemDisabled = entry.item !== void 0 && entry.item.state() === 1 /* Disabled */;
563
- if (!itemDisabled && (entryMousedOver || this.#openSubMenu !== void 0 && entry.subMenu === this.#openSubMenu)) {
683
+ const highlighted = !itemDisabled && (entryMousedOver || this.#openSubMenu !== void 0 && entry.subMenu === this.#openSubMenu);
684
+ if (highlighted) {
564
685
  ctx.fillStyle = Theme.ContextMenu.HighlightColor;
565
686
  ctx.beginPath();
566
687
  ctx.roundRect(
@@ -568,18 +689,20 @@ var ContextMenu = class _ContextMenu {
568
689
  this.#tempBox.Position.y + scaledEntryHeight / 10,
569
690
  scaledEntryWidth - scaledEntryHeight / 5,
570
691
  scaledEntryHeight - scaledEntryHeight / 5,
571
- 5 * menuScale
692
+ Theme.ContextMenu.BorderRadius * menuScale
572
693
  );
573
694
  ctx.fill();
574
695
  }
575
696
  this.#textStyle.setupStyle(ctx, menuScale);
576
697
  if (itemDisabled) {
577
698
  ctx.fillStyle = Theme.ContextMenu.DisabledFontColor;
699
+ } else if (highlighted) {
700
+ ctx.fillStyle = Theme.ContextMenu.HighlightFontColor;
578
701
  }
579
702
  ctx.fillText(entry.text, position.x + scaledEntryHeight / 5, this.#tempBox.Position.y + scaledEntryHeight / 2);
580
703
  if (entry.subMenu !== void 0) {
581
704
  ctx.beginPath();
582
- ctx.strokeStyle = Theme.ContextMenu.FontColor;
705
+ ctx.strokeStyle = highlighted ? Theme.ContextMenu.HighlightFontColor : Theme.ContextMenu.FontColor;
583
706
  ctx.lineWidth = 1 * menuScale;
584
707
  ctx.lineTo(position.x + scaledEntryWidth - scaledEntryHeight / 2.5, this.#tempBox.Position.y + scaledEntryHeight / 3);
585
708
  ctx.lineTo(position.x + scaledEntryWidth - scaledEntryHeight / 4, this.#tempBox.Position.y + scaledEntryHeight / 2);
@@ -600,7 +723,7 @@ var ContextMenu = class _ContextMenu {
600
723
  }
601
724
  }
602
725
  if (this.#openSubMenu !== void 0) {
603
- const mouseOverSub = this.#openSubMenu.render(ctx, this.#submenuPosition, menuScale, mousePosition, submenuOpenRight);
726
+ const mouseOverSub = this.#openSubMenu.render(ctx, this.#submenuPosition, menuScale, mousePosition, opensRight, scaledEntryWidth);
604
727
  if (mouseOverSub !== null) {
605
728
  mouseIsOver = mouseOverSub;
606
729
  }
@@ -609,43 +732,6 @@ var ContextMenu = class _ContextMenu {
609
732
  }
610
733
  };
611
734
 
612
- // src/render_scheduler.ts
613
- var subscribers = /* @__PURE__ */ new Set();
614
- var frameQueued = false;
615
- var continuous = 0;
616
- var canSchedule = () => typeof requestAnimationFrame === "function";
617
- function onRenderRequested(redraw) {
618
- subscribers.add(redraw);
619
- return () => {
620
- subscribers.delete(redraw);
621
- };
622
- }
623
- function requestRender() {
624
- if (frameQueued || !canSchedule()) {
625
- return;
626
- }
627
- frameQueued = true;
628
- requestAnimationFrame(() => {
629
- frameQueued = false;
630
- subscribers.forEach((redraw) => redraw());
631
- if (continuous > 0) {
632
- requestRender();
633
- }
634
- });
635
- }
636
- function renderContinuously() {
637
- continuous++;
638
- requestRender();
639
- let released = false;
640
- return () => {
641
- if (released) {
642
- return;
643
- }
644
- released = true;
645
- continuous--;
646
- };
647
- }
648
-
649
735
  // src/input.ts
650
736
  var LONG_PRESS_MS = 500;
651
737
  var LONG_PRESS_SLOP = 12;
@@ -1230,6 +1316,7 @@ function splitStringIntoLines(ctx, str, maxWidth) {
1230
1316
  // src/types/text.ts
1231
1317
  var Text = class _Text {
1232
1318
  #measured;
1319
+ #measuredForTheme = -1;
1233
1320
  #size;
1234
1321
  #style;
1235
1322
  #value;
@@ -1309,9 +1396,10 @@ var Text = class _Text {
1309
1396
  return results;
1310
1397
  }
1311
1398
  #measure(ctx) {
1312
- if (this.#measured) {
1399
+ if (this.#measured && this.#measuredForTheme === themeVersion()) {
1313
1400
  return;
1314
1401
  }
1402
+ this.#measuredForTheme = themeVersion();
1315
1403
  this.#style.setupStyle(ctx, 1);
1316
1404
  if (this.#maxWidth == -1) {
1317
1405
  const measurements = ctx.measureText(this.#value);
@@ -1387,7 +1475,7 @@ function RgbToHex(c) {
1387
1475
  return "#" + componentToHex(Math.round(c.r * 255)) + componentToHex(Math.round(c.g * 255)) + componentToHex(Math.round(c.b * 255));
1388
1476
  }
1389
1477
  function HexToColor(hex) {
1390
- var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
1478
+ var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})(?:[a-f\d]{2})?$/i.exec(hex);
1391
1479
  return result ? {
1392
1480
  r: parseInt(result[1], 16) / 255,
1393
1481
  g: parseInt(result[2], 16) / 255,
@@ -1477,6 +1565,21 @@ function portConfigTypes(config) {
1477
1565
  }
1478
1566
  return [config.type === void 0 ? "" : config.type];
1479
1567
  }
1568
+ var ARRAY_SLOT_PITCH = 12;
1569
+ function arrayInsertIndex(box, count, y) {
1570
+ if (count === 0) {
1571
+ return 0;
1572
+ }
1573
+ const index = Math.round((y - box.Position.y) / (box.Size.y / count));
1574
+ return Math.min(count, Math.max(0, index));
1575
+ }
1576
+ function arraySlotCenter(box, count, index) {
1577
+ const slots = Math.max(1, count);
1578
+ return {
1579
+ x: box.Position.x + box.Size.x / 2,
1580
+ y: box.Position.y + (index + 0.5) * box.Size.y / slots
1581
+ };
1582
+ }
1480
1583
  function fallbackColor(type, s) {
1481
1584
  let value = 0;
1482
1585
  for (var i = 0; i < type.length; i++) {
@@ -1501,6 +1604,7 @@ var Port = class {
1501
1604
  #acceptedTypes;
1502
1605
  #description;
1503
1606
  #array;
1607
+ #hidden;
1504
1608
  #configuredEmptyFill;
1505
1609
  #configuredFilledFill;
1506
1610
  #dataTypePopupElement;
@@ -1516,6 +1620,7 @@ var Port = class {
1516
1620
  this.#acceptedTypes = config?.acceptedTypes === void 0 ? [] : config.acceptedTypes;
1517
1621
  this.#description = config?.description === void 0 ? "" : config.description;
1518
1622
  this.#array = config?.array === true;
1623
+ this.#hidden = config?.hidden === true;
1519
1624
  this.#configuredEmptyFill = config?.emptyStyle?.fillColor;
1520
1625
  this.#configuredFilledFill = config?.filledStyle?.fillColor;
1521
1626
  this.#emptyStyle = {
@@ -1614,9 +1719,11 @@ var Port = class {
1614
1719
  this.setDataType(type);
1615
1720
  this.#refreshDataType();
1616
1721
  }
1617
- addConnection(connection) {
1618
- const c = this.#connections.length;
1619
- this.#connections.push(connection);
1722
+ addConnection(connection, index) {
1723
+ const c = index === void 0 ? this.#connections.length : Math.min(Math.max(index, 0), this.#connections.length);
1724
+ const was = this.hidden();
1725
+ this.#connections.splice(c, 0, connection);
1726
+ this.#visibilityMayHaveChanged(was);
1620
1727
  for (let i = 0; i < this.#onConnectionAdded.length; i++) {
1621
1728
  this.#onConnectionAdded[i](connection, c, this, this.#portType, this.#node);
1622
1729
  }
@@ -1637,6 +1744,32 @@ var Port = class {
1637
1744
  connections() {
1638
1745
  return this.#connections;
1639
1746
  }
1747
+ /**
1748
+ * How many wires the port has room for: one per connection on an array
1749
+ * input, so each can be told apart and picked up on its own.
1750
+ */
1751
+ slots() {
1752
+ if (this.#portType !== "INPUTARRAY" /* InputArray */) {
1753
+ return 1;
1754
+ }
1755
+ return Math.max(1, this.#connections.length);
1756
+ }
1757
+ setHidden(hidden) {
1758
+ const was = this.hidden();
1759
+ this.#hidden = hidden;
1760
+ this.#visibilityMayHaveChanged(was);
1761
+ }
1762
+ #visibilityMayHaveChanged(was) {
1763
+ if (was !== this.hidden() || this.#portType === "INPUTARRAY" /* InputArray */) {
1764
+ this.#node?.invalidateLayout();
1765
+ }
1766
+ }
1767
+ /**
1768
+ * A port asked to hide still shows while anything is connected to it.
1769
+ */
1770
+ hidden() {
1771
+ return this.#hidden && this.#connections.length === 0;
1772
+ }
1640
1773
  addConnectionRemovedListener(callback) {
1641
1774
  if (callback === void 0) {
1642
1775
  return;
@@ -1646,7 +1779,9 @@ var Port = class {
1646
1779
  clearConnection(connection) {
1647
1780
  const index = this.#connections.indexOf(connection);
1648
1781
  if (index > -1) {
1782
+ const was = this.hidden();
1649
1783
  this.#connections.splice(index, 1);
1784
+ this.#visibilityMayHaveChanged(was);
1650
1785
  for (let i = 0; i < this.#onConnectionRemoved.length; i++) {
1651
1786
  this.#onConnectionRemoved[i](connection, index, this, this.#portType, this.#node);
1652
1787
  }
@@ -1705,20 +1840,31 @@ var Port = class {
1705
1840
  this.#dataTypePopupElement.render(ctx, { x: xPos - size.x / 2, y: yPos }, 1, size);
1706
1841
  });
1707
1842
  }
1843
+ const slots = this.slots();
1844
+ const height2 = slots === 1 ? scaledRadius * 2 : slots * ARRAY_SLOT_PITCH * camera.zoom;
1708
1845
  this.#box.Position.x = position.x - scaledRadius;
1709
- this.#box.Position.y = position.y - scaledRadius;
1846
+ this.#box.Position.y = position.y - height2 / 2;
1710
1847
  this.#box.Size.x = scaledRadius * 2;
1711
- this.#box.Size.y = scaledRadius * 2;
1848
+ this.#box.Size.y = height2;
1712
1849
  ctx.strokeStyle = style.borderColor;
1713
1850
  ctx.fillStyle = style.fillColor;
1714
1851
  ctx.beginPath();
1715
1852
  if (this.#portType === "INPUTARRAY" /* InputArray */) {
1716
- ctx.rect(position.x - scaledRadius, position.y - scaledRadius, scaledRadius * 2, scaledRadius * 2);
1853
+ ctx.roundRect(this.#box.Position.x, this.#box.Position.y, this.#box.Size.x, this.#box.Size.y, slots === 1 ? 0 : scaledRadius);
1717
1854
  } else {
1718
1855
  ctx.arc(position.x, position.y, scaledRadius, 0, 2 * Math.PI);
1719
1856
  }
1720
1857
  ctx.fill();
1721
1858
  ctx.stroke();
1859
+ if (slots > 1) {
1860
+ ctx.fillStyle = style.borderColor;
1861
+ for (let i = 0; i < slots; i++) {
1862
+ const slot = arraySlotCenter(this.#box, slots, i);
1863
+ ctx.beginPath();
1864
+ ctx.arc(slot.x, slot.y, scaledRadius * 0.4, 0, 2 * Math.PI);
1865
+ ctx.fill();
1866
+ }
1867
+ }
1722
1868
  return this.#box;
1723
1869
  }
1724
1870
  };
@@ -1834,331 +1980,480 @@ var BoxStyle = class {
1834
1980
  }
1835
1981
  };
1836
1982
 
1837
- // src/popup.ts
1838
- var Popup = class {
1839
- #title;
1840
- #options;
1841
- #content;
1842
- #onClose;
1843
- // Styling ================================================================
1844
- #buttonCSS;
1845
- // Runtime ================================================================
1846
- #popup;
1847
- #keyHandler;
1848
- constructor(config) {
1849
- this.#title = config.title;
1850
- this.#options = config.options;
1851
- this.#content = config.content;
1852
- this.#onClose = config.onClose;
1853
- this.#buttonCSS = config.buttonCSS === void 0 ? "flex-grow: 1; margin-right: 8px;" : config.buttonCSS;
1854
- this.#popup = null;
1855
- this.#keyHandler = null;
1856
- }
1857
- Hide() {
1858
- if (this.#popup === null) {
1859
- return;
1860
- }
1861
- if (this.#keyHandler !== null) {
1862
- document.removeEventListener("keydown", this.#keyHandler, true);
1863
- this.#keyHandler = null;
1864
- }
1865
- document.body.removeChild(this.#popup);
1866
- this.#popup = null;
1983
+ // src/styles/cursor.ts
1984
+ var CursorStyle = /* @__PURE__ */ ((CursorStyle2) => {
1985
+ CursorStyle2["Default"] = "default";
1986
+ CursorStyle2["Pointer"] = "pointer";
1987
+ CursorStyle2["Grab"] = "grab";
1988
+ CursorStyle2["Grabbing"] = "grabbing";
1989
+ CursorStyle2["Wait"] = "wait";
1990
+ CursorStyle2["Help"] = "help";
1991
+ CursorStyle2["Crosshair"] = "crosshair";
1992
+ CursorStyle2["Cell"] = "cell";
1993
+ CursorStyle2["Text"] = "text";
1994
+ CursorStyle2["Move"] = "move";
1995
+ CursorStyle2["NotAllowed"] = "not-allowed";
1996
+ CursorStyle2["ZoomIn"] = "zoom-in";
1997
+ CursorStyle2["ZoomOut"] = "zoom-out";
1998
+ CursorStyle2["None"] = "none";
1999
+ CursorStyle2["ContextMenu"] = "context-menu";
2000
+ CursorStyle2["EWResize"] = "ew-resize";
2001
+ return CursorStyle2;
2002
+ })(CursorStyle || {});
2003
+
2004
+ // src/styles/bevel.ts
2005
+ function drawBevel(ctx, box, scale, colors, sunken) {
2006
+ const width2 = Math.max(1, 2 * scale);
2007
+ const half = width2 / 2;
2008
+ const left = box.Position.x + half;
2009
+ const top = box.Position.y + half;
2010
+ const right = box.Position.x + box.Size.x - half;
2011
+ const bottom = box.Position.y + box.Size.y - half;
2012
+ ctx.save();
2013
+ ctx.lineWidth = width2;
2014
+ ctx.lineCap = "square";
2015
+ ctx.strokeStyle = sunken ? colors.Dark : colors.Light;
2016
+ ctx.beginPath();
2017
+ ctx.moveTo(left, bottom);
2018
+ ctx.lineTo(left, top);
2019
+ ctx.lineTo(right, top);
2020
+ ctx.stroke();
2021
+ ctx.strokeStyle = sunken ? colors.Light : colors.Dark;
2022
+ ctx.beginPath();
2023
+ ctx.moveTo(right, top);
2024
+ ctx.lineTo(right, bottom);
2025
+ ctx.lineTo(left, bottom);
2026
+ ctx.stroke();
2027
+ ctx.restore();
2028
+ }
2029
+
2030
+ // src/widgets/field.ts
2031
+ function widgetFont(scale) {
2032
+ return `${Theme.Widget.FontSize * scale}px ${Theme.FontFamily}`;
2033
+ }
2034
+ function fieldBackground(look) {
2035
+ if (look.background !== void 0) {
2036
+ return look.background;
1867
2037
  }
1868
- #close(option) {
1869
- this.Hide();
1870
- if (this.#onClose !== void 0) {
1871
- this.#onClose(option);
1872
- }
2038
+ if (look.input) {
2039
+ return look.hovered ? Theme.Widget.Input.HoverBackgroundColor : Theme.Widget.Input.BackgroundColor;
1873
2040
  }
1874
- Show() {
1875
- this.#popup = document.createElement("div");
1876
- this.#popup.style.cssText = "position:absolute;top:0;width:100%;display:flex;height:100%;justify-content:center;align-items:center;background-color:#00000070;";
1877
- document.body.appendChild(this.#popup);
1878
- const container = document.createElement("div");
1879
- container.style.cssText = "background-color:#000;color:white;font-family:Verdana;border-radius:8px;padding:16px;";
1880
- this.#popup.appendChild(container);
1881
- const title = document.createElement("h2");
1882
- title.textContent = this.#title;
1883
- title.style.cssText = "margin-top:0;margin-bottom:16px;";
1884
- container.appendChild(title);
1885
- if (this.#content !== void 0) {
1886
- container.appendChild(this.#content());
1887
- }
1888
- const buttonContainer = document.createElement("div");
1889
- buttonContainer.style.cssText = "margin-top:16px;justify-content:space-around;align-items:center;flex-direction:row;display:flex;";
1890
- container.appendChild(buttonContainer);
1891
- for (let i = 0; i < this.#options.length; i++) {
1892
- const button = document.createElement("button");
1893
- button.style.cssText = this.#buttonCSS;
1894
- button.textContent = this.#options[i];
1895
- button.onclick = () => this.#close(this.#options[i]);
1896
- buttonContainer.appendChild(button);
1897
- }
1898
- this.#keyHandler = (e) => {
1899
- if (e.key === "Enter") {
1900
- e.preventDefault();
1901
- this.#close(this.#options.length > 0 ? this.#options[0] : null);
1902
- } else if (e.key === "Escape") {
1903
- e.preventDefault();
1904
- this.#close(null);
1905
- }
1906
- e.stopPropagation();
1907
- };
1908
- document.addEventListener("keydown", this.#keyHandler, true);
2041
+ if (look.pressed) {
2042
+ return Theme.Widget.Button.Click.BackgroundColor;
1909
2043
  }
1910
- };
1911
-
1912
- // src/styles/textBox.ts
1913
- function TextBoxStyleWithFallback(input, fallback) {
1914
- return {
1915
- box: BoxStyleWithFallback(input?.box, fallback?.box),
1916
- text: TextStyleFallback(input?.text, fallback?.text),
1917
- textAlign: input?.textAlign === void 0 ? fallback?.textAlign : input?.textAlign,
1918
- textBaseline: input?.textBaseline === void 0 ? fallback?.textBaseline : input?.textBaseline
1919
- };
2044
+ return look.hovered ? Theme.Widget.Hover.BackgroundColor : Theme.Widget.BackgroundColor;
1920
2045
  }
1921
- var TextBoxStyle = class {
1922
- #box;
1923
- #text;
1924
- #textAlign;
1925
- #textBaseline;
1926
- constructor(config) {
1927
- this.#box = new BoxStyle(config?.box);
1928
- this.#text = new TextStyle(config?.text);
1929
- this.#textAlign = config?.textAlign === void 0 ? "center" /* Center */ : config.textAlign;
1930
- this.#textBaseline = config?.textBaseline === void 0 ? "middle" /* Middle */ : config.textBaseline;
1931
- }
1932
- Draw(ctx, box, scale, text) {
1933
- this.#box.Draw(ctx, box, scale);
1934
- ctx.textAlign = this.#textAlign;
1935
- ctx.textBaseline = this.#textBaseline;
1936
- this.#text.setupStyle(ctx, scale);
1937
- ctx.fillText(
1938
- text,
1939
- box.Position.x + box.Size.x / 2,
1940
- box.Position.y + box.Size.y / 2
1941
- );
2046
+ function drawFieldBox(ctx, box, scale, look) {
2047
+ ctx.fillStyle = fieldBackground(look);
2048
+ ctx.beginPath();
2049
+ ctx.roundRect(box.Position.x, box.Position.y, box.Size.x, box.Size.y, Theme.Widget.Border.Radius * scale);
2050
+ ctx.fill();
2051
+ if (Theme.Widget.Border.Size > 0) {
2052
+ ctx.strokeStyle = look.border ?? Theme.Widget.Border.Color;
2053
+ ctx.lineWidth = Theme.Widget.Border.Size * scale;
2054
+ ctx.stroke();
1942
2055
  }
1943
- DrawUnderline(ctx, box, scale, text) {
1944
- this.#box.DrawUnderline(ctx, box, scale);
1945
- ctx.textAlign = this.#textAlign;
1946
- ctx.textBaseline = this.#textBaseline;
1947
- this.#text.setupStyle(ctx, scale);
2056
+ if (Theme.Widget.Bevel !== null) {
2057
+ drawBevel(ctx, box, scale, Theme.Widget.Bevel, look.input === true || look.pressed === true);
2058
+ }
2059
+ }
2060
+ function drawField(ctx, box, scale, look) {
2061
+ drawFieldBox(ctx, box, scale, look);
2062
+ drawFieldText(ctx, box, scale, look);
2063
+ }
2064
+ function drawFieldText(ctx, box, scale, look) {
2065
+ const padding = Theme.Widget.Padding * scale;
2066
+ const middle = box.Position.y + box.Size.y / 2;
2067
+ const textColor = look.textColor ?? (look.input ? Theme.Widget.Input.FontColor : Theme.Widget.FontColor);
2068
+ ctx.font = widgetFont(scale);
2069
+ ctx.textBaseline = "middle";
2070
+ if (look.label === void 0 || look.label === "") {
2071
+ ctx.textAlign = "center";
2072
+ ctx.fillStyle = textColor;
1948
2073
  ctx.fillText(
1949
- text,
2074
+ fitString(ctx, look.value, box.Size.x - padding * 2),
1950
2075
  box.Position.x + box.Size.x / 2,
1951
- box.Position.y + box.Size.y / 2
2076
+ middle
1952
2077
  );
2078
+ return;
1953
2079
  }
1954
- setTextColor(color) {
1955
- this.#text.setColor(color);
1956
- }
1957
- setBoxColor(color) {
1958
- this.#box.setColor(color);
1959
- }
1960
- setBorderColor(color) {
1961
- this.#box.setBorderColor(color);
2080
+ const room = box.Size.x - padding * 2;
2081
+ const label = fitString(ctx, look.label, room / 2);
2082
+ const labelWidth = ctx.measureText(label).width;
2083
+ ctx.textAlign = "left";
2084
+ ctx.fillStyle = look.textColor ?? Theme.Widget.LabelColor;
2085
+ ctx.fillText(label, box.Position.x + padding, middle);
2086
+ ctx.textAlign = "right";
2087
+ ctx.fillStyle = textColor;
2088
+ ctx.fillText(
2089
+ fitString(ctx, look.value, room - labelWidth - padding),
2090
+ box.Position.x + box.Size.x - padding,
2091
+ middle
2092
+ );
2093
+ }
2094
+
2095
+ // src/widgets/inplace.ts
2096
+ function editInPlace(config) {
2097
+ const canvas = config.canvas;
2098
+ if (canvas === null) {
2099
+ return null;
1962
2100
  }
1963
- };
2101
+ const rect = canvas.getBoundingClientRect();
2102
+ const sx = canvas.width > 0 ? rect.width / canvas.width : 1;
2103
+ const sy = canvas.height > 0 ? rect.height / canvas.height : 1;
2104
+ const input = document.createElement("input");
2105
+ input.type = "text";
2106
+ input.value = config.value;
2107
+ input.style.cssText = [
2108
+ "position: fixed",
2109
+ `left: ${rect.left + config.box.Position.x * sx}px`,
2110
+ `top: ${rect.top + config.box.Position.y * sy}px`,
2111
+ `width: ${config.box.Size.x * sx}px`,
2112
+ `height: ${config.box.Size.y * sy}px`,
2113
+ "box-sizing: border-box",
2114
+ "margin: 0",
2115
+ `padding: 0 ${Theme.Widget.Padding * config.scale}px`,
2116
+ `font: ${Theme.Widget.FontSize * config.scale * sy}px ${Theme.FontFamily}`,
2117
+ `color: ${Theme.Widget.FontColor}`,
2118
+ `background: ${Theme.Widget.Hover.BackgroundColor}`,
2119
+ `border: ${Theme.Widget.Border.Size * config.scale}px solid ${Theme.Widget.Border.Color}`,
2120
+ `border-radius: ${Theme.Widget.Border.Radius * config.scale}px`,
2121
+ "outline: none",
2122
+ "z-index: 1000"
2123
+ ].join(";");
2124
+ let done = false;
2125
+ const finish = (keep) => {
2126
+ if (done) {
2127
+ return;
2128
+ }
2129
+ done = true;
2130
+ input.remove();
2131
+ if (keep) {
2132
+ config.onCommit(input.value);
2133
+ }
2134
+ requestRender();
2135
+ };
2136
+ input.addEventListener("keydown", (event) => {
2137
+ event.stopPropagation();
2138
+ if (event.key === "Enter") {
2139
+ event.preventDefault();
2140
+ finish(true);
2141
+ } else if (event.key === "Escape") {
2142
+ event.preventDefault();
2143
+ finish(false);
2144
+ }
2145
+ });
2146
+ input.addEventListener("blur", () => finish(true));
2147
+ document.body.appendChild(input);
2148
+ input.focus();
2149
+ input.select();
2150
+ return input;
2151
+ }
1964
2152
 
1965
2153
  // src/widgets/widget.ts
1966
2154
  var width = 150;
1967
2155
  var height = 25;
1968
- function startingWidgetValue(node, property, configured, fallback) {
1969
- if (configured !== void 0) {
1970
- return configured;
2156
+ var WidgetBase = class {
2157
+ #hidden = false;
2158
+ #disabled = false;
2159
+ #hovered = false;
2160
+ #pressed = false;
2161
+ #box = { Position: { x: 0, y: 0 }, Size: { x: 0, y: 0 } };
2162
+ #canvas = null;
2163
+ #scale = 1;
2164
+ Draw(ctx, box, scale, mousePosition) {
2165
+ this.#box = { Position: { x: box.Position.x, y: box.Position.y }, Size: { x: box.Size.x, y: box.Size.y } };
2166
+ this.#canvas = ctx.canvas ?? null;
2167
+ this.#scale = scale;
2168
+ this.#hovered = !this.#disabled && mousePosition !== void 0 && InBox(box, mousePosition);
2169
+ ctx.save();
2170
+ if (this.#disabled) {
2171
+ ctx.globalAlpha *= 0.4;
2172
+ }
2173
+ this.render(ctx, box, scale);
2174
+ ctx.restore();
2175
+ }
2176
+ PointerDown(pointer) {
2177
+ if (this.#disabled) {
2178
+ return;
2179
+ }
2180
+ this.#pressed = true;
2181
+ this.onPointerDown(pointer);
2182
+ requestRender();
1971
2183
  }
1972
- if (property !== void 0 && property !== null) {
1973
- const existing = node.getProperty(property);
1974
- if (existing !== void 0 && existing !== null) {
1975
- return existing;
2184
+ PointerMove(pointer) {
2185
+ if (!this.#pressed) {
2186
+ return;
1976
2187
  }
2188
+ this.onPointerMove(pointer);
2189
+ requestRender();
1977
2190
  }
1978
- return fallback;
1979
- }
1980
-
1981
- // src/widgets/number.ts
1982
- var NumberWidget = class {
2191
+ PointerUp(pointer) {
2192
+ if (!this.#pressed) {
2193
+ return;
2194
+ }
2195
+ this.#pressed = false;
2196
+ this.onPointerUp(pointer);
2197
+ if (pointer.inside) {
2198
+ this.onClick(pointer);
2199
+ }
2200
+ requestRender();
2201
+ }
2202
+ Cursor() {
2203
+ return "pointer" /* Pointer */;
2204
+ }
2205
+ Hidden() {
2206
+ return this.#hidden;
2207
+ }
2208
+ setHidden(hidden) {
2209
+ this.#hidden = hidden;
2210
+ requestRender();
2211
+ }
2212
+ Disabled() {
2213
+ return this.#disabled;
2214
+ }
2215
+ setDisabled(disabled) {
2216
+ this.#disabled = disabled;
2217
+ if (disabled) {
2218
+ this.#pressed = false;
2219
+ }
2220
+ requestRender();
2221
+ }
2222
+ hovered() {
2223
+ return this.#hovered;
2224
+ }
2225
+ pressed() {
2226
+ return this.#pressed;
2227
+ }
2228
+ /**
2229
+ * Where the widget was last drawn, in canvas pixels.
2230
+ */
2231
+ box() {
2232
+ return this.#box;
2233
+ }
2234
+ canvas() {
2235
+ return this.#canvas;
2236
+ }
2237
+ scale() {
2238
+ return this.#scale;
2239
+ }
2240
+ onPointerDown(pointer) {
2241
+ }
2242
+ onPointerMove(pointer) {
2243
+ }
2244
+ onPointerUp(pointer) {
2245
+ }
2246
+ /**
2247
+ * A press and release that both landed on the widget.
2248
+ */
2249
+ onClick(pointer) {
2250
+ }
2251
+ };
2252
+ function startingWidgetValue(node, property, configured, fallback) {
2253
+ if (configured !== void 0) {
2254
+ return configured;
2255
+ }
2256
+ if (property !== void 0 && property !== null) {
2257
+ const existing = node.getProperty(property);
2258
+ if (existing !== void 0 && existing !== null) {
2259
+ return existing;
2260
+ }
2261
+ }
2262
+ return fallback;
2263
+ }
2264
+
2265
+ // src/widgets/value.ts
2266
+ var ValueWidget = class extends WidgetBase {
1983
2267
  #node;
1984
- #nodeProperty;
2268
+ #property;
1985
2269
  #value;
1986
- #idleBoxStyle;
1987
- #highlightBoxStyle;
1988
- #text;
2270
+ #label;
2271
+ #width;
1989
2272
  #callback;
1990
- constructor(node, config) {
1991
- this.#value = 0;
1992
- this.#text = "0";
2273
+ #onInput;
2274
+ constructor(node, config, fallback) {
2275
+ super();
1993
2276
  this.#node = node;
1994
- this.#nodeProperty = config?.property;
1995
- this.#idleBoxStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.idleBoxStyle, {
1996
- box: {
1997
- color: Theme.Widget.BackgroundColor,
1998
- border: {
1999
- size: Theme.Widget.Border.Size,
2000
- color: Theme.Widget.Border.Color
2001
- },
2002
- radius: Theme.Widget.Border.Radius
2003
- },
2004
- text: { color: Theme.Widget.FontColor }
2005
- }));
2006
- this.#highlightBoxStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.highlightBoxStyle, {
2007
- box: {
2008
- color: Theme.Widget.Hover.BackgroundColor,
2009
- border: {
2010
- size: Theme.Widget.Border.Size,
2011
- color: Theme.Widget.Border.Color
2012
- },
2013
- radius: Theme.Widget.Border.Radius
2014
- },
2015
- text: { color: Theme.Widget.FontColor }
2016
- }));
2017
- this.Set(startingWidgetValue(node, this.#nodeProperty, config?.value, 0));
2277
+ this.#property = config?.property;
2278
+ this.#label = config?.label ?? "";
2279
+ this.#width = config?.width ?? width;
2280
+ this.#value = startingWidgetValue(node, this.#property, config?.value, fallback);
2018
2281
  this.#callback = config?.callback;
2019
- if (this.#nodeProperty !== void 0) {
2020
- this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
2021
- this.Set(newVal);
2022
- });
2282
+ this.#onInput = config?.onInput;
2283
+ if (this.#property !== void 0) {
2284
+ node.setProperty(this.#property, this.#value);
2285
+ node.addPropertyChangeListener(this.#property, (_, next) => this.setValue(next));
2023
2286
  }
2024
2287
  }
2025
2288
  Size() {
2026
- return { "x": width, "y": height };
2289
+ return { x: this.#width, y: height };
2290
+ }
2291
+ value() {
2292
+ return this.#value;
2293
+ }
2294
+ /**
2295
+ * Shows a new value without treating it as a person's edit: no callback
2296
+ * and no edit event.
2297
+ */
2298
+ setValue(value) {
2299
+ if (this.equal(value, this.#value)) {
2300
+ return;
2301
+ }
2302
+ this.#value = value;
2303
+ this.valueChanged();
2304
+ if (this.#property !== void 0) {
2305
+ this.#node.setProperty(this.#property, value);
2306
+ }
2307
+ requestRender();
2308
+ }
2309
+ label() {
2310
+ return this.#label;
2027
2311
  }
2028
- Set(newNumber) {
2029
- if (this.#value === newNumber) {
2312
+ setLabel(label) {
2313
+ this.#label = label;
2314
+ requestRender();
2315
+ }
2316
+ node() {
2317
+ return this.#node;
2318
+ }
2319
+ input(value) {
2320
+ this.setValue(value);
2321
+ this.#onInput?.(value);
2322
+ }
2323
+ /**
2324
+ * Ends a person's edit. Nothing is reported when they ended where they
2325
+ * started.
2326
+ */
2327
+ commit(value, startedAt) {
2328
+ this.setValue(value);
2329
+ if (this.equal(value, startedAt)) {
2030
2330
  return;
2031
2331
  }
2032
- this.#value = newNumber;
2033
- if (this.#nodeProperty !== void 0) {
2034
- this.#node.setProperty(this.#nodeProperty, this.#value);
2332
+ if (this.#property !== void 0) {
2333
+ this.#node.editProperty(this.#property, value);
2035
2334
  }
2036
- if (this.#callback !== void 0) {
2037
- this.#callback(this.#value);
2335
+ this.#callback?.(value);
2336
+ }
2337
+ valueChanged() {
2338
+ }
2339
+ equal(a, b) {
2340
+ return Object.is(a, b);
2341
+ }
2342
+ };
2343
+
2344
+ // src/widgets/number.ts
2345
+ var DRAG_THRESHOLD = 3;
2346
+ var NumberWidget = class extends ValueWidget {
2347
+ #step;
2348
+ #integer;
2349
+ #min;
2350
+ #max;
2351
+ #startX = 0;
2352
+ #startValue = 0;
2353
+ #dragging = false;
2354
+ #dragged = false;
2355
+ constructor(node, config) {
2356
+ super(node, config, 0);
2357
+ this.#step = config?.step;
2358
+ this.#integer = config?.integer === true;
2359
+ this.#min = config?.min ?? -Infinity;
2360
+ this.#max = config?.max ?? Infinity;
2361
+ }
2362
+ Cursor() {
2363
+ return "ew-resize" /* EWResize */;
2364
+ }
2365
+ render(ctx, box, scale) {
2366
+ drawField(ctx, box, scale, {
2367
+ input: true,
2368
+ hovered: this.hovered() || this.pressed(),
2369
+ label: this.label(),
2370
+ value: formatNumber(this.value())
2371
+ });
2372
+ }
2373
+ onPointerDown(pointer) {
2374
+ this.#startX = pointer.position.x;
2375
+ this.#startValue = this.value();
2376
+ this.#dragging = false;
2377
+ this.#dragged = false;
2378
+ }
2379
+ onPointerMove(pointer) {
2380
+ const dx = pointer.position.x - this.#startX;
2381
+ if (!this.#dragging && Math.abs(dx) < DRAG_THRESHOLD) {
2382
+ return;
2038
2383
  }
2039
- this.#text = "" + parseFloat(this.#value.toPrecision(6));
2384
+ this.#dragging = true;
2385
+ this.input(this.#clean(this.#startValue + dx * this.#stepFor(this.#startValue)));
2040
2386
  }
2041
- ClickStart() {
2387
+ onPointerUp() {
2388
+ this.#dragged = this.#dragging;
2389
+ if (this.#dragging) {
2390
+ this.#dragging = false;
2391
+ this.commit(this.value(), this.#startValue);
2392
+ }
2042
2393
  }
2043
- ClickEnd() {
2044
- let input = null;
2045
- const setOption = "Set";
2046
- const cancelOption = "Cancel";
2047
- const popup = new Popup({
2048
- title: "Set Number",
2049
- options: [setOption, cancelOption],
2050
- content: () => {
2051
- const container = document.createElement("div");
2052
- input = document.createElement("input");
2053
- input.type = "number";
2054
- input.valueAsNumber = this.#value;
2055
- container.append(input);
2056
- return container;
2057
- },
2058
- onClose: (button) => {
2059
- if (button !== setOption || input === null) {
2060
- return;
2394
+ onClick() {
2395
+ if (this.#dragged) {
2396
+ return;
2397
+ }
2398
+ const before = this.value();
2399
+ editInPlace({
2400
+ canvas: this.canvas(),
2401
+ box: this.box(),
2402
+ scale: this.scale(),
2403
+ value: String(before),
2404
+ onCommit: (text) => {
2405
+ const parsed = Number.parseFloat(text);
2406
+ if (Number.isFinite(parsed)) {
2407
+ this.commit(this.#clean(parsed), before);
2061
2408
  }
2062
- this.Set(input.valueAsNumber);
2063
2409
  }
2064
2410
  });
2065
- popup.Show();
2066
2411
  }
2067
- Draw(ctx, position, scale, mousePosition) {
2068
- const box = {
2069
- Position: { x: 0, y: 0 },
2070
- Size: {
2071
- x: width * scale,
2072
- y: height * scale
2073
- }
2074
- };
2075
- CopyVector2(box.Position, position);
2076
- let style = this.#idleBoxStyle;
2077
- if (mousePosition !== void 0) {
2078
- if (InBox(box, mousePosition)) {
2079
- style = this.#highlightBoxStyle;
2080
- }
2412
+ #stepFor(value) {
2413
+ if (this.#step !== void 0) {
2414
+ return this.#step;
2081
2415
  }
2082
- style.DrawUnderline(ctx, box, scale, this.#text);
2083
- return box;
2416
+ return Math.max(Math.abs(value) * 0.01, this.#integer ? 0.1 : 0.01);
2417
+ }
2418
+ #clean(value) {
2419
+ const rounded = this.#integer ? Math.round(value) : value;
2420
+ return Math.min(this.#max, Math.max(this.#min, rounded));
2084
2421
  }
2085
2422
  };
2423
+ function formatNumber(value) {
2424
+ return "" + parseFloat(value.toPrecision(6));
2425
+ }
2086
2426
 
2087
2427
  // src/widgets/button.ts
2088
- var ButtonWidget = class {
2428
+ var ButtonWidget = class extends WidgetBase {
2089
2429
  #text;
2090
- #idleStyle;
2091
- #hoverStyle;
2092
- #clickStyle;
2093
2430
  #callback;
2094
- #gettingClicked;
2431
+ #width;
2095
2432
  constructor(config) {
2096
- this.#text = config?.text === void 0 ? "Button" : config?.text;
2433
+ super();
2434
+ this.#text = config?.text ?? "Button";
2097
2435
  this.#callback = config?.callback;
2098
- this.#gettingClicked = false;
2099
- this.#idleStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.idleStyle, {
2100
- box: {
2101
- color: Theme.Widget.BackgroundColor,
2102
- border: {
2103
- size: Theme.Widget.Border.Size,
2104
- color: Theme.Widget.Border.Color
2105
- }
2106
- },
2107
- text: { color: Theme.Widget.FontColor }
2108
- }));
2109
- this.#hoverStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.hoverStyle, {
2110
- box: {
2111
- color: Theme.Widget.Hover.BackgroundColor,
2112
- border: {
2113
- size: Theme.Widget.Border.Size,
2114
- color: Theme.Widget.Border.Color
2115
- }
2116
- },
2117
- text: { color: Theme.Widget.FontColor }
2118
- }));
2119
- this.#clickStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.clickStyle, {
2120
- box: {
2121
- color: Theme.Widget.Button.Click.BackgroundColor,
2122
- border: {
2123
- size: Theme.Widget.Border.Size,
2124
- color: Theme.Widget.Border.Color
2125
- }
2126
- },
2127
- text: { color: Theme.Widget.FontColor }
2128
- }));
2436
+ this.#width = config?.width ?? width;
2129
2437
  }
2130
2438
  Size() {
2131
- return { "x": width, "y": height };
2439
+ return { x: this.#width, y: height };
2132
2440
  }
2133
- Draw(ctx, position, scale, mousePosition) {
2134
- const box = {
2135
- Position: { x: 0, y: 0 },
2136
- Size: {
2137
- x: width * scale,
2138
- y: height * scale
2139
- }
2140
- };
2141
- CopyVector2(box.Position, position);
2142
- let style = this.#idleStyle;
2143
- if (mousePosition !== void 0 && !this.#gettingClicked) {
2144
- if (InBox(box, mousePosition)) {
2145
- style = this.#hoverStyle;
2146
- }
2147
- }
2148
- if (this.#gettingClicked) {
2149
- style = this.#clickStyle;
2150
- }
2151
- style.Draw(ctx, box, scale, this.#text);
2152
- return box;
2441
+ text() {
2442
+ return this.#text;
2153
2443
  }
2154
- ClickStart() {
2155
- this.#gettingClicked = true;
2444
+ setText(text) {
2445
+ this.#text = text;
2446
+ requestRender();
2156
2447
  }
2157
- ClickEnd() {
2158
- this.#gettingClicked = false;
2159
- if (this.#callback !== void 0) {
2160
- this.#callback();
2161
- }
2448
+ render(ctx, box, scale) {
2449
+ drawField(ctx, box, scale, {
2450
+ hovered: this.hovered(),
2451
+ pressed: this.pressed(),
2452
+ value: this.#text
2453
+ });
2454
+ }
2455
+ onClick() {
2456
+ this.#callback?.();
2162
2457
  }
2163
2458
  };
2164
2459
 
@@ -2173,500 +2468,214 @@ function contrastColor(color) {
2173
2468
  }
2174
2469
  return "white";
2175
2470
  }
2176
- var ColorWidget = class {
2177
- #node;
2178
- #nodeProperty;
2179
- #value;
2471
+ var picker = null;
2472
+ function sharedPicker() {
2473
+ if (picker === null || !picker.isConnected) {
2474
+ picker = document.createElement("input");
2475
+ picker.type = "color";
2476
+ document.body.appendChild(picker);
2477
+ }
2478
+ return picker;
2479
+ }
2480
+ var ColorWidget = class extends ValueWidget {
2180
2481
  #contrast;
2181
- #textBoxStyle;
2182
- #callback;
2183
2482
  constructor(node, config) {
2184
- this.#node = node;
2185
- this.#nodeProperty = config?.property;
2186
- this.#callback = config?.callback;
2187
- this.#value = startingWidgetValue(node, this.#nodeProperty, config?.value, "#000000");
2188
- this.#contrast = contrastColor(this.#value);
2189
- this.#textBoxStyle = new TextBoxStyle({
2190
- box: {
2191
- color: this.#value,
2192
- border: {
2193
- color: this.#contrast,
2194
- size: Theme.Widget.Border.Size
2195
- }
2196
- },
2197
- text: config?.textStyle
2483
+ super(node, config, "#000000");
2484
+ this.#contrast = contrastColor(this.value());
2485
+ }
2486
+ valueChanged() {
2487
+ this.#contrast = contrastColor(this.value());
2488
+ }
2489
+ render(ctx, box, scale) {
2490
+ drawField(ctx, box, scale, {
2491
+ input: true,
2492
+ hovered: this.hovered(),
2493
+ label: this.label(),
2494
+ value: this.value(),
2495
+ background: this.value(),
2496
+ border: this.#contrast,
2497
+ textColor: this.#contrast
2198
2498
  });
2199
- if (this.#nodeProperty !== void 0) {
2200
- this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
2201
- this.Set(newVal);
2202
- });
2203
- }
2204
- }
2205
- Size() {
2206
- return { "x": width, "y": height };
2207
2499
  }
2208
- Set(value) {
2209
- if (this.#value === value) {
2500
+ onClick() {
2501
+ const canvas = this.canvas();
2502
+ if (canvas === null) {
2210
2503
  return;
2211
2504
  }
2212
- this.#value = value;
2213
- if (this.#callback !== void 0) {
2214
- this.#callback(this.#value);
2215
- }
2216
- if (this.#nodeProperty !== void 0) {
2217
- this.#node.setProperty(this.#nodeProperty, this.#value);
2218
- }
2219
- this.#contrast = contrastColor(this.#value);
2220
- this.#textBoxStyle.setBoxColor(this.#value);
2221
- this.#textBoxStyle.setBorderColor(this.#contrast);
2222
- this.#textBoxStyle.setTextColor(this.#contrast);
2223
- }
2224
- ClickStart() {
2225
- }
2226
- ClickEnd() {
2227
- let input = null;
2228
- const popup = new Popup({
2229
- title: "Set Color",
2230
- options: ["Set", "Cancel"],
2231
- content: () => {
2232
- const container = document.createElement("div");
2233
- input = document.createElement("input");
2234
- input.type = "color";
2235
- input.value = this.#value;
2236
- input.name = "color";
2237
- container.append(input);
2238
- const label = document.createElement("label");
2239
- label.htmlFor = "color";
2240
- label.textContent = "Color";
2241
- container.append(label);
2242
- return container;
2243
- },
2244
- onClose: (button) => {
2245
- if (button !== "Set" || input === null) {
2246
- return;
2247
- }
2248
- this.Set(input.value);
2249
- }
2250
- });
2251
- popup.Show();
2252
- }
2253
- Draw(ctx, position, scale, mousePosition) {
2254
- const box = {
2255
- Position: { x: 0, y: 0 },
2256
- Size: {
2257
- x: width * scale,
2258
- y: height * scale
2259
- }
2260
- };
2261
- CopyVector2(box.Position, position);
2262
- this.#textBoxStyle.Draw(ctx, box, scale, this.#value);
2263
- return box;
2505
+ const before = this.value();
2506
+ const alpha = before.slice(7);
2507
+ const box = this.box();
2508
+ const rect = canvas.getBoundingClientRect();
2509
+ const sx = canvas.width > 0 ? rect.width / canvas.width : 1;
2510
+ const sy = canvas.height > 0 ? rect.height / canvas.height : 1;
2511
+ const input = sharedPicker();
2512
+ input.style.cssText = [
2513
+ "position: fixed",
2514
+ `left: ${rect.left + box.Position.x * sx}px`,
2515
+ `top: ${rect.top + box.Position.y * sy}px`,
2516
+ `width: ${box.Size.x * sx}px`,
2517
+ `height: ${box.Size.y * sy}px`,
2518
+ "opacity: 0",
2519
+ "pointer-events: none",
2520
+ "border: 0",
2521
+ "padding: 0"
2522
+ ].join(";");
2523
+ input.value = before.slice(0, 7);
2524
+ input.oninput = () => this.input(input.value + alpha);
2525
+ input.onchange = () => this.commit(input.value + alpha, before);
2526
+ input.getBoundingClientRect();
2527
+ input.click();
2264
2528
  }
2265
2529
  };
2266
2530
 
2267
2531
  // src/widgets/slider.ts
2268
- var SliderWidget = class {
2269
- // Data Variables =========================================================
2532
+ var SliderWidget = class extends ValueWidget {
2270
2533
  #min;
2271
2534
  #max;
2272
- #value;
2273
- #text;
2274
2535
  #snapIncrement;
2275
- #node;
2276
- #nodeProperty;
2277
- // Callbacks ==============================================================
2278
- #change;
2279
- #release;
2280
- // Styling Variables ======================================================
2281
- #textStyle;
2282
- #backgroundColor;
2283
- #borderColor;
2284
2536
  #fillColor;
2285
- // Runtime Variables ======================================================
2286
- #lastMousePosition;
2287
- #clickStartMousePosition;
2288
- #clicking;
2537
+ #startValue = 0;
2289
2538
  constructor(node, config) {
2290
- this.#node = node;
2291
- this.#value = 0;
2292
- this.#min = config?.min === void 0 ? 0 : config?.min;
2293
- this.#max = config?.max === void 0 ? 1 : config?.max;
2539
+ const min = config?.min ?? 0;
2540
+ const max = config?.max ?? 1;
2541
+ super(node, { ...config, value: config?.value === void 0 ? void 0 : Clamp(config.value, min, max) }, min);
2542
+ this.#min = min;
2543
+ this.#max = max;
2294
2544
  this.#snapIncrement = config?.snapIncrement;
2295
- this.#nodeProperty = config?.property;
2296
- this.#text = "";
2297
- this.#release = config?.release;
2298
- this.#backgroundColor = config?.backgroundColor === void 0 ? Theme.Widget.BackgroundColor : config?.backgroundColor;
2299
- this.#fillColor = config?.fillColor === void 0 ? Theme.Widget.Slider.FillColor : config?.fillColor;
2300
- this.#borderColor = config?.borderColor === void 0 ? Theme.Widget.Border.Color : config?.borderColor;
2301
- this.#textStyle = new TextStyle(TextStyleFallback(config?.textStyle, {
2302
- color: Theme.Widget.FontColor
2303
- }));
2304
- this.#lastMousePosition = Zero();
2305
- this.#clickStartMousePosition = Zero();
2306
- this.#clicking = false;
2307
- if (config?.property !== void 0 && config?.property !== null) {
2308
- node.addPropertyChangeListener(config.property, (oldVal, newVal) => {
2309
- this.SetValue(newVal);
2310
- });
2311
- }
2312
- this.SetValue(startingWidgetValue(node, this.#nodeProperty, config?.value, 0));
2313
- this.#change = config?.change;
2314
- }
2315
- SetValue(newValue) {
2316
- const cleanedValue = Clamp(newValue, this.#min, this.#max);
2317
- if (this.#value === cleanedValue) {
2318
- return;
2319
- }
2320
- this.#value = cleanedValue;
2321
- if (this.#nodeProperty) {
2322
- this.#node.setProperty(this.#nodeProperty, this.#value);
2323
- }
2324
- this.#text = this.#value.toFixed(3);
2325
- if (this.#change !== void 0) {
2326
- this.#change(this.#value);
2327
- }
2545
+ this.#fillColor = config?.fillColor;
2328
2546
  }
2329
- Size() {
2330
- return { "x": width, "y": height };
2547
+ Cursor() {
2548
+ return "ew-resize" /* EWResize */;
2331
2549
  }
2332
- ClickStart() {
2333
- this.#clicking = true;
2334
- CopyVector2(this.#clickStartMousePosition, this.#lastMousePosition);
2550
+ setValue(value) {
2551
+ super.setValue(Clamp(value, this.#min, this.#max));
2335
2552
  }
2336
- ClickEnd() {
2337
- this.#clicking = false;
2338
- if (this.#release !== void 0) {
2339
- this.#release(this.#value);
2340
- }
2341
- }
2342
- Draw(ctx, position, scale, mousePosition) {
2343
- const scaledWidth = width * scale;
2344
- const scaledHeight = height * scale;
2345
- const scaledBorderThickness = Theme.Widget.Border.Size * scale;
2346
- ctx.fillStyle = this.#backgroundColor;
2347
- ctx.strokeStyle = this.#borderColor;
2348
- ctx.lineWidth = scaledBorderThickness;
2553
+ render(ctx, box, scale) {
2554
+ drawFieldBox(ctx, box, scale, { input: true, hovered: this.hovered() || this.pressed(), value: "" });
2555
+ const border = Theme.Widget.Border.Size * scale;
2556
+ const fill = Clamp01((this.value() - this.#min) / (this.#max - this.#min));
2557
+ ctx.fillStyle = this.#fillColor ?? Theme.Widget.Slider.FillColor;
2349
2558
  ctx.beginPath();
2350
2559
  ctx.roundRect(
2351
- position.x,
2352
- position.y,
2353
- scaledWidth,
2354
- scaledHeight,
2560
+ box.Position.x + border / 2,
2561
+ box.Position.y + border / 2,
2562
+ Math.max(0, box.Size.x * fill - border),
2563
+ box.Size.y - border,
2355
2564
  Theme.Widget.Border.Radius * scale
2356
2565
  );
2357
2566
  ctx.fill();
2358
- ctx.stroke();
2359
- const fill = Clamp01((this.#value - this.#min) / (this.#max - this.#min));
2360
- ctx.fillStyle = this.#fillColor;
2361
- ctx.beginPath();
2362
- ctx.roundRect(
2363
- position.x + scaledBorderThickness / 2,
2364
- position.y + scaledBorderThickness / 2,
2365
- scaledWidth * fill - scaledBorderThickness,
2366
- scaledHeight - scaledBorderThickness,
2367
- Theme.Widget.Border.Radius * scale
2368
- );
2369
- ctx.fill();
2370
- ctx.textAlign = "center" /* Center */;
2371
- ctx.textBaseline = "middle" /* Middle */;
2372
- this.#textStyle.setupStyle(ctx, scale);
2373
- ctx.fillText(
2374
- this.#text,
2375
- position.x + scaledWidth / 2,
2376
- position.y + scaledHeight / 2
2377
- );
2378
- if (mousePosition !== void 0) {
2379
- CopyVector2(this.#lastMousePosition, mousePosition);
2380
- if (this.#clicking) {
2381
- const min = position.x + scaledBorderThickness / 2;
2382
- const max = min + scaledWidth - scaledBorderThickness;
2383
- const p = Clamp01((this.#lastMousePosition.x - min) / (max - min));
2384
- let value = p * (this.#max - this.#min) + this.#min;
2385
- if (this.#snapIncrement !== void 0) {
2386
- value = Math.round(value / this.#snapIncrement) * this.#snapIncrement;
2387
- }
2388
- this.SetValue(value);
2389
- }
2567
+ drawFieldText(ctx, box, scale, {
2568
+ input: true,
2569
+ hovered: false,
2570
+ label: this.label(),
2571
+ value: formatNumber(Number(this.value().toFixed(3)))
2572
+ });
2573
+ }
2574
+ onPointerDown(pointer) {
2575
+ this.#startValue = this.value();
2576
+ this.input(this.#valueAt(pointer.position.x));
2577
+ }
2578
+ onPointerMove(pointer) {
2579
+ this.input(this.#valueAt(pointer.position.x));
2580
+ }
2581
+ onPointerUp() {
2582
+ this.commit(this.value(), this.#startValue);
2583
+ }
2584
+ #valueAt(x) {
2585
+ const box = this.box();
2586
+ const border = Theme.Widget.Border.Size * this.scale();
2587
+ const min = box.Position.x + border / 2;
2588
+ const max = min + box.Size.x - border;
2589
+ const p = Clamp01((x - min) / (max - min));
2590
+ let value = p * (this.#max - this.#min) + this.#min;
2591
+ if (this.#snapIncrement !== void 0) {
2592
+ value = Math.round(value / this.#snapIncrement) * this.#snapIncrement;
2390
2593
  }
2391
- return {
2392
- Position: { x: position.x, y: position.y },
2393
- Size: {
2394
- x: scaledWidth,
2395
- y: scaledHeight
2396
- }
2397
- };
2594
+ return Clamp(value, this.#min, this.#max);
2398
2595
  }
2399
2596
  };
2400
2597
 
2401
- // src/popups/string.ts
2402
- function SetStringPopup(config) {
2403
- let input = null;
2404
- return new Popup({
2405
- title: config.title,
2406
- options: ["Set", "Cancel"],
2407
- content: () => {
2408
- const container = document.createElement("div");
2409
- input = document.createElement("input");
2410
- input.value = config.startingValue;
2411
- container.append(input);
2412
- return container;
2413
- },
2414
- onClose: (button) => {
2415
- if (button !== "Set" || input === null) {
2416
- if (config.onCancel) {
2417
- config.onCancel();
2418
- }
2419
- return;
2420
- }
2421
- config.onUpdate(input.value);
2422
- }
2423
- });
2424
- }
2425
-
2426
2598
  // src/widgets/string.ts
2427
- var StringWidget = class {
2428
- #value;
2429
- #idleStyle;
2430
- #hoverStyle;
2431
- #callback;
2432
- #node;
2433
- #nodeProperty;
2599
+ var StringWidget = class extends ValueWidget {
2434
2600
  constructor(node, config) {
2435
- this.#value = "";
2436
- this.#node = node;
2437
- this.#nodeProperty = config?.property;
2438
- this.#idleStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.textBoxStyle, {
2439
- box: {
2440
- color: Theme.Widget.BackgroundColor,
2441
- border: {
2442
- size: Theme.Widget.Border.Size,
2443
- color: Theme.Widget.Border.Color
2444
- },
2445
- radius: Theme.Widget.Border.Radius
2446
- },
2447
- text: { color: Theme.Widget.FontColor }
2448
- }));
2449
- this.#hoverStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.textBoxStyle, {
2450
- box: {
2451
- color: Theme.Widget.Hover.BackgroundColor,
2452
- border: {
2453
- size: Theme.Widget.Border.Size,
2454
- color: Theme.Widget.Border.Color
2455
- },
2456
- radius: Theme.Widget.Border.Radius
2457
- },
2458
- text: { color: Theme.Widget.FontColor }
2459
- }));
2460
- this.Set(startingWidgetValue(node, this.#nodeProperty, config?.value, ""));
2461
- this.#callback = config?.callback;
2462
- if (this.#nodeProperty !== void 0) {
2463
- this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
2464
- this.Set(newVal);
2465
- });
2466
- }
2601
+ super(node, config, "");
2467
2602
  }
2468
- Size() {
2469
- return { "x": width, "y": height };
2470
- }
2471
- Set(value) {
2472
- if (this.#value === value) {
2473
- return;
2474
- }
2475
- this.#value = value;
2476
- if (this.#nodeProperty !== void 0) {
2477
- this.#node.setProperty(this.#nodeProperty, this.#value);
2478
- }
2479
- if (this.#callback !== void 0) {
2480
- this.#callback(this.#value);
2481
- }
2603
+ Cursor() {
2604
+ return "text" /* Text */;
2482
2605
  }
2483
- ClickStart() {
2484
- }
2485
- ClickEnd() {
2486
- const popup = SetStringPopup({
2487
- title: "Set String",
2488
- startingValue: this.#value,
2489
- onUpdate: (value) => {
2490
- this.Set(value);
2491
- }
2606
+ render(ctx, box, scale) {
2607
+ drawField(ctx, box, scale, {
2608
+ input: true,
2609
+ hovered: this.hovered(),
2610
+ label: this.label(),
2611
+ value: this.value()
2492
2612
  });
2493
- popup.Show();
2494
2613
  }
2495
- Draw(ctx, position, scale, mousePosition) {
2496
- const box = {
2497
- Position: { x: 0, y: 0 },
2498
- Size: {
2499
- x: width * scale,
2500
- y: height * scale
2501
- }
2502
- };
2503
- CopyVector2(box.Position, position);
2504
- let style = this.#idleStyle;
2505
- if (mousePosition !== void 0) {
2506
- if (InBox(box, mousePosition)) {
2507
- style = this.#hoverStyle;
2508
- }
2509
- }
2510
- style.DrawUnderline(ctx, box, scale, fitString(ctx, this.#value, box.Size.x - 20 * scale));
2511
- return box;
2614
+ onClick() {
2615
+ const before = this.value();
2616
+ editInPlace({
2617
+ canvas: this.canvas(),
2618
+ box: this.box(),
2619
+ scale: this.scale(),
2620
+ value: before,
2621
+ onCommit: (text) => this.commit(text, before)
2622
+ });
2512
2623
  }
2513
2624
  };
2514
2625
 
2515
2626
  // src/widgets/toggle.ts
2516
- var ToggleStyle = class {
2517
- #idleStyle;
2518
- #hoverStyle;
2519
- #lightColor;
2520
- #lightBorderColor;
2521
- #lightBlur;
2522
- #box;
2523
- constructor(config) {
2524
- this.#idleStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.idle, {
2525
- box: {
2526
- color: Theme.Widget.BackgroundColor,
2527
- border: {
2528
- size: Theme.Widget.Border.Size,
2529
- color: Theme.Widget.Border.Color
2530
- }
2531
- },
2532
- text: { color: Theme.Widget.FontColor }
2533
- }));
2534
- this.#hoverStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.hover, {
2535
- box: {
2536
- color: Theme.Widget.Hover.BackgroundColor,
2537
- border: {
2538
- size: Theme.Widget.Border.Size,
2539
- color: Theme.Widget.Border.Color
2540
- }
2541
- },
2542
- text: { color: Theme.Widget.FontColor }
2543
- }));
2544
- this.#lightColor = config?.lightColor === void 0 ? "#222222" : config.lightColor;
2545
- this.#lightBorderColor = config?.lightBorderColor === void 0 ? "black" : config.lightBorderColor;
2546
- this.#lightBlur = config?.lightBlur;
2547
- this.#box = { Position: { x: 0, y: 0 }, Size: { x: 0, y: 0 } };
2548
- }
2549
- Draw(ctx, pos, scale, text, mousePosition) {
2550
- const scaledWidth = width * scale;
2551
- const scaledHeight = height * scale;
2552
- this.#box.Size.x = scaledWidth;
2553
- this.#box.Size.y = scaledHeight;
2554
- CopyVector2(this.#box.Position, pos);
2555
- let style = this.#idleStyle;
2556
- if (mousePosition !== void 0) {
2557
- if (InBox(this.#box, mousePosition)) {
2558
- style = this.#hoverStyle;
2559
- }
2560
- }
2561
- style.Draw(ctx, this.#box, scale, text);
2562
- const lightScale = Math.min(scaledWidth, scaledHeight);
2563
- ctx.strokeStyle = this.#lightBorderColor;
2564
- ctx.fillStyle = this.#lightColor;
2565
- ctx.beginPath();
2566
- ctx.roundRect(
2567
- pos.x + lightScale * 0.2,
2568
- pos.y + lightScale * 0.2,
2569
- lightScale * 0.6,
2570
- lightScale * 0.6,
2571
- Theme.Widget.Border.Radius * scale
2572
- );
2573
- if (this.#lightBlur) {
2574
- ctx.shadowBlur = this.#lightBlur * scale;
2575
- ctx.shadowColor = this.#lightColor;
2576
- }
2577
- ctx.fill();
2578
- if (this.#lightBlur) {
2579
- ctx.shadowBlur = 0;
2580
- }
2581
- return this.#box;
2582
- }
2583
- };
2584
- var ToggleWidget = class {
2585
- #node;
2586
- #nodeProperty;
2587
- #value;
2588
- #text;
2589
- #enabledStyle;
2590
- #disabledStyle;
2591
- #callback;
2627
+ var ToggleWidget = class extends ValueWidget {
2628
+ #onColor;
2629
+ #offColor;
2592
2630
  constructor(node, config) {
2593
- this.#value = false;
2594
- this.#node = node;
2595
- this.#nodeProperty = config?.property;
2596
- this.#text = config?.text === void 0 ? "Toggle" : config?.text;
2597
- this.#enabledStyle = new ToggleStyle({
2598
- idle: TextBoxStyleWithFallback(config?.enabledStyle?.idle, {
2599
- box: {
2600
- color: Theme.Widget.BackgroundColor,
2601
- border: {
2602
- size: Theme.Widget.Border.Size,
2603
- color: Theme.Widget.Border.Color
2604
- }
2605
- },
2606
- text: { color: Theme.Widget.FontColor }
2607
- }),
2608
- lightBorderColor: config?.enabledStyle?.lightBorderColor,
2609
- lightColor: config?.enabledStyle?.lightColor === void 0 ? "#00FF00" : config?.enabledStyle?.lightColor,
2610
- lightBlur: config?.enabledStyle?.lightBlur === void 0 ? 15 : config?.enabledStyle?.lightBlur
2611
- });
2612
- this.#disabledStyle = new ToggleStyle({
2613
- idle: TextBoxStyleWithFallback(config?.disabledStyle?.idle, {
2614
- box: {
2615
- color: Theme.Widget.BackgroundColor,
2616
- border: {
2617
- size: Theme.Widget.Border.Size,
2618
- color: Theme.Widget.Border.Color
2619
- }
2620
- },
2621
- text: { color: Theme.Widget.FontColor }
2622
- }),
2623
- lightBorderColor: config?.disabledStyle?.lightBorderColor,
2624
- lightColor: config?.disabledStyle?.lightColor === void 0 ? "#004400" : config?.enabledStyle?.lightColor
2625
- });
2626
- this.Set(startingWidgetValue(node, this.#nodeProperty, config?.value, false));
2627
- this.#callback = config?.callback;
2628
- if (this.#nodeProperty !== void 0) {
2629
- this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
2630
- this.Set(newVal);
2631
- });
2632
- }
2633
- }
2634
- Size() {
2635
- return { "x": width, "y": height };
2636
- }
2637
- Draw(ctx, position, scale, mousePosition) {
2638
- let style = this.#value ? this.#enabledStyle : this.#disabledStyle;
2639
- return style.Draw(ctx, position, scale, this.#text, mousePosition);
2631
+ super(node, { ...config, label: config?.label ?? "Toggle" }, false);
2632
+ this.#onColor = config?.onColor ?? "#00FF00";
2633
+ this.#offColor = config?.offColor ?? "#004400";
2640
2634
  }
2641
2635
  Toggle() {
2642
- this.Set(!this.#value);
2643
- }
2644
- Set(value) {
2645
- if (this.#value === value) {
2646
- return;
2647
- }
2648
- this.#value = value;
2649
- if (this.#nodeProperty !== void 0) {
2650
- this.#node.setProperty(this.#nodeProperty, this.#value);
2651
- }
2652
- if (this.#callback !== void 0) {
2653
- this.#callback(this.#value);
2636
+ const before = this.value();
2637
+ this.commit(!before, before);
2638
+ }
2639
+ render(ctx, box, scale) {
2640
+ drawFieldBox(ctx, box, scale, { hovered: this.hovered(), value: "" });
2641
+ const light = Math.min(box.Size.x, box.Size.y) * 0.6;
2642
+ const inset = (box.Size.y - light) / 2;
2643
+ const on = this.value();
2644
+ ctx.fillStyle = on ? this.#onColor : this.#offColor;
2645
+ ctx.beginPath();
2646
+ ctx.roundRect(box.Position.x + inset, box.Position.y + inset, light, light, Theme.Widget.Border.Radius * scale);
2647
+ if (on) {
2648
+ ctx.shadowBlur = 15 * scale;
2649
+ ctx.shadowColor = this.#onColor;
2654
2650
  }
2651
+ ctx.fill();
2652
+ ctx.shadowBlur = 0;
2653
+ const textLeft = box.Position.x + inset * 2 + light;
2654
+ ctx.font = widgetFont(scale);
2655
+ ctx.textAlign = "center";
2656
+ ctx.textBaseline = "middle";
2657
+ ctx.fillStyle = Theme.Widget.FontColor;
2658
+ const room = box.Position.x + box.Size.x - textLeft - inset;
2659
+ ctx.fillText(
2660
+ fitString(ctx, this.label(), room),
2661
+ textLeft + room / 2,
2662
+ box.Position.y + box.Size.y / 2
2663
+ );
2655
2664
  }
2656
- ClickStart() {
2665
+ onClick() {
2657
2666
  this.Toggle();
2658
2667
  }
2659
- ClickEnd() {
2660
- }
2661
2668
  };
2662
2669
 
2663
2670
  // src/widgets/image.ts
2664
- var ImageWidget = class {
2671
+ var ImageWidget = class extends WidgetBase {
2665
2672
  #url;
2673
+ #ownedUrl;
2666
2674
  #image;
2667
2675
  #maxWidth;
2668
2676
  #maxHeight;
2669
2677
  constructor(config) {
2678
+ super();
2670
2679
  this.#maxWidth = config?.maxWidth === void 0 ? 150 : config?.maxWidth;
2671
2680
  this.#maxHeight = config?.maxHeight === void 0 ? 150 : config?.maxHeight;
2672
2681
  if (config?.image) {
@@ -2676,16 +2685,23 @@ var ImageWidget = class {
2676
2685
  }
2677
2686
  }
2678
2687
  SetBlob(blob) {
2679
- const urlCreator = window.URL || window.webkitURL;
2680
- const imageUrl = urlCreator.createObjectURL(blob);
2681
- this.SetUrl(imageUrl);
2688
+ const url = URL.createObjectURL(blob);
2689
+ this.SetUrl(url);
2690
+ this.#ownedUrl = url;
2682
2691
  }
2683
2692
  SetUrl(url) {
2693
+ if (this.#ownedUrl !== void 0) {
2694
+ URL.revokeObjectURL(this.#ownedUrl);
2695
+ this.#ownedUrl = void 0;
2696
+ }
2684
2697
  this.#image = void 0;
2685
2698
  this.#url = url;
2686
2699
  const img = document.createElement("img");
2687
2700
  img.src = url;
2688
2701
  img.onload = () => {
2702
+ if (this.#url !== url) {
2703
+ return;
2704
+ }
2689
2705
  this.#image = img;
2690
2706
  requestRender();
2691
2707
  };
@@ -2693,6 +2709,9 @@ var ImageWidget = class {
2693
2709
  console.log("error loading image:", url, event);
2694
2710
  };
2695
2711
  }
2712
+ Cursor() {
2713
+ return "default" /* Default */;
2714
+ }
2696
2715
  Size() {
2697
2716
  if (this.#image === void 0) {
2698
2717
  return { "x": 0, "y": 0 };
@@ -2712,88 +2731,178 @@ var ImageWidget = class {
2712
2731
  "y": adjust * this.#image.height
2713
2732
  };
2714
2733
  }
2715
- ClickStart() {
2716
- }
2717
- ClickEnd() {
2718
- }
2719
- Draw(ctx, position, scale, mousePosition) {
2720
- const size = this.Size();
2721
- const box = {
2722
- Position: { x: 0, y: 0 },
2723
- Size: {
2724
- x: size.x * scale,
2725
- y: size.y * scale
2726
- }
2727
- };
2728
- CopyVector2(box.Position, position);
2734
+ render(ctx, box, scale) {
2729
2735
  if (!this.#image) {
2730
- return box;
2736
+ return;
2731
2737
  }
2738
+ const size = this.Size();
2739
+ const w = size.x * scale;
2740
+ const h = size.y * scale;
2732
2741
  ctx.drawImage(
2733
2742
  this.#image,
2734
- position.x,
2735
- position.y,
2736
- box.Size.x,
2737
- box.Size.y
2743
+ box.Position.x + (box.Size.x - w) / 2,
2744
+ box.Position.y,
2745
+ w,
2746
+ h
2738
2747
  );
2739
- return box;
2740
2748
  }
2741
2749
  };
2742
2750
 
2743
2751
  // src/widgets/text.ts
2744
- var TextWidget = class {
2752
+ var TextWidget = class extends WidgetBase {
2745
2753
  #value;
2746
- #callback;
2747
- #node;
2748
- #nodeProperty;
2749
2754
  #size;
2755
+ #styleConfig;
2756
+ #styledForTheme = -1;
2757
+ #followTheme() {
2758
+ if (this.#styledForTheme === themeVersion()) {
2759
+ return;
2760
+ }
2761
+ this.#styledForTheme = themeVersion();
2762
+ const style = this.#value.style();
2763
+ style.setColor(this.#styleConfig?.color ?? Theme.Widget.FontColor);
2764
+ style.setSize(this.#styleConfig?.size ?? Theme.Note.FontSize);
2765
+ }
2750
2766
  constructor(node, config) {
2767
+ super();
2751
2768
  this.#size = Zero();
2752
- this.#node = node;
2753
- this.#nodeProperty = config?.property;
2754
- this.#value = new Text("", TextStyleFallback(config?.textBoxStyle, {
2755
- font: Theme.FontFamily,
2756
- color: Theme.Widget.FontColor,
2757
- size: Theme.Note.FontSize
2758
- }));
2759
- this.Set(config?.value === void 0 ? "" : config?.value);
2760
- this.#callback = config?.callback;
2761
- if (this.#nodeProperty !== void 0) {
2762
- this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
2763
- this.Set(newVal);
2764
- });
2769
+ this.#styleConfig = config?.textBoxStyle;
2770
+ this.#value = new Text("", TextStyleFallback(config?.textBoxStyle, {}));
2771
+ this.#followTheme();
2772
+ this.setText(config?.value ?? "");
2773
+ const property = config?.property;
2774
+ if (property !== void 0) {
2775
+ const existing = node.getProperty(property);
2776
+ if (existing !== void 0 && existing !== null) {
2777
+ this.setText(String(existing));
2778
+ }
2779
+ node.addPropertyChangeListener(property, (_, next) => this.setText(String(next)));
2765
2780
  }
2766
2781
  }
2782
+ Cursor() {
2783
+ return "default" /* Default */;
2784
+ }
2767
2785
  Size() {
2768
2786
  return this.#size;
2769
2787
  }
2770
- Set(value) {
2788
+ text() {
2789
+ return this.#value.get();
2790
+ }
2791
+ setText(value) {
2771
2792
  if (this.#value.get() === value) {
2772
2793
  return;
2773
2794
  }
2774
2795
  this.#value.set(value);
2775
- if (this.#nodeProperty !== void 0) {
2776
- this.#node.setProperty(this.#nodeProperty, value);
2796
+ requestRender();
2797
+ }
2798
+ render(ctx, box, scale) {
2799
+ this.#followTheme();
2800
+ this.#value.size(ctx, 1, this.#size);
2801
+ ctx.textAlign = "left";
2802
+ this.#value.render(ctx, scale, box.Position);
2803
+ }
2804
+ };
2805
+
2806
+ // src/widgets/row.ts
2807
+ var RowWidget = class extends WidgetBase {
2808
+ #children;
2809
+ #grow;
2810
+ #gap;
2811
+ #boxes = [];
2812
+ #captured = null;
2813
+ #capturedBox = null;
2814
+ #mouse;
2815
+ constructor(children, config) {
2816
+ super();
2817
+ this.#children = children;
2818
+ this.#grow = config?.grow ?? children.map(() => 1);
2819
+ this.#gap = config?.gap ?? Theme.Widget.Gap;
2820
+ }
2821
+ children() {
2822
+ return this.#children;
2823
+ }
2824
+ Size() {
2825
+ const shown = this.#shown();
2826
+ let x = 0;
2827
+ let y = 0;
2828
+ for (const child of shown) {
2829
+ const size = child.Size();
2830
+ x += size.x;
2831
+ y = Math.max(y, size.y);
2832
+ }
2833
+ x += Math.max(0, shown.length - 1) * this.#gap;
2834
+ return { x, y };
2835
+ }
2836
+ Cursor() {
2837
+ const over = this.#captured ?? this.#childAt(this.#mouse);
2838
+ return over?.Cursor?.() ?? "default" /* Default */;
2839
+ }
2840
+ Draw(ctx, box, scale, mousePosition) {
2841
+ this.#mouse = mousePosition;
2842
+ super.Draw(ctx, box, scale, mousePosition);
2843
+ }
2844
+ render(ctx, box, scale) {
2845
+ const shown = this.#shown();
2846
+ const gap = this.#gap * scale;
2847
+ const natural = shown.reduce((sum, child) => sum + child.Size().x * scale, 0);
2848
+ const extra = Math.max(0, box.Size.x - natural - gap * Math.max(0, shown.length - 1));
2849
+ const totalGrow = shown.reduce((sum, child) => sum + this.#growOf(child), 0);
2850
+ this.#boxes = this.#children.map(() => null);
2851
+ let x = box.Position.x;
2852
+ for (const child of shown) {
2853
+ const size = child.Size();
2854
+ const share = totalGrow > 0 ? extra * (this.#growOf(child) / totalGrow) : 0;
2855
+ const childBox = {
2856
+ Position: { x, y: box.Position.y + (box.Size.y - size.y * scale) / 2 },
2857
+ Size: { x: size.x * scale + share, y: size.y * scale }
2858
+ };
2859
+ this.#boxes[this.#children.indexOf(child)] = childBox;
2860
+ child.Draw(ctx, childBox, scale, this.#captured === null || this.#captured === child ? this.#mouse : void 0);
2861
+ x += childBox.Size.x + gap;
2777
2862
  }
2778
- if (this.#callback !== void 0) {
2779
- this.#callback(value);
2863
+ }
2864
+ onPointerDown(pointer) {
2865
+ const child = this.#childAt(pointer.position);
2866
+ if (child === null || child.Disabled?.()) {
2867
+ return;
2780
2868
  }
2869
+ this.#captured = child;
2870
+ this.#capturedBox = this.#boxes[this.#children.indexOf(child)];
2871
+ child.PointerDown?.(this.#forChild(pointer));
2781
2872
  }
2782
- ClickStart() {
2873
+ onPointerMove(pointer) {
2874
+ this.#captured?.PointerMove?.(this.#forChild(pointer));
2783
2875
  }
2784
- ClickEnd() {
2876
+ onPointerUp(pointer) {
2877
+ const child = this.#captured;
2878
+ const forChild = this.#forChild(pointer);
2879
+ this.#captured = null;
2880
+ this.#capturedBox = null;
2881
+ child?.PointerUp?.(forChild);
2785
2882
  }
2786
- Draw(ctx, position, scale, mousePosition) {
2787
- this.#value.size(ctx, scale, this.#size);
2788
- const box = {
2789
- Position: { x: 0, y: 0 },
2790
- Size: this.#size
2883
+ #forChild(pointer) {
2884
+ return {
2885
+ position: pointer.position,
2886
+ inside: this.#capturedBox !== null && InBox(this.#capturedBox, pointer.position)
2791
2887
  };
2792
- CopyVector2(box.Position, position);
2793
- this.#value.size(ctx, 1, this.#size);
2794
- ctx.textAlign = "left";
2795
- this.#value.render(ctx, scale, box.Position);
2796
- return box;
2888
+ }
2889
+ #childAt(position) {
2890
+ if (position === void 0) {
2891
+ return null;
2892
+ }
2893
+ for (let i = 0; i < this.#children.length; i++) {
2894
+ const box = this.#boxes[i];
2895
+ if (box && InBox(box, position)) {
2896
+ return this.#children[i];
2897
+ }
2898
+ }
2899
+ return null;
2900
+ }
2901
+ #shown() {
2902
+ return this.#children.filter((child) => !child.Hidden?.());
2903
+ }
2904
+ #growOf(child) {
2905
+ return this.#grow[this.#children.indexOf(child)] ?? 1;
2797
2906
  }
2798
2907
  };
2799
2908
 
@@ -2813,6 +2922,9 @@ var WidgetFactory = class {
2813
2922
  }
2814
2923
  return builder(node, config);
2815
2924
  }
2925
+ createFromConfig(node, config) {
2926
+ return this.create(node, config.type, config.config);
2927
+ }
2816
2928
  };
2817
2929
  var GlobalWidgetFactory = new WidgetFactory();
2818
2930
  GlobalWidgetFactory.register("button", (node, config) => new ButtonWidget(config));
@@ -2823,6 +2935,10 @@ GlobalWidgetFactory.register("string", (node, config) => new StringWidget(node,
2823
2935
  GlobalWidgetFactory.register("toggle", (node, config) => new ToggleWidget(node, config));
2824
2936
  GlobalWidgetFactory.register("image", (node, config) => new ImageWidget(config));
2825
2937
  GlobalWidgetFactory.register("text", (node, config) => new TextWidget(node, config));
2938
+ GlobalWidgetFactory.register("row", (node, config) => new RowWidget(
2939
+ (config?.widgets ?? []).map((child) => GlobalWidgetFactory.createFromConfig(node, child)),
2940
+ config
2941
+ ));
2826
2942
 
2827
2943
  // src/connection.ts
2828
2944
  var BACKWARD_PORT_STUB = 30;
@@ -2907,7 +3023,7 @@ var Connection = class {
2907
3023
  return;
2908
3024
  }
2909
3025
  if (this.#inNode !== null) {
2910
- const inPortBox = this.#inNode.inputPortPosition(this.#inNodePortIndex);
3026
+ const inPortBox = this.#inSlotBox();
2911
3027
  if (inPortBox === void 0) {
2912
3028
  return;
2913
3029
  }
@@ -2966,14 +3082,14 @@ var Connection = class {
2966
3082
  this.clearInput();
2967
3083
  this.clearOutput();
2968
3084
  }
2969
- setInput(node, portIndex, replace) {
3085
+ setInput(node, portIndex, replace, insertAt) {
2970
3086
  this.#inNode = node;
2971
3087
  this.#inNodePortIndex = portIndex;
2972
3088
  const port = this.#inNode.inputPort(portIndex);
2973
3089
  if (!!replace && port.connections().length > 0) {
2974
3090
  port.replaceConnection(this, 0);
2975
3091
  } else {
2976
- port.addConnection(this);
3092
+ port.addConnection(this, insertAt);
2977
3093
  }
2978
3094
  }
2979
3095
  setOutput(node, portIndex) {
@@ -2991,9 +3107,33 @@ var Connection = class {
2991
3107
  this.#outNode = null;
2992
3108
  this.#outNodePortIndex = -1;
2993
3109
  }
3110
+ /**
3111
+ * The part of the input port this wire lands on: the whole port, or its
3112
+ * own slot on a port holding several wires.
3113
+ */
3114
+ #inSlotBox() {
3115
+ if (this.#inNode === null) {
3116
+ return void 0;
3117
+ }
3118
+ const box = this.#inNode.inputPortPosition(this.#inNodePortIndex);
3119
+ const port = this.#inNode.inputPort(this.#inNodePortIndex);
3120
+ const slots = port.slots();
3121
+ if (box === void 0 || slots === 1) {
3122
+ return box;
3123
+ }
3124
+ const index = port.connections().indexOf(this);
3125
+ if (index === -1) {
3126
+ return box;
3127
+ }
3128
+ const height2 = box.Size.y / slots;
3129
+ return {
3130
+ Position: { x: box.Position.x, y: box.Position.y + index * height2 },
3131
+ Size: { x: box.Size.x, y: height2 }
3132
+ };
3133
+ }
2994
3134
  mouseOverPort(mousePosition) {
2995
3135
  if (this.#inNode !== null) {
2996
- const inPortBox = this.#inNode.inputPortPosition(this.#inNodePortIndex);
3136
+ const inPortBox = this.#inSlotBox();
2997
3137
  if (inPortBox !== void 0 && InBox(inPortBox, mousePosition)) {
2998
3138
  return this.#inNode.inputPort(this.#inNodePortIndex);
2999
3139
  }
@@ -3070,7 +3210,10 @@ var Camera = class {
3070
3210
  // src/organize.ts
3071
3211
  var widthSpacing = 100;
3072
3212
  var heightSpacing = 50;
3213
+ var laneSpacing = 25;
3214
+ var pieceSpacing = 150;
3073
3215
  var orderingSweeps = 8;
3216
+ var placementSweeps = 8;
3074
3217
  function topologicalOrder(sorting, upstream) {
3075
3218
  const visited = /* @__PURE__ */ new Set();
3076
3219
  const order = new Array();
@@ -3092,11 +3235,7 @@ function topologicalOrder(sorting, upstream) {
3092
3235
  continue;
3093
3236
  }
3094
3237
  visited.add(top.node);
3095
- const feeders = upstream.get(top.node);
3096
- if (feeders === void 0) {
3097
- continue;
3098
- }
3099
- for (const feeder of feeders) {
3238
+ for (const feeder of upstream.get(top.node) ?? []) {
3100
3239
  if (!visited.has(feeder)) {
3101
3240
  stack.push({ node: feeder, expanded: false });
3102
3241
  }
@@ -3105,43 +3244,103 @@ function topologicalOrder(sorting, upstream) {
3105
3244
  }
3106
3245
  return order;
3107
3246
  }
3247
+ function consumersOf(sorting, upstream) {
3248
+ const downstream = /* @__PURE__ */ new Map();
3249
+ for (const node of sorting) {
3250
+ downstream.set(node, []);
3251
+ }
3252
+ for (const node of sorting) {
3253
+ for (const feeder of upstream.get(node) ?? []) {
3254
+ const consumers = downstream.get(feeder);
3255
+ if (consumers !== void 0 && !consumers.includes(node)) {
3256
+ consumers.push(node);
3257
+ }
3258
+ }
3259
+ }
3260
+ return downstream;
3261
+ }
3108
3262
  function assignLayers(sorting, upstream) {
3109
- const order = topologicalOrder(sorting, upstream);
3263
+ const downstream = consumersOf(sorting, upstream);
3264
+ const order = topologicalOrder(sorting, downstream);
3110
3265
  const placed = /* @__PURE__ */ new Set();
3111
- const layerOf = /* @__PURE__ */ new Map();
3266
+ const rank = /* @__PURE__ */ new Map();
3112
3267
  for (const node of order) {
3113
- let layer = 0;
3114
- for (const feeder of upstream.get(node) ?? []) {
3115
- if (placed.has(feeder)) {
3116
- layer = Math.max(layer, layerOf.get(feeder) + 1);
3268
+ let r = 0;
3269
+ for (const consumer of downstream.get(node) ?? []) {
3270
+ if (placed.has(consumer)) {
3271
+ r = Math.max(r, rank.get(consumer) + 1);
3117
3272
  }
3118
3273
  }
3119
- layerOf.set(node, layer);
3274
+ rank.set(node, r);
3120
3275
  placed.add(node);
3121
3276
  }
3122
3277
  let deepest = 0;
3123
- for (const layer of layerOf.values()) {
3124
- deepest = Math.max(deepest, layer);
3278
+ for (const r of rank.values()) {
3279
+ deepest = Math.max(deepest, r);
3125
3280
  }
3126
- const layers = new Array(deepest + 1);
3127
- for (let i = 0; i < layers.length; i++) {
3128
- layers[i] = new Array();
3281
+ const layerOf = /* @__PURE__ */ new Map();
3282
+ for (const node of sorting) {
3283
+ layerOf.set(node, deepest - rank.get(node));
3129
3284
  }
3130
- for (const node of order) {
3131
- layers[layerOf.get(node)].push(node);
3285
+ for (const node of sorting) {
3286
+ const layer = layerOf.get(node);
3287
+ if ((downstream.get(node) ?? []).some((c) => layerOf.get(c) > layer)) {
3288
+ continue;
3289
+ }
3290
+ let pulled = -1;
3291
+ for (const feeder of upstream.get(node) ?? []) {
3292
+ const feederLayer = layerOf.get(feeder);
3293
+ if (feederLayer !== void 0 && feederLayer < layer) {
3294
+ pulled = Math.max(pulled, feederLayer + 1);
3295
+ }
3296
+ }
3297
+ if (pulled >= 0) {
3298
+ layerOf.set(node, pulled);
3299
+ }
3300
+ }
3301
+ const used = [...new Set(layerOf.values())].sort((a, b) => a - b);
3302
+ const compact = /* @__PURE__ */ new Map();
3303
+ used.forEach((layer, i) => compact.set(layer, i));
3304
+ const layers = used.map(() => new Array());
3305
+ for (const node of sorting) {
3306
+ const layer = compact.get(layerOf.get(node));
3307
+ layerOf.set(node, layer);
3308
+ layers[layer].push(node);
3132
3309
  }
3133
3310
  return { layers, layerOf };
3134
3311
  }
3135
- function orderWithinLayers(layout, upstream, downstream) {
3312
+ function crossingsOf(layout, downstream) {
3136
3313
  const slotOf = /* @__PURE__ */ new Map();
3137
- const recordSlots = () => {
3138
- for (const layer of layout.layers) {
3139
- for (let i = 0; i < layer.length; i++) {
3140
- slotOf.set(layer[i], i);
3314
+ for (const layer of layout.layers) {
3315
+ layer.forEach((node, slot) => slotOf.set(node, slot));
3316
+ }
3317
+ let total = 0;
3318
+ for (let i = 0; i + 1 < layout.layers.length; i++) {
3319
+ const spans = new Array();
3320
+ for (const node of layout.layers[i]) {
3321
+ for (const consumer of downstream.get(node) ?? []) {
3322
+ if (layout.layerOf.get(consumer) === i + 1) {
3323
+ spans.push([slotOf.get(node), slotOf.get(consumer)]);
3324
+ }
3141
3325
  }
3142
3326
  }
3143
- };
3144
- recordSlots();
3327
+ for (let a = 0; a < spans.length; a++) {
3328
+ for (let b = a + 1; b < spans.length; b++) {
3329
+ if ((spans[a][0] - spans[b][0]) * (spans[a][1] - spans[b][1]) < 0) {
3330
+ total++;
3331
+ }
3332
+ }
3333
+ }
3334
+ }
3335
+ return total;
3336
+ }
3337
+ function orderWithinLayers(layout, upstream, downstream) {
3338
+ const slotOf = /* @__PURE__ */ new Map();
3339
+ for (const layer of layout.layers) {
3340
+ for (let i = 0; i < layer.length; i++) {
3341
+ slotOf.set(layer[i], i);
3342
+ }
3343
+ }
3145
3344
  const meanSlot = (node, neighbours, layer) => {
3146
3345
  let total = 0;
3147
3346
  let count = 0;
@@ -3152,31 +3351,386 @@ function orderWithinLayers(layout, upstream, downstream) {
3152
3351
  total += slotOf.get(neighbour);
3153
3352
  count++;
3154
3353
  }
3155
- return count === 0 ? -1 : total / count;
3354
+ return count === 0 ? -1 : total / count;
3355
+ };
3356
+ const sweep = (neighbours, towards) => {
3357
+ for (let i = 0; i < layout.layers.length; i++) {
3358
+ const index = towards > 0 ? i : layout.layers.length - 1 - i;
3359
+ const adjacent = index + towards;
3360
+ if (adjacent < 0 || adjacent >= layout.layers.length) {
3361
+ continue;
3362
+ }
3363
+ const layer = layout.layers[index];
3364
+ const keys = /* @__PURE__ */ new Map();
3365
+ for (const node of layer) {
3366
+ const mean = meanSlot(node, neighbours, adjacent);
3367
+ keys.set(node, mean === -1 ? slotOf.get(node) : mean);
3368
+ }
3369
+ layer.sort((a, b) => keys.get(a) - keys.get(b));
3370
+ for (let s = 0; s < layer.length; s++) {
3371
+ slotOf.set(layer[s], s);
3372
+ }
3373
+ }
3374
+ };
3375
+ let best = layout.layers.map((layer) => layer.slice());
3376
+ let fewest = crossingsOf(layout, downstream);
3377
+ for (let i = 0; i < orderingSweeps && fewest > 0; i++) {
3378
+ sweep(upstream, -1);
3379
+ sweep(downstream, 1);
3380
+ const crossings = crossingsOf(layout, downstream);
3381
+ if (crossings < fewest) {
3382
+ fewest = crossings;
3383
+ best = layout.layers.map((layer) => layer.slice());
3384
+ }
3385
+ }
3386
+ for (let i = 0; i < best.length; i++) {
3387
+ layout.layers[i] = best[i];
3388
+ }
3389
+ }
3390
+ function findPieces(sorting, upstream, downstream) {
3391
+ const rankOf = /* @__PURE__ */ new Map();
3392
+ sorting.forEach((node, i) => rankOf.set(node, i));
3393
+ const seen = /* @__PURE__ */ new Set();
3394
+ const pieces = new Array();
3395
+ for (const start of sorting) {
3396
+ if (seen.has(start)) {
3397
+ continue;
3398
+ }
3399
+ seen.add(start);
3400
+ const piece = new Array();
3401
+ const stack = [start];
3402
+ while (stack.length > 0) {
3403
+ const node = stack.pop();
3404
+ piece.push(node);
3405
+ for (const neighbour of [...upstream.get(node) ?? [], ...downstream.get(node) ?? []]) {
3406
+ if (rankOf.has(neighbour) && !seen.has(neighbour)) {
3407
+ seen.add(neighbour);
3408
+ stack.push(neighbour);
3409
+ }
3410
+ }
3411
+ }
3412
+ piece.sort((a, b) => rankOf.get(a) - rankOf.get(b));
3413
+ pieces.push(piece);
3414
+ }
3415
+ return pieces;
3416
+ }
3417
+ function treeRoot(piece, downstream) {
3418
+ let root;
3419
+ for (const node of piece) {
3420
+ const consumers = downstream.get(node) ?? [];
3421
+ if (consumers.length > 1) {
3422
+ return void 0;
3423
+ }
3424
+ if (consumers.length === 0) {
3425
+ if (root !== void 0) {
3426
+ return void 0;
3427
+ }
3428
+ root = node;
3429
+ }
3430
+ }
3431
+ return root;
3432
+ }
3433
+ function columnXs(widths) {
3434
+ const xs = new Array();
3435
+ let x = 0;
3436
+ for (const width2 of widths) {
3437
+ xs.push(x);
3438
+ x += width2 + widthSpacing;
3439
+ }
3440
+ return xs;
3441
+ }
3442
+ function layoutTree(root, upstream, sizes) {
3443
+ const children = (node) => [...new Set(upstream.get(node) ?? [])];
3444
+ const depthOf = /* @__PURE__ */ new Map([[root, 0]]);
3445
+ const preorder = new Array();
3446
+ const stack = [root];
3447
+ let deepest = 0;
3448
+ while (stack.length > 0) {
3449
+ const node = stack.pop();
3450
+ preorder.push(node);
3451
+ const kids = children(node);
3452
+ for (let i = kids.length - 1; i >= 0; i--) {
3453
+ const depth = depthOf.get(node) + 1;
3454
+ depthOf.set(kids[i], depth);
3455
+ deepest = Math.max(deepest, depth);
3456
+ stack.push(kids[i]);
3457
+ }
3458
+ }
3459
+ const widths = new Array(deepest + 1).fill(0);
3460
+ for (const [node, depth] of depthOf) {
3461
+ widths[deepest - depth] = Math.max(widths[deepest - depth], sizes.get(node).x);
3462
+ }
3463
+ const xs = columnXs(widths);
3464
+ const band = /* @__PURE__ */ new Map();
3465
+ const topInBand = /* @__PURE__ */ new Map();
3466
+ const childOffset = /* @__PURE__ */ new Map();
3467
+ for (let i = preorder.length - 1; i >= 0; i--) {
3468
+ const node = preorder[i];
3469
+ const height2 = sizes.get(node).y;
3470
+ const kids = children(node);
3471
+ if (kids.length === 0) {
3472
+ band.set(node, height2);
3473
+ topInBand.set(node, 0);
3474
+ continue;
3475
+ }
3476
+ let offset = 0;
3477
+ for (const kid of kids) {
3478
+ childOffset.set(kid, offset);
3479
+ offset += band.get(kid) + heightSpacing;
3480
+ }
3481
+ const centre = (kid) => childOffset.get(kid) + topInBand.get(kid) + sizes.get(kid).y / 2;
3482
+ let top = (centre(kids[0]) + centre(kids[kids.length - 1])) / 2 - height2 / 2;
3483
+ let shift = 0;
3484
+ if (top < 0) {
3485
+ shift = -top;
3486
+ top = 0;
3487
+ for (const kid of kids) {
3488
+ childOffset.set(kid, childOffset.get(kid) + shift);
3489
+ }
3490
+ }
3491
+ band.set(node, Math.max(offset - heightSpacing + shift, top + height2));
3492
+ topInBand.set(node, top);
3493
+ }
3494
+ const positions = /* @__PURE__ */ new Map();
3495
+ const bandTop = /* @__PURE__ */ new Map([[root, 0]]);
3496
+ for (const node of preorder) {
3497
+ const column = deepest - depthOf.get(node);
3498
+ const size = sizes.get(node);
3499
+ positions.set(node, {
3500
+ x: xs[column] + (widths[column] - size.x) / 2,
3501
+ y: bandTop.get(node) + topInBand.get(node)
3502
+ });
3503
+ for (const kid of children(node)) {
3504
+ bandTop.set(kid, bandTop.get(node) + childOffset.get(kid));
3505
+ }
3506
+ }
3507
+ return positions;
3508
+ }
3509
+ function isotonic(targets) {
3510
+ const blocks = new Array();
3511
+ for (const target of targets) {
3512
+ blocks.push({ sum: target, count: 1 });
3513
+ while (blocks.length > 1) {
3514
+ const last = blocks[blocks.length - 1];
3515
+ const before = blocks[blocks.length - 2];
3516
+ if (before.sum / before.count <= last.sum / last.count) {
3517
+ break;
3518
+ }
3519
+ before.sum += last.sum;
3520
+ before.count += last.count;
3521
+ blocks.pop();
3522
+ }
3523
+ }
3524
+ const result = new Array();
3525
+ for (const block of blocks) {
3526
+ for (let i = 0; i < block.count; i++) {
3527
+ result.push(block.sum / block.count);
3528
+ }
3529
+ }
3530
+ return result;
3531
+ }
3532
+ function median(values) {
3533
+ const sorted = values.slice().sort((a, b) => a - b);
3534
+ const middle = Math.floor(sorted.length / 2);
3535
+ return sorted.length % 2 === 1 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
3536
+ }
3537
+ function layoutLayered(piece, upstream, downstream, sizes) {
3538
+ const layout = assignLayers(piece, upstream);
3539
+ const up = /* @__PURE__ */ new Map();
3540
+ const down = /* @__PURE__ */ new Map();
3541
+ const heightOf = /* @__PURE__ */ new Map();
3542
+ for (const node of piece) {
3543
+ up.set(node, [...new Set(upstream.get(node) ?? [])]);
3544
+ down.set(node, [...new Set(downstream.get(node) ?? [])]);
3545
+ heightOf.set(node, sizes.get(node).y);
3546
+ }
3547
+ let nextLane = -1;
3548
+ for (const from of piece) {
3549
+ const fromLayer = layout.layerOf.get(from);
3550
+ const consumers = down.get(from);
3551
+ for (let c = 0; c < consumers.length; c++) {
3552
+ const to = consumers[c];
3553
+ const toLayer = layout.layerOf.get(to);
3554
+ if (toLayer - fromLayer < 2) {
3555
+ continue;
3556
+ }
3557
+ let previous = from;
3558
+ for (let layer = fromLayer + 1; layer < toLayer; layer++) {
3559
+ const lane = nextLane--;
3560
+ layout.layerOf.set(lane, layer);
3561
+ layout.layers[layer].push(lane);
3562
+ heightOf.set(lane, 0);
3563
+ up.set(lane, [previous]);
3564
+ down.set(lane, []);
3565
+ if (previous === from) {
3566
+ consumers[c] = lane;
3567
+ } else {
3568
+ down.get(previous).push(lane);
3569
+ }
3570
+ previous = lane;
3571
+ }
3572
+ down.get(previous).push(to);
3573
+ const feeders = up.get(to);
3574
+ feeders[feeders.indexOf(from)] = previous;
3575
+ }
3576
+ }
3577
+ const sequence = /* @__PURE__ */ new Map();
3578
+ for (const sink of piece) {
3579
+ if (down.get(sink).length > 0) {
3580
+ continue;
3581
+ }
3582
+ const stack = [sink];
3583
+ while (stack.length > 0) {
3584
+ const node = stack.pop();
3585
+ if (sequence.has(node)) {
3586
+ continue;
3587
+ }
3588
+ sequence.set(node, sequence.size);
3589
+ const feeders = up.get(node) ?? [];
3590
+ for (let i = feeders.length - 1; i >= 0; i--) {
3591
+ stack.push(feeders[i]);
3592
+ }
3593
+ }
3594
+ }
3595
+ for (const layer of layout.layers) {
3596
+ layer.sort((a, b) => (sequence.get(a) ?? Infinity) - (sequence.get(b) ?? Infinity));
3597
+ }
3598
+ orderWithinLayers(layout, up, down);
3599
+ const gapAfter = (above, below) => above < 0 || below < 0 ? laneSpacing : heightSpacing;
3600
+ const topOf = /* @__PURE__ */ new Map();
3601
+ for (const layer of layout.layers) {
3602
+ let y = 0;
3603
+ for (let i = 0; i < layer.length; i++) {
3604
+ topOf.set(layer[i], y);
3605
+ y += heightOf.get(layer[i]) + (i + 1 < layer.length ? gapAfter(layer[i], layer[i + 1]) : 0);
3606
+ }
3607
+ for (const node of layer) {
3608
+ topOf.set(node, topOf.get(node) - y / 2);
3609
+ }
3610
+ }
3611
+ const centreOf = (node) => topOf.get(node) + heightOf.get(node) / 2;
3612
+ const place = (layer, neighbours, adjacent) => {
3613
+ const offsets = new Array();
3614
+ const targets = new Array();
3615
+ let offset = 0;
3616
+ for (let i = 0; i < layer.length; i++) {
3617
+ const node = layer[i];
3618
+ const centres = (neighbours.get(node) ?? []).filter((n) => layout.layerOf.get(n) === adjacent).map(centreOf);
3619
+ const desired = centres.length > 0 ? median(centres) - heightOf.get(node) / 2 : topOf.get(node);
3620
+ offsets.push(offset);
3621
+ targets.push(desired - offset);
3622
+ if (i + 1 < layer.length) {
3623
+ offset += heightOf.get(node) + gapAfter(node, layer[i + 1]);
3624
+ }
3625
+ }
3626
+ const solved = isotonic(targets);
3627
+ for (let i = 0; i < layer.length; i++) {
3628
+ topOf.set(layer[i], solved[i] + offsets[i]);
3629
+ }
3156
3630
  };
3157
- const sweep = (neighbours, towards) => {
3158
- for (let i = 0; i < layout.layers.length; i++) {
3159
- const index = towards > 0 ? i : layout.layers.length - 1 - i;
3160
- const adjacent = index + towards;
3161
- if (adjacent < 0 || adjacent >= layout.layers.length) {
3162
- continue;
3631
+ for (let s = 0; s < placementSweeps; s++) {
3632
+ if (s % 2 === 0) {
3633
+ for (let i = 1; i < layout.layers.length; i++) {
3634
+ place(layout.layers[i], up, i - 1);
3163
3635
  }
3164
- const layer = layout.layers[index];
3165
- const keys = /* @__PURE__ */ new Map();
3166
- for (const node of layer) {
3167
- const mean = meanSlot(node, neighbours, adjacent);
3168
- keys.set(node, mean === -1 ? slotOf.get(node) : mean);
3169
- }
3170
- layer.sort((a, b) => keys.get(a) - keys.get(b));
3171
- for (let s = 0; s < layer.length; s++) {
3172
- slotOf.set(layer[s], s);
3636
+ } else {
3637
+ for (let i = layout.layers.length - 2; i >= 0; i--) {
3638
+ place(layout.layers[i], down, i + 1);
3173
3639
  }
3174
3640
  }
3175
- };
3176
- for (let i = 0; i < orderingSweeps; i++) {
3177
- sweep(upstream, -1);
3178
- sweep(downstream, 1);
3179
3641
  }
3642
+ const widths = layout.layers.map((layer) => layer.reduce((widest, node) => node < 0 ? widest : Math.max(widest, sizes.get(node).x), 0));
3643
+ const xs = columnXs(widths);
3644
+ const positions = /* @__PURE__ */ new Map();
3645
+ for (const node of piece) {
3646
+ const layer = layout.layerOf.get(node);
3647
+ positions.set(node, {
3648
+ x: xs[layer] + (widths[layer] - sizes.get(node).x) / 2,
3649
+ y: topOf.get(node)
3650
+ });
3651
+ }
3652
+ return positions;
3653
+ }
3654
+ function layoutLoose(loose, sizes) {
3655
+ const columns = Math.ceil(Math.sqrt(loose.length));
3656
+ const widths = new Array(columns).fill(0);
3657
+ const heights = new Array(Math.ceil(loose.length / columns)).fill(0);
3658
+ loose.forEach((node, i) => {
3659
+ const size = sizes.get(node);
3660
+ widths[i % columns] = Math.max(widths[i % columns], size.x);
3661
+ heights[Math.floor(i / columns)] = Math.max(heights[Math.floor(i / columns)], size.y);
3662
+ });
3663
+ const xs = new Array();
3664
+ let x = 0;
3665
+ for (const width2 of widths) {
3666
+ xs.push(x);
3667
+ x += width2 + heightSpacing;
3668
+ }
3669
+ const ys = new Array();
3670
+ let y = 0;
3671
+ for (const height2 of heights) {
3672
+ ys.push(y);
3673
+ y += height2 + heightSpacing;
3674
+ }
3675
+ const positions = /* @__PURE__ */ new Map();
3676
+ loose.forEach((node, i) => positions.set(node, { x: xs[i % columns], y: ys[Math.floor(i / columns)] }));
3677
+ return positions;
3678
+ }
3679
+ function toBlock(positions, sizes) {
3680
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
3681
+ for (const [node, position] of positions) {
3682
+ const size = sizes.get(node);
3683
+ minX = Math.min(minX, position.x);
3684
+ minY = Math.min(minY, position.y);
3685
+ maxX = Math.max(maxX, position.x + size.x);
3686
+ maxY = Math.max(maxY, position.y + size.y);
3687
+ }
3688
+ const shifted = /* @__PURE__ */ new Map();
3689
+ for (const [node, position] of positions) {
3690
+ shifted.set(node, { x: position.x - minX, y: position.y - minY });
3691
+ }
3692
+ return { positions: shifted, size: { x: maxX - minX, y: maxY - minY } };
3693
+ }
3694
+ function layoutGraph(sorting, upstream, downstream, sizes) {
3695
+ const blocks = new Array();
3696
+ const loose = new Array();
3697
+ for (const piece of findPieces(sorting, upstream, downstream)) {
3698
+ if (piece.length === 1) {
3699
+ loose.push(piece[0]);
3700
+ continue;
3701
+ }
3702
+ const root = treeRoot(piece, downstream);
3703
+ const positions2 = root !== void 0 ? layoutTree(root, upstream, sizes) : layoutLayered(piece, upstream, downstream, sizes);
3704
+ blocks.push(toBlock(positions2, sizes));
3705
+ }
3706
+ blocks.sort((a, b) => b.size.y - a.size.y || b.size.x - a.size.x);
3707
+ if (loose.length > 0) {
3708
+ blocks.push(toBlock(layoutLoose(loose, sizes), sizes));
3709
+ }
3710
+ let area = 0;
3711
+ let widest = 0;
3712
+ for (const block of blocks) {
3713
+ area += (block.size.x + pieceSpacing) * (block.size.y + pieceSpacing);
3714
+ widest = Math.max(widest, block.size.x);
3715
+ }
3716
+ const rowWidth = Math.max(widest, Math.sqrt(area));
3717
+ const positions = /* @__PURE__ */ new Map();
3718
+ let x = 0;
3719
+ let y = 0;
3720
+ let rowHeight = 0;
3721
+ for (const block of blocks) {
3722
+ if (x > 0 && x + block.size.x > rowWidth) {
3723
+ x = 0;
3724
+ y += rowHeight + pieceSpacing;
3725
+ rowHeight = 0;
3726
+ }
3727
+ for (const [node, position] of block.positions) {
3728
+ positions.set(node, { x: position.x + x, y: position.y + y });
3729
+ }
3730
+ x += block.size.x + pieceSpacing;
3731
+ rowHeight = Math.max(rowHeight, block.size.y);
3732
+ }
3733
+ return positions;
3180
3734
  }
3181
3735
  function Organize(ctx, graph, nodesToSort) {
3182
3736
  const nodes = graph.getNodes();
@@ -3185,9 +3739,6 @@ function Organize(ctx, graph, nodesToSort) {
3185
3739
  }
3186
3740
  const sorting = new Array();
3187
3741
  if (nodesToSort) {
3188
- if (nodesToSort.length < 2) {
3189
- return;
3190
- }
3191
3742
  for (const index of nodesToSort) {
3192
3743
  if (index >= 0 && index < nodes.length) {
3193
3744
  sorting.push(index);
@@ -3207,33 +3758,28 @@ function Organize(ctx, graph, nodesToSort) {
3207
3758
  nodeLUT.set(nodes[i], i);
3208
3759
  }
3209
3760
  const camera = new Camera();
3210
- const bounds = /* @__PURE__ */ new Map();
3761
+ const sizes = /* @__PURE__ */ new Map();
3211
3762
  for (const index of sorting) {
3212
- bounds.set(index, nodes[index].calculateBounds(ctx, camera));
3763
+ sizes.set(index, nodes[index].calculateBounds(ctx, camera).Size);
3213
3764
  }
3214
3765
  const upstream = /* @__PURE__ */ new Map();
3215
- const downstream = /* @__PURE__ */ new Map();
3216
3766
  for (const index of sorting) {
3767
+ const node = nodes[index];
3217
3768
  const feeders = new Array();
3218
- for (const feeder of graph.connectedInputsNodeReferencesByIndex(index)) {
3219
- const feederIndex = nodeLUT.get(feeder);
3220
- if (feederIndex !== void 0 && inLayout.has(feederIndex) && feederIndex !== index) {
3221
- feeders.push(feederIndex);
3769
+ for (let i = 0; i < node.inputs(); i++) {
3770
+ for (const connection of node.inputPort(i).connections()) {
3771
+ const feeder = connection.outNode();
3772
+ const feederIndex = feeder === null ? void 0 : nodeLUT.get(feeder);
3773
+ if (feederIndex !== void 0 && inLayout.has(feederIndex) && feederIndex !== index && !feeders.includes(feederIndex)) {
3774
+ feeders.push(feederIndex);
3775
+ }
3222
3776
  }
3223
3777
  }
3224
3778
  upstream.set(index, feeders);
3225
- const consumers = new Array();
3226
- for (const consumer of graph.connectedOutputsNodeReferences(index)) {
3227
- const consumerIndex = nodeLUT.get(consumer);
3228
- if (consumerIndex !== void 0 && inLayout.has(consumerIndex) && consumerIndex !== index) {
3229
- consumers.push(consumerIndex);
3230
- }
3231
- }
3232
- downstream.set(index, consumers);
3233
3779
  }
3234
- const layout = assignLayers(sorting, upstream);
3235
- orderWithinLayers(layout, upstream, downstream);
3236
- let anchor = { x: 0, y: 0 };
3780
+ const downstream = consumersOf(sorting, upstream);
3781
+ const positions = layoutGraph(sorting, upstream, downstream, sizes);
3782
+ const anchor = { x: 0, y: 0 };
3237
3783
  for (const index of sorting) {
3238
3784
  const position = nodes[index].getPosition();
3239
3785
  anchor.x += position.x;
@@ -3241,19 +3787,14 @@ function Organize(ctx, graph, nodesToSort) {
3241
3787
  }
3242
3788
  anchor.x /= sorting.length;
3243
3789
  anchor.y /= sorting.length;
3244
- const layerWidths = layout.layers.map((layer) => layer.reduce((widest, node) => Math.max(widest, bounds.get(node).Size.x), 0));
3245
- const layerHeights = layout.layers.map((layer) => layer.reduce((total, node) => total + bounds.get(node).Size.y + heightSpacing, -heightSpacing));
3246
- const totalWidth = layerWidths.reduce((total, width2) => total + width2 + widthSpacing, -widthSpacing);
3247
- const tallest = layerHeights.reduce((tallest2, height2) => Math.max(tallest2, height2), 0);
3248
- let x = anchor.x - totalWidth / 2;
3249
- for (let i = 0; i < layout.layers.length; i++) {
3250
- let y = anchor.y - tallest / 2 + (tallest - layerHeights[i]) / 2;
3251
- for (const node of layout.layers[i]) {
3252
- const size = bounds.get(node).Size;
3253
- nodes[node].setPosition({ x: x + (layerWidths[i] - size.x) / 2, y });
3254
- y += size.y + heightSpacing;
3255
- }
3256
- x += layerWidths[i] + widthSpacing;
3790
+ let right = 0;
3791
+ let bottom = 0;
3792
+ for (const [index, position] of positions) {
3793
+ right = Math.max(right, position.x + sizes.get(index).x);
3794
+ bottom = Math.max(bottom, position.y + sizes.get(index).y);
3795
+ }
3796
+ for (const [index, position] of positions) {
3797
+ nodes[index].setPosition({ x: anchor.x - right / 2 + position.x, y: anchor.y - bottom / 2 + position.y });
3257
3798
  }
3258
3799
  }
3259
3800
 
@@ -3445,14 +3986,17 @@ var NodeSubsystem2 = class {
3445
3986
  #portHovering;
3446
3987
  #widgetHovering;
3447
3988
  #widgetCurrentlyClicking;
3989
+ #widgetHoveringBox = null;
3990
+ #capturedWidgetBox = null;
3991
+ #capturedPointer = Zero();
3448
3992
  #cursor;
3449
3993
  #nodeFactory;
3450
3994
  #boxSelect;
3451
3995
  #boxSelectStart_graphSpace;
3452
3996
  #boxSelectEnd_graphSpace;
3453
3997
  #boxSelectionNodes;
3454
- #boxSelectStyle;
3455
3998
  #lastCtx;
3999
+ #lastMousePosition;
3456
4000
  // LISTENERS ==============================================================
3457
4001
  #nodeRemovedCallbacks;
3458
4002
  #registeredNodeAddedCallbacks;
@@ -3477,14 +4021,6 @@ var NodeSubsystem2 = class {
3477
4021
  this.#boxSelectEnd_graphSpace = Zero();
3478
4022
  this.#boxSelectionNodes = new List();
3479
4023
  this.#lastCtx = null;
3480
- this.#boxSelectStyle = new BoxStyle({
3481
- border: {
3482
- color: Theme.BoxSelect.Color,
3483
- size: Theme.BoxSelect.Size
3484
- },
3485
- color: "rgba(0,0,0,0)",
3486
- radius: Theme.BoxSelect.Radius
3487
- });
3488
4024
  }
3489
4025
  addPublisher(identifier, publisher) {
3490
4026
  this.#nodeFactory.addPublisher(identifier, publisher);
@@ -3521,6 +4057,7 @@ var NodeSubsystem2 = class {
3521
4057
  }
3522
4058
  if (intersection.Widget !== void 0) {
3523
4059
  this.#widgetHovering = intersection.Widget;
4060
+ this.#widgetHoveringBox = intersection.WidgetBox ?? null;
3524
4061
  }
3525
4062
  if (intersection.Port !== void 0 && intersection.Node !== void 0 && intersection.PortIndex !== void 0 && intersection.PortIsInput !== void 0) {
3526
4063
  this.#portHovering = {
@@ -3543,8 +4080,15 @@ var NodeSubsystem2 = class {
3543
4080
  hoveringSomething = true;
3544
4081
  }
3545
4082
  if (this.#widgetHovering !== null) {
3546
- this.#widgetHovering.ClickStart();
3547
- this.#widgetCurrentlyClicking = this.#widgetHovering;
4083
+ if (!this.#widgetHovering.Disabled?.()) {
4084
+ this.#widgetCurrentlyClicking = this.#widgetHovering;
4085
+ this.#capturedWidgetBox = this.#widgetHoveringBox === null ? null : {
4086
+ Position: { x: this.#widgetHoveringBox.Position.x, y: this.#widgetHoveringBox.Position.y },
4087
+ Size: { x: this.#widgetHoveringBox.Size.x, y: this.#widgetHoveringBox.Size.y }
4088
+ };
4089
+ CopyVector2(this.#capturedPointer, mousePosition);
4090
+ this.#widgetCurrentlyClicking.PointerDown?.(this.#widgetPointer());
4091
+ }
3548
4092
  hoveringSomething = true;
3549
4093
  }
3550
4094
  if (this.#portHovering === null) {
@@ -3559,12 +4103,12 @@ var NodeSubsystem2 = class {
3559
4103
  return hoveringSomething || ctrlKey;
3560
4104
  }
3561
4105
  if (this.#portHovering.InputPort) {
3562
- for (let i = 0; i < this.#connections.length; i++) {
3563
- if (this.#connections[i].inPort() === this.#portHovering.Port) {
3564
- this.#connections[i].clearInput();
3565
- this.#connectionSelected = this.#connections[i];
3566
- break;
3567
- }
4106
+ const port = this.#portHovering.Port;
4107
+ const wires = port.connections();
4108
+ const picked = wires.find((wire) => wire.mouseOverPort(mousePosition) === port) ?? wires[0];
4109
+ if (picked !== void 0) {
4110
+ picked.clearInput();
4111
+ this.#connectionSelected = picked;
3568
4112
  }
3569
4113
  } else {
3570
4114
  let inNode = this.#portHovering.Node;
@@ -3584,7 +4128,19 @@ var NodeSubsystem2 = class {
3584
4128
  }
3585
4129
  return true;
3586
4130
  }
4131
+ #widgetPointer() {
4132
+ return {
4133
+ position: { x: this.#capturedPointer.x, y: this.#capturedPointer.y },
4134
+ inside: this.#capturedWidgetBox !== null && InBox(this.#capturedWidgetBox, this.#capturedPointer)
4135
+ };
4136
+ }
3587
4137
  mouseDragEvent(delta, scale) {
4138
+ if (this.#widgetCurrentlyClicking !== null) {
4139
+ this.#capturedPointer.x += delta.x;
4140
+ this.#capturedPointer.y += delta.y;
4141
+ this.#widgetCurrentlyClicking.PointerMove?.(this.#widgetPointer());
4142
+ return true;
4143
+ }
3588
4144
  let nodeMoved = false;
3589
4145
  VectorPool.run(() => {
3590
4146
  const scaledDelta = VectorPool.get();
@@ -3703,13 +4259,20 @@ var NodeSubsystem2 = class {
3703
4259
  }
3704
4260
  #firstCompatiblePort(node, side, against) {
3705
4261
  const count = side === "INPUT" /* Input */ ? node.inputs() : node.outputs();
4262
+ let firstHidden = -1;
3706
4263
  for (let i = 0; i < count; i++) {
3707
4264
  const port = side === "INPUT" /* Input */ ? node.inputPort(i) : node.outputPort(i);
3708
- if (portsCompatible(port, against)) {
4265
+ if (!portsCompatible(port, against)) {
4266
+ continue;
4267
+ }
4268
+ if (!port.hidden()) {
3709
4269
  return i;
3710
4270
  }
4271
+ if (firstHidden === -1) {
4272
+ firstHidden = i;
4273
+ }
3711
4274
  }
3712
- return -1;
4275
+ return firstHidden;
3713
4276
  }
3714
4277
  clickEnd() {
3715
4278
  for (let i = 0; i < this.#nodesGrabbed.Count(); i++) {
@@ -3725,10 +4288,11 @@ var NodeSubsystem2 = class {
3725
4288
  this.#boxSelect = false;
3726
4289
  }
3727
4290
  if (this.#widgetCurrentlyClicking !== null) {
3728
- if (this.#widgetHovering === this.#widgetCurrentlyClicking) {
3729
- this.#widgetCurrentlyClicking.ClickEnd();
3730
- }
4291
+ const widget = this.#widgetCurrentlyClicking;
4292
+ const pointer = this.#widgetPointer();
3731
4293
  this.#widgetCurrentlyClicking = null;
4294
+ this.#capturedWidgetBox = null;
4295
+ widget.PointerUp?.(pointer);
3732
4296
  }
3733
4297
  if (this.#connectionSelected === null) {
3734
4298
  return;
@@ -3773,7 +4337,7 @@ var NodeSubsystem2 = class {
3773
4337
  inputPort.connections()[0].clearOutput();
3774
4338
  this.#removeConnection(inputPort.connections()[0], false);
3775
4339
  }
3776
- conn.setInput(this.#portHovering.Node, this.#portHovering.Index, replace);
4340
+ conn.setInput(this.#portHovering.Node, this.#portHovering.Index, replace, this.#insertIndex());
3777
4341
  } else {
3778
4342
  conn.setOutput(this.#portHovering.Node, this.#portHovering.Index);
3779
4343
  }
@@ -3913,6 +4477,14 @@ var NodeSubsystem2 = class {
3913
4477
  this.#removeNodeConnections(nodeIndex);
3914
4478
  const node = this.#nodes[nodeIndex];
3915
4479
  this.#nodes.splice(nodeIndex, 1);
4480
+ if (this.#nodeHovering === nodeIndex) {
4481
+ this.#nodeHovering = -1;
4482
+ } else if (this.#nodeHovering > nodeIndex) {
4483
+ this.#nodeHovering--;
4484
+ }
4485
+ if (this.#portHovering?.Node === node) {
4486
+ this.#portHovering = null;
4487
+ }
3916
4488
  requestRender();
3917
4489
  for (let i = 0; i < this.#nodeRemovedCallbacks.length; i++) {
3918
4490
  this.#nodeRemovedCallbacks[i](node);
@@ -4137,7 +4709,8 @@ var NodeSubsystem2 = class {
4137
4709
  }
4138
4710
  if (intersection.Widget !== void 0) {
4139
4711
  this.#widgetHovering = intersection.Widget;
4140
- this.#cursor = "pointer" /* Pointer */;
4712
+ this.#widgetHoveringBox = intersection.WidgetBox ?? null;
4713
+ this.#cursor = intersection.Widget.Disabled?.() ? "default" /* Default */ : intersection.Widget.Cursor?.() ?? "pointer" /* Pointer */;
4141
4714
  }
4142
4715
  if (intersection.Port !== void 0 && intersection.Node !== void 0 && intersection.PortIndex !== void 0 && intersection.PortIsInput !== void 0) {
4143
4716
  this.#portHovering = {
@@ -4160,6 +4733,9 @@ var NodeSubsystem2 = class {
4160
4733
  }
4161
4734
  this.#nodes[i].render(ctx, camera, state, mousePosition, this.#postProcessPass);
4162
4735
  }
4736
+ if (this.#widgetCurrentlyClicking !== null) {
4737
+ this.#cursor = this.#widgetCurrentlyClicking.Cursor?.() ?? "pointer" /* Pointer */;
4738
+ }
4163
4739
  }
4164
4740
  #boxSelectionScreenspaceBox(camera) {
4165
4741
  const box = { Position: Zero(), Size: Zero() };
@@ -4168,29 +4744,174 @@ var NodeSubsystem2 = class {
4168
4744
  SubVector2(box.Size, box.Size, box.Position);
4169
4745
  return box;
4170
4746
  }
4747
+ /**
4748
+ * Where a wire being dropped on the hovered array input would go, or
4749
+ * undefined when it is not over one.
4750
+ */
4751
+ #insertIndex() {
4752
+ const hovering = this.#portHovering;
4753
+ if (hovering === null || !hovering.InputPort || this.#lastMousePosition === void 0 || hovering.Port.getPortType() !== "INPUTARRAY" /* InputArray */) {
4754
+ return void 0;
4755
+ }
4756
+ const box = hovering.Node.inputPortPosition(hovering.Index);
4757
+ return arrayInsertIndex(box, hovering.Port.connections().length, this.#lastMousePosition.y);
4758
+ }
4759
+ #renderInsertMarker(ctx, camera) {
4760
+ const hovering = this.#portHovering;
4761
+ if (this.#connectionSelected === null || this.#connectionSelected.inPort() !== null || hovering === null) {
4762
+ return;
4763
+ }
4764
+ const index = this.#insertIndex();
4765
+ const count = hovering.Port.connections().length;
4766
+ if (index === void 0 || count === 0) {
4767
+ return;
4768
+ }
4769
+ const box = hovering.Node.inputPortPosition(hovering.Index);
4770
+ const y = box.Position.y + index * box.Size.y / count;
4771
+ const overhang = 6 * camera.zoom;
4772
+ ctx.save();
4773
+ ctx.strokeStyle = "#ffffff";
4774
+ ctx.lineWidth = Math.max(1, 2 * camera.zoom);
4775
+ ctx.beginPath();
4776
+ ctx.moveTo(box.Position.x - overhang, y);
4777
+ ctx.lineTo(box.Position.x + box.Size.x + overhang, y);
4778
+ ctx.stroke();
4779
+ ctx.restore();
4780
+ }
4171
4781
  render(ctx, camera, mousePosition) {
4172
4782
  this.#lastCtx = ctx;
4783
+ this.#lastMousePosition = mousePosition;
4173
4784
  this.#cursor = "default" /* Default */;
4174
4785
  const awaitingLayout = this.#nodes.some((node) => !node.hasBeenLaidOut());
4786
+ for (const node of this.#nodes) {
4787
+ if (node.hasBeenLaidOut()) {
4788
+ node.placePorts(node.calculateBounds(ctx, camera), camera.zoom);
4789
+ }
4790
+ }
4175
4791
  TimeExecution("Render_Connections", () => {
4176
4792
  this.#renderConnections(ctx, camera, mousePosition);
4177
4793
  });
4178
4794
  TimeExecution("Render_Nodes", () => {
4179
4795
  this.#renderNodes(ctx, camera, mousePosition);
4180
4796
  });
4797
+ this.#renderInsertMarker(ctx, camera);
4181
4798
  if (awaitingLayout) {
4182
4799
  requestRender();
4183
4800
  }
4184
4801
  if (this.#boxSelect) {
4185
4802
  const box = this.#boxSelectionScreenspaceBox(camera);
4186
4803
  ctx.setLineDash([Theme.BoxSelect.LineDashLength]);
4187
- this.#boxSelectStyle.Draw(ctx, box, 1);
4804
+ new BoxStyle({
4805
+ border: { color: Theme.BoxSelect.Color, size: Theme.BoxSelect.Size },
4806
+ color: "rgba(0,0,0,0)",
4807
+ radius: Theme.BoxSelect.Radius
4808
+ }).Draw(ctx, box, 1);
4188
4809
  ctx.setLineDash([]);
4189
4810
  }
4190
4811
  return { cursorStyle: this.#cursor };
4191
4812
  }
4192
4813
  };
4193
4814
 
4815
+ // src/popup.ts
4816
+ var Popup = class {
4817
+ #title;
4818
+ #options;
4819
+ #content;
4820
+ #onClose;
4821
+ // Styling ================================================================
4822
+ #buttonCSS;
4823
+ // Runtime ================================================================
4824
+ #popup;
4825
+ #keyHandler;
4826
+ constructor(config) {
4827
+ this.#title = config.title;
4828
+ this.#options = config.options;
4829
+ this.#content = config.content;
4830
+ this.#onClose = config.onClose;
4831
+ this.#buttonCSS = config.buttonCSS === void 0 ? "flex-grow: 1; margin-right: 8px;" : config.buttonCSS;
4832
+ this.#popup = null;
4833
+ this.#keyHandler = null;
4834
+ }
4835
+ Hide() {
4836
+ if (this.#popup === null) {
4837
+ return;
4838
+ }
4839
+ if (this.#keyHandler !== null) {
4840
+ document.removeEventListener("keydown", this.#keyHandler, true);
4841
+ this.#keyHandler = null;
4842
+ }
4843
+ document.body.removeChild(this.#popup);
4844
+ this.#popup = null;
4845
+ }
4846
+ #close(option) {
4847
+ this.Hide();
4848
+ if (this.#onClose !== void 0) {
4849
+ this.#onClose(option);
4850
+ }
4851
+ }
4852
+ Show() {
4853
+ this.#popup = document.createElement("div");
4854
+ 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};`;
4855
+ document.body.appendChild(this.#popup);
4856
+ const container = document.createElement("div");
4857
+ 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;`;
4858
+ this.#popup.appendChild(container);
4859
+ const title = document.createElement("h2");
4860
+ title.textContent = this.#title;
4861
+ title.style.cssText = "margin-top:0;margin-bottom:16px;";
4862
+ container.appendChild(title);
4863
+ if (this.#content !== void 0) {
4864
+ container.appendChild(this.#content());
4865
+ }
4866
+ const buttonContainer = document.createElement("div");
4867
+ buttonContainer.style.cssText = "margin-top:16px;justify-content:space-around;align-items:center;flex-direction:row;display:flex;";
4868
+ container.appendChild(buttonContainer);
4869
+ for (let i = 0; i < this.#options.length; i++) {
4870
+ const button = document.createElement("button");
4871
+ button.style.cssText = this.#buttonCSS;
4872
+ button.textContent = this.#options[i];
4873
+ button.onclick = () => this.#close(this.#options[i]);
4874
+ buttonContainer.appendChild(button);
4875
+ }
4876
+ this.#keyHandler = (e) => {
4877
+ if (e.key === "Enter") {
4878
+ e.preventDefault();
4879
+ this.#close(this.#options.length > 0 ? this.#options[0] : null);
4880
+ } else if (e.key === "Escape") {
4881
+ e.preventDefault();
4882
+ this.#close(null);
4883
+ }
4884
+ e.stopPropagation();
4885
+ };
4886
+ document.addEventListener("keydown", this.#keyHandler, true);
4887
+ }
4888
+ };
4889
+
4890
+ // src/popups/string.ts
4891
+ function SetStringPopup(config) {
4892
+ let input = null;
4893
+ return new Popup({
4894
+ title: config.title,
4895
+ options: ["Set", "Cancel"],
4896
+ content: () => {
4897
+ const container = document.createElement("div");
4898
+ input = document.createElement("input");
4899
+ input.value = config.startingValue;
4900
+ container.append(input);
4901
+ return container;
4902
+ },
4903
+ onClose: (button) => {
4904
+ if (button !== "Set" || input === null) {
4905
+ if (config.onCancel) {
4906
+ config.onCancel();
4907
+ }
4908
+ return;
4909
+ }
4910
+ config.onUpdate(input.value);
4911
+ }
4912
+ });
4913
+ }
4914
+
4194
4915
  // src/popups/form.ts
4195
4916
  function FormPopup(config) {
4196
4917
  let input = new Array();
@@ -4307,9 +5028,11 @@ var FlowNode = class {
4307
5028
  #titleChangeCallback;
4308
5029
  #infoChangeCallback;
4309
5030
  // Styling ================================================================
5031
+ #styleConfig;
5032
+ #styledForTheme = -1;
4310
5033
  #titleColor;
4311
5034
  #selectedStyle;
4312
- #stateStyles;
5035
+ #stateStyles = /* @__PURE__ */ new Map();
4313
5036
  #padding;
4314
5037
  #portTextStyle;
4315
5038
  #elementSpacing;
@@ -4321,6 +5044,7 @@ var FlowNode = class {
4321
5044
  #data;
4322
5045
  #registeredAnyPropertyChangeCallbacks;
4323
5046
  #registeredPropertyChangeCallbacks;
5047
+ #registeredPropertyEditCallbacks = /* @__PURE__ */ new Map();
4324
5048
  constructor(config) {
4325
5049
  this.#input = new Array();
4326
5050
  this.#output = new Array();
@@ -4373,58 +5097,17 @@ var FlowNode = class {
4373
5097
  this.#position = config?.position === void 0 ? { x: 0, y: 0 } : config.position;
4374
5098
  this.#title = new Text(
4375
5099
  config?.title === void 0 ? "" : config.title,
4376
- TextStyleFallback(config?.style?.title?.textStyle, {
4377
- size: 16,
4378
- weight: "bold" /* Bold */,
4379
- color: Theme.Node.FontColor
4380
- })
5100
+ TextStyleFallback(config?.style?.title?.textStyle, { size: 16, weight: "bold" /* Bold */ })
4381
5101
  );
4382
5102
  this.#subTitle = new Text(
4383
5103
  config?.subTitle === void 0 ? "" : config.subTitle,
4384
- TextStyleFallback(config?.style?.title?.textStyle, {
4385
- size: 10,
4386
- weight: "bold" /* Bold */,
4387
- color: Theme.Node.FontColor
4388
- })
4389
- );
4390
- this.#infoSymbol = new Text(
4391
- "i",
4392
- {
4393
- size: 13,
4394
- weight: "bold" /* Bold */,
4395
- color: Theme.Node.FontColor
4396
- }
5104
+ TextStyleFallback(config?.style?.title?.textStyle, { size: 10, weight: "bold" /* Bold */ })
4397
5105
  );
5106
+ this.#infoSymbol = new Text("i", { size: 13, weight: "bold" /* Bold */ });
4398
5107
  this.#infoText = config?.info ? config?.info : "";
4399
- this.#titleColor = config?.style?.title?.color === void 0 ? "#154050" : config?.style?.title?.color;
4400
5108
  this.#padding = config?.style?.title?.padding === void 0 ? 15 : config?.style?.title?.padding;
4401
- this.#stateStyles = /* @__PURE__ */ new Map();
4402
- this.#stateStyles.set(0 /* Idle */, new BoxStyle(BoxStyleWithFallback(config?.style?.idle, {
4403
- border: { color: Theme.Node.Border.Idle, size: 1 },
4404
- radius: Theme.Node.BorderRadius,
4405
- color: Theme.Node.BackgroundColor
4406
- })));
4407
- this.#stateStyles.set(1 /* MouseOver */, new BoxStyle(BoxStyleWithFallback(config?.style?.mouseOver, {
4408
- border: { color: Theme.Node.Border.MouseOver, size: 1.1 },
4409
- radius: Theme.Node.BorderRadius,
4410
- color: Theme.Node.BackgroundColor
4411
- })));
4412
- this.#stateStyles.set(2 /* Grabbed */, new BoxStyle(BoxStyleWithFallback(config?.style?.grabbed, {
4413
- border: { color: Theme.Node.Border.Grabbed, size: 2 },
4414
- radius: Theme.Node.BorderRadius,
4415
- color: Theme.Node.BackgroundColor
4416
- })));
4417
- this.#selectedStyle = new BoxStyle(BoxStyleWithFallback(config?.style?.selected, {
4418
- border: { color: Theme.Node.Border.Selected, size: 1 },
4419
- radius: Theme.Node.BorderRadius,
4420
- color: Theme.Node.BackgroundColor
4421
- }));
4422
- this.#portTextStyle = new TextStyle({
4423
- size: config?.style?.portText?.size === void 0 ? 14 : config?.style?.portText?.size,
4424
- color: config?.style?.portText?.color === void 0 ? Theme.Node.Port.FontColor : config?.style?.portText?.color,
4425
- font: config?.style?.portText?.font,
4426
- weight: config?.style?.portText?.weight
4427
- });
5109
+ this.#styleConfig = config?.style;
5110
+ this.#applyStyle();
4428
5111
  if (config?.inputs !== void 0) {
4429
5112
  for (let i = 0; i < config.inputs.length; i++) {
4430
5113
  this.addInput(config.inputs[i]);
@@ -4436,12 +5119,8 @@ var FlowNode = class {
4436
5119
  }
4437
5120
  }
4438
5121
  if (config?.widgets !== void 0) {
4439
- for (let i = 0; i < config.widgets.length; i++) {
4440
- const widget = config.widgets[i];
4441
- if (widget.type === void 0) {
4442
- continue;
4443
- }
4444
- this.addWidget(GlobalWidgetFactory.create(this, widget.type, widget.config));
5122
+ for (const widget of config.widgets) {
5123
+ this.addWidget(GlobalWidgetFactory.createFromConfig(this, widget));
4445
5124
  }
4446
5125
  }
4447
5126
  }
@@ -4498,7 +5177,7 @@ var FlowNode = class {
4498
5177
  }
4499
5178
  setProperty(name, value) {
4500
5179
  const oldValue = this.#data[name];
4501
- if (oldValue === value) {
5180
+ if (Object.is(oldValue, value)) {
4502
5181
  return;
4503
5182
  }
4504
5183
  this.#data[name] = value;
@@ -4514,6 +5193,32 @@ var FlowNode = class {
4514
5193
  callbacks[i](oldValue, value);
4515
5194
  }
4516
5195
  }
5196
+ /**
5197
+ * Listens for a person changing the property through a widget, but not
5198
+ * for the property being set from code.
5199
+ */
5200
+ addPropertyEditListener(name, callback) {
5201
+ let callbacks = this.#registeredPropertyEditCallbacks.get(name);
5202
+ if (callbacks === void 0) {
5203
+ callbacks = [];
5204
+ this.#registeredPropertyEditCallbacks.set(name, callbacks);
5205
+ }
5206
+ callbacks.push(callback);
5207
+ }
5208
+ /**
5209
+ * Sets the property as a person's finished edit, so edit listeners hear
5210
+ * it even when the value was already showing.
5211
+ */
5212
+ editProperty(name, value) {
5213
+ this.setProperty(name, value);
5214
+ const callbacks = this.#registeredPropertyEditCallbacks.get(name);
5215
+ if (callbacks === void 0) {
5216
+ return;
5217
+ }
5218
+ for (const callback of [...callbacks]) {
5219
+ callback(value);
5220
+ }
5221
+ }
4517
5222
  getProperty(name) {
4518
5223
  return this.#data[name];
4519
5224
  }
@@ -4802,6 +5507,7 @@ var FlowNode = class {
4802
5507
  // return this.#titleTextStyle.measure(ctx, scale, this.#title);
4803
5508
  // }
4804
5509
  calculateBounds(ctx, camera) {
5510
+ this.#followTheme();
4805
5511
  const tempMeasurement = Zero();
4806
5512
  const doublePadding = this.#padding * 2;
4807
5513
  const screenSpacePosition = Zero();
@@ -4815,25 +5521,21 @@ var FlowNode = class {
4815
5521
  size.x += size.y * 4;
4816
5522
  }
4817
5523
  size.x += doublePadding;
4818
- size.y += doublePadding + this.#elementSpacing * this.#input.length;
4819
- for (let i = 0; i < this.#input.length; i++) {
4820
- const port = this.#input[i];
4821
- this.#portTextStyle.measure(ctx, 1, port.getDisplayName(), tempMeasurement);
4822
- size.y += tempMeasurement.y;
4823
- size.x = Math.max(size.x, tempMeasurement.x + doublePadding);
4824
- }
4825
- size.y += this.#elementSpacing * this.#output.length;
4826
- for (let i = 0; i < this.#output.length; i++) {
4827
- const port = this.#output[i];
5524
+ size.y += doublePadding;
5525
+ for (const port of [...this.#input, ...this.#output]) {
5526
+ if (port.hidden()) {
5527
+ continue;
5528
+ }
4828
5529
  this.#portTextStyle.measure(ctx, 1, port.getDisplayName(), tempMeasurement);
4829
- size.y += tempMeasurement.y;
5530
+ size.y += this.#portRowHeight(port, tempMeasurement.y) + this.#elementSpacing;
4830
5531
  size.x = Math.max(size.x, tempMeasurement.x + doublePadding);
4831
5532
  }
4832
- size.y += this.#elementSpacing * this.#widgets.length;
4833
- for (let i = 0; i < this.#widgets.length; i++) {
4834
- const element = this.#widgets[i];
4835
- const eleSize = element.Size();
4836
- size.y += eleSize.y;
5533
+ for (const widget of this.#widgets) {
5534
+ if (widget.Hidden?.()) {
5535
+ continue;
5536
+ }
5537
+ const eleSize = widget.Size();
5538
+ size.y += eleSize.y + this.#elementSpacing;
4837
5539
  size.x = Math.max(size.x, eleSize.x + doublePadding);
4838
5540
  }
4839
5541
  size.y += this.#elementSpacing;
@@ -4917,7 +5619,7 @@ var FlowNode = class {
4917
5619
  intersection.Node = this;
4918
5620
  }
4919
5621
  for (let i = 0; i < this.#inputPortPositions.Count(); i++) {
4920
- if (InBox(this.#inputPortPositions.At(i), position)) {
5622
+ if (!this.#input[i].hidden() && InBox(this.#inputPortPositions.At(i), position)) {
4921
5623
  intersection.Node = this;
4922
5624
  intersection.PortIndex = i;
4923
5625
  intersection.PortIsInput = true;
@@ -4925,7 +5627,7 @@ var FlowNode = class {
4925
5627
  }
4926
5628
  }
4927
5629
  for (let i = 0; i < this.#outputPortPositions.Count(); i++) {
4928
- if (InBox(this.#outputPortPositions.At(i), position)) {
5630
+ if (!this.#output[i].hidden() && InBox(this.#outputPortPositions.At(i), position)) {
4929
5631
  intersection.Node = this;
4930
5632
  intersection.PortIndex = i;
4931
5633
  intersection.PortIsInput = false;
@@ -4933,10 +5635,12 @@ var FlowNode = class {
4933
5635
  }
4934
5636
  }
4935
5637
  for (let i = 0; i < this.#widgetPositions.Count(); i++) {
4936
- if (InBox(this.#widgetPositions.At(i), position)) {
5638
+ const widget = this.#widgets[i];
5639
+ if (widget !== void 0 && !widget.Hidden?.() && InBox(this.#widgetPositions.At(i), position)) {
4937
5640
  intersection.Node = this;
4938
5641
  intersection.WidgetIndex = i;
4939
- intersection.Widget = this.#widgets[i];
5642
+ intersection.Widget = widget;
5643
+ intersection.WidgetBox = this.#widgetPositions.At(i);
4940
5644
  }
4941
5645
  }
4942
5646
  return intersection;
@@ -4951,10 +5655,6 @@ var FlowNode = class {
4951
5655
  return this.#title.get();
4952
5656
  }
4953
5657
  setTitle(newTitle) {
4954
- if (!this.#canEditTitle) {
4955
- console.warn("setTitle instruction ignored, as node has been marked un-editable");
4956
- return;
4957
- }
4958
5658
  let cleaned = newTitle;
4959
5659
  if (cleaned === null || cleaned === void 0) {
4960
5660
  cleaned = "";
@@ -4971,9 +5671,6 @@ var FlowNode = class {
4971
5671
  }
4972
5672
  }
4973
5673
  setInfo(newInfo) {
4974
- if (!this.#canEditInfo) {
4975
- console.warn("setInfo instruction ignored, as node has been marked un-editable");
4976
- }
4977
5674
  let cleaned = newInfo;
4978
5675
  if (cleaned === null || cleaned === void 0) {
4979
5676
  cleaned = "";
@@ -4992,6 +5689,12 @@ var FlowNode = class {
4992
5689
  inputPortPosition(index) {
4993
5690
  return this.#inputPortPositions.At(index);
4994
5691
  }
5692
+ widgetPosition(index) {
5693
+ if (index < 0 || index >= this.#widgetPositions.Count()) {
5694
+ return void 0;
5695
+ }
5696
+ return this.#widgetPositions.At(index);
5697
+ }
4995
5698
  inputPort(index) {
4996
5699
  return this.#input[index];
4997
5700
  }
@@ -5007,6 +5710,35 @@ var FlowNode = class {
5007
5710
  outputs() {
5008
5711
  return this.#output.length;
5009
5712
  }
5713
+ #applyStyle() {
5714
+ const style = this.#styleConfig;
5715
+ const titleText = style?.title?.textStyle?.color ?? Theme.Node.Title.FontColor;
5716
+ this.#title.style().setColor(titleText);
5717
+ this.#subTitle.style().setColor(titleText);
5718
+ this.#infoSymbol.style().setColor(titleText);
5719
+ this.#titleColor = style?.title?.color ?? Theme.Node.Title.Color;
5720
+ const box = (config, border, size) => new BoxStyle(BoxStyleWithFallback(config, {
5721
+ border: { color: border, size },
5722
+ radius: Theme.Node.BorderRadius,
5723
+ color: Theme.Node.BackgroundColor
5724
+ }));
5725
+ this.#stateStyles.set(0 /* Idle */, box(style?.idle, Theme.Node.Border.Idle, 1));
5726
+ this.#stateStyles.set(1 /* MouseOver */, box(style?.mouseOver, Theme.Node.Border.MouseOver, 1.1));
5727
+ this.#stateStyles.set(2 /* Grabbed */, box(style?.grabbed, Theme.Node.Border.Grabbed, 2));
5728
+ this.#selectedStyle = box(style?.selected, Theme.Node.Border.Selected, 1);
5729
+ this.#portTextStyle = new TextStyle({
5730
+ size: style?.portText?.size ?? 14,
5731
+ color: style?.portText?.color ?? Theme.Node.Port.FontColor,
5732
+ font: style?.portText?.font,
5733
+ weight: style?.portText?.weight
5734
+ });
5735
+ this.#styledForTheme = themeVersion();
5736
+ }
5737
+ #followTheme() {
5738
+ if (this.#styledForTheme !== themeVersion()) {
5739
+ this.#applyStyle();
5740
+ }
5741
+ }
5010
5742
  #calculateStyle(state) {
5011
5743
  if (this.#selected && state === 0 /* Idle */) {
5012
5744
  return this.#selectedStyle;
@@ -5023,9 +5755,22 @@ var FlowNode = class {
5023
5755
  clearMessages() {
5024
5756
  this.#messages = [];
5025
5757
  }
5758
+ #portRowHeight(port, textHeight) {
5759
+ const slots = port.slots();
5760
+ return slots > 1 ? Math.max(textHeight, slots * ARRAY_SLOT_PITCH) : textHeight;
5761
+ }
5026
5762
  hasBeenLaidOut() {
5027
5763
  return this.#laidOut;
5028
5764
  }
5765
+ /**
5766
+ * Throws away where the ports were last placed, so wires wait for the
5767
+ * next draw instead of pointing at the old spots.
5768
+ */
5769
+ invalidateLayout() {
5770
+ this.#laidOut = false;
5771
+ this.#portOffsets = { input: [], output: [] };
5772
+ requestRender();
5773
+ }
5029
5774
  #laidOut = false;
5030
5775
  render(ctx, camera, state, mousePosition, postProcess) {
5031
5776
  this.#laidOut = true;
@@ -5063,6 +5808,9 @@ var FlowNode = class {
5063
5808
  [nodeStyle.radius() * camera.zoom, nodeStyle.radius() * camera.zoom, 0, 0]
5064
5809
  );
5065
5810
  ctx.fill();
5811
+ if (Theme.Node.Bevel !== null) {
5812
+ drawBevel(ctx, nodeBounds, camera.zoom, Theme.Node.Bevel, false);
5813
+ }
5066
5814
  const titlePosition = VectorPool.get();
5067
5815
  titlePosition.x = nodeBounds.Position.x + nodeBounds.Size.x / 2;
5068
5816
  titlePosition.y = nodeBounds.Position.y + scaledPadding + titleSize.y / 2;
@@ -5123,8 +5871,12 @@ var FlowNode = class {
5123
5871
  for (let i = 0; i < this.#input.length; i++) {
5124
5872
  ctx.textAlign = "left" /* Left */;
5125
5873
  const port = this.#input[i];
5874
+ if (port.hidden()) {
5875
+ this.#inputPortPositions.Push({ Position: { x: nodeBounds.Position.x, y: startY }, Size: { x: 0, y: 0 } });
5876
+ continue;
5877
+ }
5126
5878
  this.#portTextStyle.measure(ctx, 1, port.getDisplayName(), tempMeasurement);
5127
- const portHeight = tempMeasurement.y * camera.zoom;
5879
+ const portHeight = this.#portRowHeight(port, tempMeasurement.y) * camera.zoom;
5128
5880
  const position = VectorPool.get();
5129
5881
  position.x = nodeBounds.Position.x;
5130
5882
  position.y = startY + portHeight / 2;
@@ -5137,6 +5889,10 @@ var FlowNode = class {
5137
5889
  ctx.textAlign = "right" /* Right */;
5138
5890
  for (let i = 0; i < this.#output.length; i++) {
5139
5891
  const port = this.#output[i];
5892
+ if (port.hidden()) {
5893
+ this.#outputPortPositions.Push({ Position: { x: rightSide, y: startY }, Size: { x: 0, y: 0 } });
5894
+ continue;
5895
+ }
5140
5896
  this.#portTextStyle.measure(ctx, 1, port.getDisplayName(), tempMeasurement);
5141
5897
  const portHeight = tempMeasurement.y * camera.zoom;
5142
5898
  const position = VectorPool.get();
@@ -5149,13 +5905,18 @@ var FlowNode = class {
5149
5905
  }
5150
5906
  for (let i = 0; i < this.#widgets.length; i++) {
5151
5907
  const widget = this.#widgets[i];
5908
+ if (widget.Hidden?.()) {
5909
+ this.#widgetPositions.Push({ Position: { x: nodeBounds.Position.x, y: startY }, Size: { x: 0, y: 0 } });
5910
+ continue;
5911
+ }
5152
5912
  const widgetSize = widget.Size();
5153
- const scaledWidgetWidth = widgetSize.x * camera.zoom;
5154
- const position = VectorPool.get();
5155
- position.x = nodeBounds.Position.x + (nodeBounds.Size.x - scaledWidgetWidth) / 2;
5156
- position.y = startY;
5157
- this.#widgetPositions.Push(widget.Draw(ctx, position, camera.zoom, mousePosition));
5158
- startY += widgetSize.y * camera.zoom + scaledElementSpacing;
5913
+ const box = {
5914
+ Position: { x: nodeBounds.Position.x + scaledPadding, y: startY },
5915
+ Size: { x: nodeBounds.Size.x - scaledPadding * 2, y: widgetSize.y * camera.zoom }
5916
+ };
5917
+ widget.Draw(ctx, box, camera.zoom, mousePosition);
5918
+ this.#widgetPositions.Push(box);
5919
+ startY += box.Size.y + scaledElementSpacing;
5159
5920
  }
5160
5921
  const messageStart = VectorPool.get();
5161
5922
  messageStart.x = nodeBounds.Position.x + nodeBounds.Size.x / 2;
@@ -5192,6 +5953,13 @@ var FlowNode = class {
5192
5953
  record(this.#outputPortPositions, this.#portOffsets.output);
5193
5954
  }
5194
5955
  layoutPortsWithoutDrawing(bounds, zoom) {
5956
+ if (!this.placePorts(bounds, zoom)) {
5957
+ return false;
5958
+ }
5959
+ this.#widgetPositions.Clear();
5960
+ return true;
5961
+ }
5962
+ placePorts(bounds, zoom) {
5195
5963
  if (this.#portOffsets.input.length === 0 && this.#portOffsets.output.length === 0) {
5196
5964
  return false;
5197
5965
  }
@@ -5213,7 +5981,6 @@ var FlowNode = class {
5213
5981
  };
5214
5982
  project(this.#portOffsets.input, this.#inputPortPositions);
5215
5983
  project(this.#portOffsets.output, this.#outputPortPositions);
5216
- this.#widgetPositions.Clear();
5217
5984
  return true;
5218
5985
  }
5219
5986
  };
@@ -5405,33 +6172,33 @@ var CodeBlockEntry = class {
5405
6172
  }
5406
6173
  render(ctx, position, scale, maxWidth) {
5407
6174
  this.#calculateLayout(ctx, maxWidth);
5408
- let padding = Theme.Note.CodeBlock.Padding * scale;
5409
- let max = 0;
5410
- for (let i = 0; i < this.#calculatedEntries.Count(); i++) {
5411
- const pos = this.#calculatedPositions.At(i);
5412
- max = Math.max(max, pos.y * scale);
5413
- }
5414
- const y = position.y + max + scale * 5;
6175
+ const padding = Theme.Note.CodeBlock.Padding * scale;
6176
+ const lineCount = this.#calculatedEntries.Count();
6177
+ const lastTop = lineCount === 0 ? 0 : this.#calculatedPositions.At(lineCount - 1).y;
6178
+ const contentHeight = lineCount === 0 ? 0 : (lastTop + this.#text.style().getSize()) * scale;
6179
+ const height2 = contentHeight + padding * 2;
5415
6180
  ctx.fillStyle = Theme.Note.CodeBlock.BackgroundColor;
5416
6181
  ctx.beginPath();
5417
6182
  ctx.roundRect(
5418
6183
  position.x,
5419
6184
  position.y,
5420
6185
  maxWidth * scale,
5421
- max + padding * 2,
6186
+ height2,
5422
6187
  Theme.Note.CodeBlock.BorderRadius * scale
5423
6188
  );
5424
6189
  ctx.fill();
5425
- for (let i = 0; i < this.#calculatedEntries.Count(); i++) {
6190
+ const baseline = ctx.textBaseline;
6191
+ ctx.textBaseline = "top";
6192
+ for (let i = 0; i < lineCount; i++) {
5426
6193
  const entry = this.#calculatedEntries.At(i);
5427
6194
  const pos = this.#calculatedPositions.At(i);
5428
6195
  entry.render(ctx, scale, {
5429
6196
  x: pos.x * scale + position.x + padding,
5430
6197
  y: pos.y * scale + position.y + padding
5431
6198
  });
5432
- max = Math.max(max, pos.y * scale);
5433
6199
  }
5434
- return max + padding * 2;
6200
+ ctx.textBaseline = baseline;
6201
+ return height2;
5435
6202
  }
5436
6203
  };
5437
6204
  var UnorderedListMarkdownEntry = class {
@@ -5809,9 +6576,10 @@ var MarkdownSyntaxParser = class {
5809
6576
  this.#inc();
5810
6577
  this.#inc();
5811
6578
  this.#inc();
5812
- const codeBlockText = this.#originalText.substring(start, end);
6579
+ const codeBlockText = this.#originalText.substring(start, end).replace(/\r?\n$/, "");
5813
6580
  const entryText = new Text(codeBlockText);
5814
6581
  this.#assignStandardStyling([entryText]);
6582
+ entryText.setColor(Theme.Note.CodeBlock.FontColor);
5815
6583
  return new CodeBlockEntry(entryText);
5816
6584
  }
5817
6585
  parse() {
@@ -5866,6 +6634,7 @@ var BOX_SIZE = 10;
5866
6634
  var FlowNote = class {
5867
6635
  #originalText;
5868
6636
  #document;
6637
+ #parsedForTheme = -1;
5869
6638
  #width;
5870
6639
  #edittingStyle;
5871
6640
  // Runtime ================================================================
@@ -5908,6 +6677,7 @@ var FlowNote = class {
5908
6677
  setText(text) {
5909
6678
  this.#originalText = text;
5910
6679
  this.#document = BuildMarkdown(this.#originalText);
6680
+ this.#parsedForTheme = themeVersion();
5911
6681
  for (let i = 0; i < this.#contentChangeCallbacks.length; i++) {
5912
6682
  this.#contentChangeCallbacks[i](this, text);
5913
6683
  }
@@ -5995,6 +6765,10 @@ var FlowNote = class {
5995
6765
  }
5996
6766
  camera.graphSpaceToScreenSpace(this.#position, this.#tempPosition);
5997
6767
  CopyVector2(this.#lastRenderedBox.Position, this.#tempPosition);
6768
+ if (this.#parsedForTheme !== themeVersion()) {
6769
+ this.#document = BuildMarkdown(this.#originalText);
6770
+ this.#parsedForTheme = themeVersion();
6771
+ }
5998
6772
  const startY = this.#tempPosition.y;
5999
6773
  const lineSpacing = Theme.Note.EntrySpacing * camera.zoom;
6000
6774
  ctx.textAlign = "left" /* Left */;
@@ -6690,13 +7464,15 @@ var Minimap = class {
6690
7464
  // src/graph.ts
6691
7465
  function BuildBackgroundRenderer(backgroundColor) {
6692
7466
  return (canvas, context, position, scale) => {
6693
- context.fillStyle = backgroundColor;
7467
+ context.fillStyle = backgroundColor ?? Theme.Graph.BackgroundColor;
6694
7468
  context.fillRect(0, 0, canvas.width, canvas.height);
6695
- const alpha = Math.round(Clamp01(scale - 0.3) * 255);
7469
+ const alpha = Clamp01(scale - 0.3);
6696
7470
  if (alpha <= 0) {
6697
7471
  return;
6698
7472
  }
6699
- context.fillStyle = `rgba(41, 54, 57, ${alpha})`;
7473
+ context.save();
7474
+ context.globalAlpha = alpha;
7475
+ context.fillStyle = Theme.Graph.DotColor;
6700
7476
  const spacing = 100;
6701
7477
  const pi2 = 2 * Math.PI;
6702
7478
  const dotScale = 2 * scale;
@@ -6709,6 +7485,7 @@ function BuildBackgroundRenderer(backgroundColor) {
6709
7485
  context.fill();
6710
7486
  }
6711
7487
  }
7488
+ context.restore();
6712
7489
  };
6713
7490
  }
6714
7491
  var contextMenuGroup = "graph-context-menu";
@@ -6759,9 +7536,9 @@ var GraphView = class {
6759
7536
  const results = {};
6760
7537
  for (let i = 0; i < this.#subsystems.length; i++) {
6761
7538
  TimeExecution("Render_Subsystem_" + i, () => {
6762
- let results2 = this.#subsystems[i].render(ctx, camera, mousePosition);
6763
- if (results2?.cursorStyle) {
6764
- results2.cursorStyle = results2?.cursorStyle;
7539
+ const layer = this.#subsystems[i].render(ctx, camera, mousePosition);
7540
+ if (layer?.cursorStyle) {
7541
+ results.cursorStyle = layer.cursorStyle;
6765
7542
  }
6766
7543
  });
6767
7544
  }
@@ -6821,8 +7598,7 @@ var NodeFlowGraph = class {
6821
7598
  if (config?.backgroundRenderer !== void 0) {
6822
7599
  this.#backgroundRenderer = config?.backgroundRenderer;
6823
7600
  } else {
6824
- const backgroundColor = config?.backgroundColor === void 0 ? Theme.Graph.BackgroundColor : config.backgroundColor;
6825
- this.#backgroundRenderer = BuildBackgroundRenderer(backgroundColor);
7601
+ this.#backgroundRenderer = BuildBackgroundRenderer(config?.backgroundColor);
6826
7602
  }
6827
7603
  this.#minimap = new Minimap(config?.minimap);
6828
7604
  this.#mainNodeSubsystem.setConnectionReleasedHandler((dangling) => {
@@ -7288,11 +8064,15 @@ export {
7288
8064
  ColorWidget,
7289
8065
  Connection,
7290
8066
  ContextMenuItemState,
8067
+ CursorStyle,
7291
8068
  DefaultConnectionRenderer,
8069
+ DefaultTheme,
7292
8070
  FlowNode,
7293
8071
  FlowNote,
7294
8072
  GlobalWidgetFactory,
8073
+ GraphView,
7295
8074
  ImageWidget,
8075
+ InBox,
7296
8076
  MessageType,
7297
8077
  Minimap,
7298
8078
  NodeFactory,
@@ -7302,16 +8082,28 @@ export {
7302
8082
  Port,
7303
8083
  PortType,
7304
8084
  Publisher,
8085
+ RowWidget,
7305
8086
  SliderWidget,
7306
8087
  StringWidget,
7307
8088
  TextWidget,
7308
8089
  Theme,
7309
8090
  ToggleWidget,
8091
+ ValueWidget,
8092
+ WidgetBase,
7310
8093
  WidgetFactory,
8094
+ drawField,
8095
+ drawFieldBox,
8096
+ drawFieldText,
8097
+ editInPlace,
8098
+ formatNumber,
8099
+ onThemeChange,
7311
8100
  portConfigTypes,
7312
8101
  portsCompatible,
7313
8102
  renderContinuously,
7314
8103
  requestRender,
7315
- typeSetsCompatible
8104
+ setTheme,
8105
+ themeVersion,
8106
+ typeSetsCompatible,
8107
+ widgetFont
7316
8108
  };
7317
8109
  //# sourceMappingURL=index.mjs.map