@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.js CHANGED
@@ -24,11 +24,15 @@ __export(index_exports, {
24
24
  ColorWidget: () => ColorWidget,
25
25
  Connection: () => Connection,
26
26
  ContextMenuItemState: () => ContextMenuItemState,
27
+ CursorStyle: () => CursorStyle,
27
28
  DefaultConnectionRenderer: () => DefaultConnectionRenderer,
29
+ DefaultTheme: () => DefaultTheme,
28
30
  FlowNode: () => FlowNode,
29
31
  FlowNote: () => FlowNote,
30
32
  GlobalWidgetFactory: () => GlobalWidgetFactory,
33
+ GraphView: () => GraphView,
31
34
  ImageWidget: () => ImageWidget,
35
+ InBox: () => InBox,
32
36
  MessageType: () => MessageType,
33
37
  Minimap: () => Minimap,
34
38
  NodeFactory: () => NodeFactory,
@@ -38,25 +42,75 @@ __export(index_exports, {
38
42
  Port: () => Port,
39
43
  PortType: () => PortType,
40
44
  Publisher: () => Publisher,
45
+ RowWidget: () => RowWidget,
41
46
  SliderWidget: () => SliderWidget,
42
47
  StringWidget: () => StringWidget,
43
48
  TextWidget: () => TextWidget,
44
49
  Theme: () => Theme,
45
50
  ToggleWidget: () => ToggleWidget,
51
+ ValueWidget: () => ValueWidget,
52
+ WidgetBase: () => WidgetBase,
46
53
  WidgetFactory: () => WidgetFactory,
54
+ drawField: () => drawField,
55
+ drawFieldBox: () => drawFieldBox,
56
+ drawFieldText: () => drawFieldText,
57
+ editInPlace: () => editInPlace,
58
+ formatNumber: () => formatNumber,
59
+ onThemeChange: () => onThemeChange,
47
60
  portConfigTypes: () => portConfigTypes,
48
61
  portsCompatible: () => portsCompatible,
49
62
  renderContinuously: () => renderContinuously,
50
63
  requestRender: () => requestRender,
51
- typeSetsCompatible: () => typeSetsCompatible
64
+ setTheme: () => setTheme,
65
+ themeVersion: () => themeVersion,
66
+ typeSetsCompatible: () => typeSetsCompatible,
67
+ widgetFont: () => widgetFont
52
68
  });
53
69
  module.exports = __toCommonJS(index_exports);
54
70
 
71
+ // src/render_scheduler.ts
72
+ var subscribers = /* @__PURE__ */ new Set();
73
+ var frameQueued = false;
74
+ var continuous = 0;
75
+ var canSchedule = () => typeof requestAnimationFrame === "function";
76
+ function onRenderRequested(redraw) {
77
+ subscribers.add(redraw);
78
+ return () => {
79
+ subscribers.delete(redraw);
80
+ };
81
+ }
82
+ function requestRender() {
83
+ if (frameQueued || !canSchedule()) {
84
+ return;
85
+ }
86
+ frameQueued = true;
87
+ requestAnimationFrame(() => {
88
+ frameQueued = false;
89
+ subscribers.forEach((redraw) => redraw());
90
+ if (continuous > 0) {
91
+ requestRender();
92
+ }
93
+ });
94
+ }
95
+ function renderContinuously() {
96
+ continuous++;
97
+ requestRender();
98
+ let released = false;
99
+ return () => {
100
+ if (released) {
101
+ return;
102
+ }
103
+ released = true;
104
+ continuous--;
105
+ };
106
+ }
107
+
55
108
  // src/theme.ts
56
- var Theme = {
109
+ var defaults = {
57
110
  FontFamily: "Courier New",
58
111
  Graph: {
59
- BackgroundColor: "#07212a"
112
+ BackgroundColor: "#07212a",
113
+ DotColor: "rgb(41, 54, 57)"
60
114
  },
61
115
  Node: {
62
116
  FontColor: "#afb9bb",
@@ -73,6 +127,7 @@ var Theme = {
73
127
  Grabbed: "white",
74
128
  Selected: "#6e6e6e"
75
129
  },
130
+ Bevel: null,
76
131
  Port: {
77
132
  FontColor: "#afb9bb"
78
133
  },
@@ -90,15 +145,25 @@ var Theme = {
90
145
  },
91
146
  Widget: {
92
147
  FontColor: "#afb9bb",
148
+ LabelColor: "#7f9497",
149
+ FontSize: 14,
150
+ Padding: 6,
151
+ Gap: 4,
93
152
  BackgroundColor: "#0f313c",
94
153
  Border: {
95
154
  Color: "#154050",
96
155
  Size: 2,
97
156
  Radius: 2
98
157
  },
158
+ Bevel: null,
99
159
  Hover: {
100
160
  BackgroundColor: "#195366"
101
161
  },
162
+ Input: {
163
+ BackgroundColor: "#0f313c",
164
+ HoverBackgroundColor: "#195366",
165
+ FontColor: "#afb9bb"
166
+ },
102
167
  Slider: {
103
168
  FillColor: "#07212A"
104
169
  },
@@ -111,8 +176,10 @@ var Theme = {
111
176
  ContextMenu: {
112
177
  BackgroundColor: "#07212A",
113
178
  HighlightColor: "#205A6D",
179
+ HighlightFontColor: "#afb9bb",
114
180
  FontColor: "#afb9bb",
115
- DisabledFontColor: "#4a585c"
181
+ DisabledFontColor: "#4a585c",
182
+ BorderRadius: 5
116
183
  },
117
184
  Minimap: {
118
185
  BackgroundColor: "rgba(10, 16, 18, 0.85)",
@@ -122,6 +189,13 @@ var Theme = {
122
189
  NodeColor: "#6b7f85",
123
190
  ViewportColor: "rgba(175, 185, 187, 0.25)"
124
191
  },
192
+ Popup: {
193
+ BackgroundColor: "#000000",
194
+ FontColor: "#ffffff",
195
+ FontFamily: "Verdana",
196
+ BorderRadius: 8,
197
+ Backdrop: "#00000070"
198
+ },
125
199
  Note: {
126
200
  FontColor: "#afb9bb",
127
201
  FontSize: 16,
@@ -140,11 +214,47 @@ var Theme = {
140
214
  },
141
215
  CodeBlock: {
142
216
  BackgroundColor: "#0c2b35",
217
+ FontColor: "#afb9bb",
143
218
  Padding: 16,
144
219
  BorderRadius: 4
145
220
  }
146
221
  }
147
222
  };
223
+ function clone(value) {
224
+ return JSON.parse(JSON.stringify(value));
225
+ }
226
+ function merge(target, source) {
227
+ if (source === void 0) {
228
+ return target;
229
+ }
230
+ if (source === null || typeof source !== "object" || target === null || typeof target !== "object") {
231
+ return source;
232
+ }
233
+ for (const key of Object.keys(source)) {
234
+ target[key] = merge(target[key], source[key]);
235
+ }
236
+ return target;
237
+ }
238
+ var DefaultTheme = Object.freeze(clone(defaults));
239
+ var Theme = clone(defaults);
240
+ var version = 0;
241
+ var listeners = /* @__PURE__ */ new Set();
242
+ function setTheme(config) {
243
+ const next = merge(clone(defaults), config);
244
+ for (const key of Object.keys(next)) {
245
+ Theme[key] = next[key];
246
+ }
247
+ version++;
248
+ listeners.forEach((listener) => listener());
249
+ requestRender();
250
+ }
251
+ function themeVersion() {
252
+ return version;
253
+ }
254
+ function onThemeChange(listener) {
255
+ listeners.add(listener);
256
+ return () => listeners.delete(listener);
257
+ }
148
258
 
149
259
  // src/styles/text.ts
150
260
  var DefaultSize = 16;
@@ -167,19 +277,19 @@ var TextStyle = class {
167
277
  constructor(config) {
168
278
  this.#size = config?.size === void 0 ? DefaultSize : config.size;
169
279
  this.#color = config?.color === void 0 ? DefaultColor : config.color;
170
- this.#font = config?.font === void 0 ? Theme.FontFamily : config.font;
280
+ this.#font = config?.font;
171
281
  this.#weight = config?.weight === void 0 ? "" /* Normal */ : config.weight;
172
282
  this.#fontStyle = config?.style === void 0 ? "" /* Normal */ : config.style;
173
283
  }
174
284
  setupStyle(ctx, scale) {
175
285
  ctx.fillStyle = this.#color;
176
- ctx.font = this.#fontStyle + " " + this.#weight + " " + this.#size * scale + "px " + this.#font;
286
+ ctx.font = this.#fontStyle + " " + this.#weight + " " + this.#size * scale + "px " + this.getFont();
177
287
  }
178
288
  getSize() {
179
289
  return this.#size;
180
290
  }
181
291
  getFont() {
182
- return this.#font;
292
+ return this.#font ?? Theme.FontFamily;
183
293
  }
184
294
  getWeight() {
185
295
  return this.#weight;
@@ -550,7 +660,18 @@ var ContextMenu = class _ContextMenu {
550
660
  }
551
661
  #tempBox = { Position: Zero(), Size: Zero() };
552
662
  #openSubMenu;
553
- render(ctx, pppp, graphScale, mousePosition, openRight) {
663
+ #bounds = null;
664
+ #contains(point) {
665
+ if (this.#bounds !== null && InBox(this.#bounds, point)) {
666
+ return true;
667
+ }
668
+ return this.#openSubMenu !== void 0 && this.#openSubMenu.#contains(point);
669
+ }
670
+ /**
671
+ * A submenu is anchored to its parent's left edge and told the parent's
672
+ * width, so it can open on whichever side has room.
673
+ */
674
+ render(ctx, anchor, graphScale, mousePosition, openRight, parentWidth = 0) {
554
675
  const menuScale = 1.25;
555
676
  const scaledEntryHeight = menuScale * contextEntryHeight;
556
677
  const scaledEntryWidth = menuScale * 40 + this.#getMaxWidthForText(ctx, menuScale);
@@ -559,19 +680,35 @@ var ContextMenu = class _ContextMenu {
559
680
  totalScaledHeight += this.#groups.At(i).height();
560
681
  }
561
682
  totalScaledHeight *= menuScale;
562
- const position = { x: 0, y: 0 };
563
- CopyVector2(position, pppp);
564
- if (!openRight) {
565
- position.x -= scaledEntryWidth;
683
+ const canvasWidth = ctx.canvas.clientWidth;
684
+ const position = { x: anchor.x, y: anchor.y };
685
+ let opensRight = openRight;
686
+ if (parentWidth === 0) {
687
+ if (!openRight) {
688
+ position.x -= scaledEntryWidth;
689
+ }
690
+ if (openRight && position.x + scaledEntryWidth > canvasWidth) {
691
+ position.x = canvasWidth - scaledEntryWidth;
692
+ opensRight = false;
693
+ }
694
+ } else {
695
+ const rightOfParent = anchor.x + parentWidth;
696
+ const leftOfParent = anchor.x - scaledEntryWidth;
697
+ const fitsRight = rightOfParent + scaledEntryWidth <= canvasWidth;
698
+ const fitsLeft = leftOfParent >= 0;
699
+ opensRight = openRight ? fitsRight || !fitsLeft : !fitsLeft && fitsRight;
700
+ position.x = opensRight ? rightOfParent : leftOfParent;
701
+ position.x = Math.max(0, Math.min(position.x, canvasWidth - scaledEntryWidth));
566
702
  }
567
703
  if (position.y + totalScaledHeight > ctx.canvas.clientHeight) {
568
704
  position.y = ctx.canvas.clientHeight - totalScaledHeight;
569
705
  }
570
- let submenuOpenRight = openRight;
571
- if (openRight && position.x + scaledEntryWidth > ctx.canvas.clientWidth) {
572
- position.x = ctx.canvas.clientWidth - scaledEntryWidth;
573
- submenuOpenRight = !submenuOpenRight;
574
- }
706
+ position.y = Math.max(0, position.y);
707
+ this.#bounds = {
708
+ Position: { x: position.x, y: position.y },
709
+ Size: { x: scaledEntryWidth, y: totalScaledHeight }
710
+ };
711
+ const pointer = mousePosition !== void 0 && this.#openSubMenu !== void 0 && this.#openSubMenu.#contains(mousePosition) ? void 0 : mousePosition;
575
712
  this.#tempBox.Size.x = scaledEntryWidth;
576
713
  this.#tempBox.Size.y = scaledEntryHeight;
577
714
  CopyVector2(this.#tempBox.Position, position);
@@ -585,36 +722,36 @@ var ContextMenu = class _ContextMenu {
585
722
  this.#tempBox.Position.y,
586
723
  scaledEntryWidth,
587
724
  totalScaledHeight,
588
- 5 * menuScale
725
+ Theme.ContextMenu.BorderRadius * menuScale
589
726
  );
590
727
  ctx.fill();
591
728
  ctx.shadowBlur = 0;
592
729
  let mouseIsOver = null;
593
- let subOpenedThisFrame = false;
730
+ if (pointer !== void 0) {
731
+ let row = 0;
732
+ for (let groupIndex = 0; groupIndex < this.#groups.Count(); groupIndex++) {
733
+ for (const entry of this.#groups.At(groupIndex).entries) {
734
+ this.#tempBox.Position.y = position.y + scaledEntryHeight * row;
735
+ if (InBox(this.#tempBox, pointer)) {
736
+ mouseIsOver = entry;
737
+ this.#openSubMenu = entry.subMenu;
738
+ this.#submenuPosition.x = position.x;
739
+ this.#submenuPosition.y = this.#tempBox.Position.y;
740
+ }
741
+ row++;
742
+ }
743
+ }
744
+ }
594
745
  let optionsRendered = 0;
595
746
  for (let groupIndex = 0; groupIndex < this.#groups.Count(); groupIndex++) {
596
747
  const group = this.#groups.At(groupIndex);
597
748
  for (let entryIndex = 0; entryIndex < group.entries.length; entryIndex++) {
598
749
  const entry = group.entries[entryIndex];
599
750
  this.#tempBox.Position.y = position.y + scaledEntryHeight * optionsRendered;
600
- let entryMousedOver = false;
601
- if (mousePosition !== void 0 && InBox(this.#tempBox, mousePosition)) {
602
- mouseIsOver = entry;
603
- entryMousedOver = true;
604
- if (entry.subMenu !== void 0) {
605
- this.#openSubMenu = entry.subMenu;
606
- this.#submenuPosition.x = position.x;
607
- this.#submenuPosition.y = this.#tempBox.Position.y;
608
- if (submenuOpenRight) {
609
- this.#submenuPosition.x += scaledEntryWidth;
610
- }
611
- subOpenedThisFrame = true;
612
- } else {
613
- this.#openSubMenu = void 0;
614
- }
615
- }
751
+ const entryMousedOver = entry === mouseIsOver;
616
752
  const itemDisabled = entry.item !== void 0 && entry.item.state() === 1 /* Disabled */;
617
- if (!itemDisabled && (entryMousedOver || this.#openSubMenu !== void 0 && entry.subMenu === this.#openSubMenu)) {
753
+ const highlighted = !itemDisabled && (entryMousedOver || this.#openSubMenu !== void 0 && entry.subMenu === this.#openSubMenu);
754
+ if (highlighted) {
618
755
  ctx.fillStyle = Theme.ContextMenu.HighlightColor;
619
756
  ctx.beginPath();
620
757
  ctx.roundRect(
@@ -622,18 +759,20 @@ var ContextMenu = class _ContextMenu {
622
759
  this.#tempBox.Position.y + scaledEntryHeight / 10,
623
760
  scaledEntryWidth - scaledEntryHeight / 5,
624
761
  scaledEntryHeight - scaledEntryHeight / 5,
625
- 5 * menuScale
762
+ Theme.ContextMenu.BorderRadius * menuScale
626
763
  );
627
764
  ctx.fill();
628
765
  }
629
766
  this.#textStyle.setupStyle(ctx, menuScale);
630
767
  if (itemDisabled) {
631
768
  ctx.fillStyle = Theme.ContextMenu.DisabledFontColor;
769
+ } else if (highlighted) {
770
+ ctx.fillStyle = Theme.ContextMenu.HighlightFontColor;
632
771
  }
633
772
  ctx.fillText(entry.text, position.x + scaledEntryHeight / 5, this.#tempBox.Position.y + scaledEntryHeight / 2);
634
773
  if (entry.subMenu !== void 0) {
635
774
  ctx.beginPath();
636
- ctx.strokeStyle = Theme.ContextMenu.FontColor;
775
+ ctx.strokeStyle = highlighted ? Theme.ContextMenu.HighlightFontColor : Theme.ContextMenu.FontColor;
637
776
  ctx.lineWidth = 1 * menuScale;
638
777
  ctx.lineTo(position.x + scaledEntryWidth - scaledEntryHeight / 2.5, this.#tempBox.Position.y + scaledEntryHeight / 3);
639
778
  ctx.lineTo(position.x + scaledEntryWidth - scaledEntryHeight / 4, this.#tempBox.Position.y + scaledEntryHeight / 2);
@@ -654,7 +793,7 @@ var ContextMenu = class _ContextMenu {
654
793
  }
655
794
  }
656
795
  if (this.#openSubMenu !== void 0) {
657
- const mouseOverSub = this.#openSubMenu.render(ctx, this.#submenuPosition, menuScale, mousePosition, submenuOpenRight);
796
+ const mouseOverSub = this.#openSubMenu.render(ctx, this.#submenuPosition, menuScale, mousePosition, opensRight, scaledEntryWidth);
658
797
  if (mouseOverSub !== null) {
659
798
  mouseIsOver = mouseOverSub;
660
799
  }
@@ -663,43 +802,6 @@ var ContextMenu = class _ContextMenu {
663
802
  }
664
803
  };
665
804
 
666
- // src/render_scheduler.ts
667
- var subscribers = /* @__PURE__ */ new Set();
668
- var frameQueued = false;
669
- var continuous = 0;
670
- var canSchedule = () => typeof requestAnimationFrame === "function";
671
- function onRenderRequested(redraw) {
672
- subscribers.add(redraw);
673
- return () => {
674
- subscribers.delete(redraw);
675
- };
676
- }
677
- function requestRender() {
678
- if (frameQueued || !canSchedule()) {
679
- return;
680
- }
681
- frameQueued = true;
682
- requestAnimationFrame(() => {
683
- frameQueued = false;
684
- subscribers.forEach((redraw) => redraw());
685
- if (continuous > 0) {
686
- requestRender();
687
- }
688
- });
689
- }
690
- function renderContinuously() {
691
- continuous++;
692
- requestRender();
693
- let released = false;
694
- return () => {
695
- if (released) {
696
- return;
697
- }
698
- released = true;
699
- continuous--;
700
- };
701
- }
702
-
703
805
  // src/input.ts
704
806
  var LONG_PRESS_MS = 500;
705
807
  var LONG_PRESS_SLOP = 12;
@@ -1284,6 +1386,7 @@ function splitStringIntoLines(ctx, str, maxWidth) {
1284
1386
  // src/types/text.ts
1285
1387
  var Text = class _Text {
1286
1388
  #measured;
1389
+ #measuredForTheme = -1;
1287
1390
  #size;
1288
1391
  #style;
1289
1392
  #value;
@@ -1363,9 +1466,10 @@ var Text = class _Text {
1363
1466
  return results;
1364
1467
  }
1365
1468
  #measure(ctx) {
1366
- if (this.#measured) {
1469
+ if (this.#measured && this.#measuredForTheme === themeVersion()) {
1367
1470
  return;
1368
1471
  }
1472
+ this.#measuredForTheme = themeVersion();
1369
1473
  this.#style.setupStyle(ctx, 1);
1370
1474
  if (this.#maxWidth == -1) {
1371
1475
  const measurements = ctx.measureText(this.#value);
@@ -1441,7 +1545,7 @@ function RgbToHex(c) {
1441
1545
  return "#" + componentToHex(Math.round(c.r * 255)) + componentToHex(Math.round(c.g * 255)) + componentToHex(Math.round(c.b * 255));
1442
1546
  }
1443
1547
  function HexToColor(hex) {
1444
- var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
1548
+ var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})(?:[a-f\d]{2})?$/i.exec(hex);
1445
1549
  return result ? {
1446
1550
  r: parseInt(result[1], 16) / 255,
1447
1551
  g: parseInt(result[2], 16) / 255,
@@ -1531,6 +1635,21 @@ function portConfigTypes(config) {
1531
1635
  }
1532
1636
  return [config.type === void 0 ? "" : config.type];
1533
1637
  }
1638
+ var ARRAY_SLOT_PITCH = 12;
1639
+ function arrayInsertIndex(box, count, y) {
1640
+ if (count === 0) {
1641
+ return 0;
1642
+ }
1643
+ const index = Math.round((y - box.Position.y) / (box.Size.y / count));
1644
+ return Math.min(count, Math.max(0, index));
1645
+ }
1646
+ function arraySlotCenter(box, count, index) {
1647
+ const slots = Math.max(1, count);
1648
+ return {
1649
+ x: box.Position.x + box.Size.x / 2,
1650
+ y: box.Position.y + (index + 0.5) * box.Size.y / slots
1651
+ };
1652
+ }
1534
1653
  function fallbackColor(type, s) {
1535
1654
  let value = 0;
1536
1655
  for (var i = 0; i < type.length; i++) {
@@ -1555,6 +1674,7 @@ var Port = class {
1555
1674
  #acceptedTypes;
1556
1675
  #description;
1557
1676
  #array;
1677
+ #hidden;
1558
1678
  #configuredEmptyFill;
1559
1679
  #configuredFilledFill;
1560
1680
  #dataTypePopupElement;
@@ -1570,6 +1690,7 @@ var Port = class {
1570
1690
  this.#acceptedTypes = config?.acceptedTypes === void 0 ? [] : config.acceptedTypes;
1571
1691
  this.#description = config?.description === void 0 ? "" : config.description;
1572
1692
  this.#array = config?.array === true;
1693
+ this.#hidden = config?.hidden === true;
1573
1694
  this.#configuredEmptyFill = config?.emptyStyle?.fillColor;
1574
1695
  this.#configuredFilledFill = config?.filledStyle?.fillColor;
1575
1696
  this.#emptyStyle = {
@@ -1668,9 +1789,11 @@ var Port = class {
1668
1789
  this.setDataType(type);
1669
1790
  this.#refreshDataType();
1670
1791
  }
1671
- addConnection(connection) {
1672
- const c = this.#connections.length;
1673
- this.#connections.push(connection);
1792
+ addConnection(connection, index) {
1793
+ const c = index === void 0 ? this.#connections.length : Math.min(Math.max(index, 0), this.#connections.length);
1794
+ const was = this.hidden();
1795
+ this.#connections.splice(c, 0, connection);
1796
+ this.#visibilityMayHaveChanged(was);
1674
1797
  for (let i = 0; i < this.#onConnectionAdded.length; i++) {
1675
1798
  this.#onConnectionAdded[i](connection, c, this, this.#portType, this.#node);
1676
1799
  }
@@ -1691,6 +1814,32 @@ var Port = class {
1691
1814
  connections() {
1692
1815
  return this.#connections;
1693
1816
  }
1817
+ /**
1818
+ * How many wires the port has room for: one per connection on an array
1819
+ * input, so each can be told apart and picked up on its own.
1820
+ */
1821
+ slots() {
1822
+ if (this.#portType !== "INPUTARRAY" /* InputArray */) {
1823
+ return 1;
1824
+ }
1825
+ return Math.max(1, this.#connections.length);
1826
+ }
1827
+ setHidden(hidden) {
1828
+ const was = this.hidden();
1829
+ this.#hidden = hidden;
1830
+ this.#visibilityMayHaveChanged(was);
1831
+ }
1832
+ #visibilityMayHaveChanged(was) {
1833
+ if (was !== this.hidden() || this.#portType === "INPUTARRAY" /* InputArray */) {
1834
+ this.#node?.invalidateLayout();
1835
+ }
1836
+ }
1837
+ /**
1838
+ * A port asked to hide still shows while anything is connected to it.
1839
+ */
1840
+ hidden() {
1841
+ return this.#hidden && this.#connections.length === 0;
1842
+ }
1694
1843
  addConnectionRemovedListener(callback) {
1695
1844
  if (callback === void 0) {
1696
1845
  return;
@@ -1700,7 +1849,9 @@ var Port = class {
1700
1849
  clearConnection(connection) {
1701
1850
  const index = this.#connections.indexOf(connection);
1702
1851
  if (index > -1) {
1852
+ const was = this.hidden();
1703
1853
  this.#connections.splice(index, 1);
1854
+ this.#visibilityMayHaveChanged(was);
1704
1855
  for (let i = 0; i < this.#onConnectionRemoved.length; i++) {
1705
1856
  this.#onConnectionRemoved[i](connection, index, this, this.#portType, this.#node);
1706
1857
  }
@@ -1759,20 +1910,31 @@ var Port = class {
1759
1910
  this.#dataTypePopupElement.render(ctx, { x: xPos - size.x / 2, y: yPos }, 1, size);
1760
1911
  });
1761
1912
  }
1913
+ const slots = this.slots();
1914
+ const height2 = slots === 1 ? scaledRadius * 2 : slots * ARRAY_SLOT_PITCH * camera.zoom;
1762
1915
  this.#box.Position.x = position.x - scaledRadius;
1763
- this.#box.Position.y = position.y - scaledRadius;
1916
+ this.#box.Position.y = position.y - height2 / 2;
1764
1917
  this.#box.Size.x = scaledRadius * 2;
1765
- this.#box.Size.y = scaledRadius * 2;
1918
+ this.#box.Size.y = height2;
1766
1919
  ctx.strokeStyle = style.borderColor;
1767
1920
  ctx.fillStyle = style.fillColor;
1768
1921
  ctx.beginPath();
1769
1922
  if (this.#portType === "INPUTARRAY" /* InputArray */) {
1770
- ctx.rect(position.x - scaledRadius, position.y - scaledRadius, scaledRadius * 2, scaledRadius * 2);
1923
+ ctx.roundRect(this.#box.Position.x, this.#box.Position.y, this.#box.Size.x, this.#box.Size.y, slots === 1 ? 0 : scaledRadius);
1771
1924
  } else {
1772
1925
  ctx.arc(position.x, position.y, scaledRadius, 0, 2 * Math.PI);
1773
1926
  }
1774
1927
  ctx.fill();
1775
1928
  ctx.stroke();
1929
+ if (slots > 1) {
1930
+ ctx.fillStyle = style.borderColor;
1931
+ for (let i = 0; i < slots; i++) {
1932
+ const slot = arraySlotCenter(this.#box, slots, i);
1933
+ ctx.beginPath();
1934
+ ctx.arc(slot.x, slot.y, scaledRadius * 0.4, 0, 2 * Math.PI);
1935
+ ctx.fill();
1936
+ }
1937
+ }
1776
1938
  return this.#box;
1777
1939
  }
1778
1940
  };
@@ -1888,331 +2050,480 @@ var BoxStyle = class {
1888
2050
  }
1889
2051
  };
1890
2052
 
1891
- // src/popup.ts
1892
- var Popup = class {
1893
- #title;
1894
- #options;
1895
- #content;
1896
- #onClose;
1897
- // Styling ================================================================
1898
- #buttonCSS;
1899
- // Runtime ================================================================
1900
- #popup;
1901
- #keyHandler;
1902
- constructor(config) {
1903
- this.#title = config.title;
1904
- this.#options = config.options;
1905
- this.#content = config.content;
1906
- this.#onClose = config.onClose;
1907
- this.#buttonCSS = config.buttonCSS === void 0 ? "flex-grow: 1; margin-right: 8px;" : config.buttonCSS;
1908
- this.#popup = null;
1909
- this.#keyHandler = null;
1910
- }
1911
- Hide() {
1912
- if (this.#popup === null) {
1913
- return;
1914
- }
1915
- if (this.#keyHandler !== null) {
1916
- document.removeEventListener("keydown", this.#keyHandler, true);
1917
- this.#keyHandler = null;
1918
- }
1919
- document.body.removeChild(this.#popup);
1920
- this.#popup = null;
2053
+ // src/styles/cursor.ts
2054
+ var CursorStyle = /* @__PURE__ */ ((CursorStyle2) => {
2055
+ CursorStyle2["Default"] = "default";
2056
+ CursorStyle2["Pointer"] = "pointer";
2057
+ CursorStyle2["Grab"] = "grab";
2058
+ CursorStyle2["Grabbing"] = "grabbing";
2059
+ CursorStyle2["Wait"] = "wait";
2060
+ CursorStyle2["Help"] = "help";
2061
+ CursorStyle2["Crosshair"] = "crosshair";
2062
+ CursorStyle2["Cell"] = "cell";
2063
+ CursorStyle2["Text"] = "text";
2064
+ CursorStyle2["Move"] = "move";
2065
+ CursorStyle2["NotAllowed"] = "not-allowed";
2066
+ CursorStyle2["ZoomIn"] = "zoom-in";
2067
+ CursorStyle2["ZoomOut"] = "zoom-out";
2068
+ CursorStyle2["None"] = "none";
2069
+ CursorStyle2["ContextMenu"] = "context-menu";
2070
+ CursorStyle2["EWResize"] = "ew-resize";
2071
+ return CursorStyle2;
2072
+ })(CursorStyle || {});
2073
+
2074
+ // src/styles/bevel.ts
2075
+ function drawBevel(ctx, box, scale, colors, sunken) {
2076
+ const width2 = Math.max(1, 2 * scale);
2077
+ const half = width2 / 2;
2078
+ const left = box.Position.x + half;
2079
+ const top = box.Position.y + half;
2080
+ const right = box.Position.x + box.Size.x - half;
2081
+ const bottom = box.Position.y + box.Size.y - half;
2082
+ ctx.save();
2083
+ ctx.lineWidth = width2;
2084
+ ctx.lineCap = "square";
2085
+ ctx.strokeStyle = sunken ? colors.Dark : colors.Light;
2086
+ ctx.beginPath();
2087
+ ctx.moveTo(left, bottom);
2088
+ ctx.lineTo(left, top);
2089
+ ctx.lineTo(right, top);
2090
+ ctx.stroke();
2091
+ ctx.strokeStyle = sunken ? colors.Light : colors.Dark;
2092
+ ctx.beginPath();
2093
+ ctx.moveTo(right, top);
2094
+ ctx.lineTo(right, bottom);
2095
+ ctx.lineTo(left, bottom);
2096
+ ctx.stroke();
2097
+ ctx.restore();
2098
+ }
2099
+
2100
+ // src/widgets/field.ts
2101
+ function widgetFont(scale) {
2102
+ return `${Theme.Widget.FontSize * scale}px ${Theme.FontFamily}`;
2103
+ }
2104
+ function fieldBackground(look) {
2105
+ if (look.background !== void 0) {
2106
+ return look.background;
1921
2107
  }
1922
- #close(option) {
1923
- this.Hide();
1924
- if (this.#onClose !== void 0) {
1925
- this.#onClose(option);
1926
- }
2108
+ if (look.input) {
2109
+ return look.hovered ? Theme.Widget.Input.HoverBackgroundColor : Theme.Widget.Input.BackgroundColor;
1927
2110
  }
1928
- Show() {
1929
- this.#popup = document.createElement("div");
1930
- this.#popup.style.cssText = "position:absolute;top:0;width:100%;display:flex;height:100%;justify-content:center;align-items:center;background-color:#00000070;";
1931
- document.body.appendChild(this.#popup);
1932
- const container = document.createElement("div");
1933
- container.style.cssText = "background-color:#000;color:white;font-family:Verdana;border-radius:8px;padding:16px;";
1934
- this.#popup.appendChild(container);
1935
- const title = document.createElement("h2");
1936
- title.textContent = this.#title;
1937
- title.style.cssText = "margin-top:0;margin-bottom:16px;";
1938
- container.appendChild(title);
1939
- if (this.#content !== void 0) {
1940
- container.appendChild(this.#content());
1941
- }
1942
- const buttonContainer = document.createElement("div");
1943
- buttonContainer.style.cssText = "margin-top:16px;justify-content:space-around;align-items:center;flex-direction:row;display:flex;";
1944
- container.appendChild(buttonContainer);
1945
- for (let i = 0; i < this.#options.length; i++) {
1946
- const button = document.createElement("button");
1947
- button.style.cssText = this.#buttonCSS;
1948
- button.textContent = this.#options[i];
1949
- button.onclick = () => this.#close(this.#options[i]);
1950
- buttonContainer.appendChild(button);
1951
- }
1952
- this.#keyHandler = (e) => {
1953
- if (e.key === "Enter") {
1954
- e.preventDefault();
1955
- this.#close(this.#options.length > 0 ? this.#options[0] : null);
1956
- } else if (e.key === "Escape") {
1957
- e.preventDefault();
1958
- this.#close(null);
1959
- }
1960
- e.stopPropagation();
1961
- };
1962
- document.addEventListener("keydown", this.#keyHandler, true);
2111
+ if (look.pressed) {
2112
+ return Theme.Widget.Button.Click.BackgroundColor;
1963
2113
  }
1964
- };
1965
-
1966
- // src/styles/textBox.ts
1967
- function TextBoxStyleWithFallback(input, fallback) {
1968
- return {
1969
- box: BoxStyleWithFallback(input?.box, fallback?.box),
1970
- text: TextStyleFallback(input?.text, fallback?.text),
1971
- textAlign: input?.textAlign === void 0 ? fallback?.textAlign : input?.textAlign,
1972
- textBaseline: input?.textBaseline === void 0 ? fallback?.textBaseline : input?.textBaseline
1973
- };
2114
+ return look.hovered ? Theme.Widget.Hover.BackgroundColor : Theme.Widget.BackgroundColor;
1974
2115
  }
1975
- var TextBoxStyle = class {
1976
- #box;
1977
- #text;
1978
- #textAlign;
1979
- #textBaseline;
1980
- constructor(config) {
1981
- this.#box = new BoxStyle(config?.box);
1982
- this.#text = new TextStyle(config?.text);
1983
- this.#textAlign = config?.textAlign === void 0 ? "center" /* Center */ : config.textAlign;
1984
- this.#textBaseline = config?.textBaseline === void 0 ? "middle" /* Middle */ : config.textBaseline;
1985
- }
1986
- Draw(ctx, box, scale, text) {
1987
- this.#box.Draw(ctx, box, scale);
1988
- ctx.textAlign = this.#textAlign;
1989
- ctx.textBaseline = this.#textBaseline;
1990
- this.#text.setupStyle(ctx, scale);
1991
- ctx.fillText(
1992
- text,
1993
- box.Position.x + box.Size.x / 2,
1994
- box.Position.y + box.Size.y / 2
1995
- );
2116
+ function drawFieldBox(ctx, box, scale, look) {
2117
+ ctx.fillStyle = fieldBackground(look);
2118
+ ctx.beginPath();
2119
+ ctx.roundRect(box.Position.x, box.Position.y, box.Size.x, box.Size.y, Theme.Widget.Border.Radius * scale);
2120
+ ctx.fill();
2121
+ if (Theme.Widget.Border.Size > 0) {
2122
+ ctx.strokeStyle = look.border ?? Theme.Widget.Border.Color;
2123
+ ctx.lineWidth = Theme.Widget.Border.Size * scale;
2124
+ ctx.stroke();
1996
2125
  }
1997
- DrawUnderline(ctx, box, scale, text) {
1998
- this.#box.DrawUnderline(ctx, box, scale);
1999
- ctx.textAlign = this.#textAlign;
2000
- ctx.textBaseline = this.#textBaseline;
2001
- this.#text.setupStyle(ctx, scale);
2126
+ if (Theme.Widget.Bevel !== null) {
2127
+ drawBevel(ctx, box, scale, Theme.Widget.Bevel, look.input === true || look.pressed === true);
2128
+ }
2129
+ }
2130
+ function drawField(ctx, box, scale, look) {
2131
+ drawFieldBox(ctx, box, scale, look);
2132
+ drawFieldText(ctx, box, scale, look);
2133
+ }
2134
+ function drawFieldText(ctx, box, scale, look) {
2135
+ const padding = Theme.Widget.Padding * scale;
2136
+ const middle = box.Position.y + box.Size.y / 2;
2137
+ const textColor = look.textColor ?? (look.input ? Theme.Widget.Input.FontColor : Theme.Widget.FontColor);
2138
+ ctx.font = widgetFont(scale);
2139
+ ctx.textBaseline = "middle";
2140
+ if (look.label === void 0 || look.label === "") {
2141
+ ctx.textAlign = "center";
2142
+ ctx.fillStyle = textColor;
2002
2143
  ctx.fillText(
2003
- text,
2144
+ fitString(ctx, look.value, box.Size.x - padding * 2),
2004
2145
  box.Position.x + box.Size.x / 2,
2005
- box.Position.y + box.Size.y / 2
2146
+ middle
2006
2147
  );
2148
+ return;
2007
2149
  }
2008
- setTextColor(color) {
2009
- this.#text.setColor(color);
2010
- }
2011
- setBoxColor(color) {
2012
- this.#box.setColor(color);
2013
- }
2014
- setBorderColor(color) {
2015
- this.#box.setBorderColor(color);
2150
+ const room = box.Size.x - padding * 2;
2151
+ const label = fitString(ctx, look.label, room / 2);
2152
+ const labelWidth = ctx.measureText(label).width;
2153
+ ctx.textAlign = "left";
2154
+ ctx.fillStyle = look.textColor ?? Theme.Widget.LabelColor;
2155
+ ctx.fillText(label, box.Position.x + padding, middle);
2156
+ ctx.textAlign = "right";
2157
+ ctx.fillStyle = textColor;
2158
+ ctx.fillText(
2159
+ fitString(ctx, look.value, room - labelWidth - padding),
2160
+ box.Position.x + box.Size.x - padding,
2161
+ middle
2162
+ );
2163
+ }
2164
+
2165
+ // src/widgets/inplace.ts
2166
+ function editInPlace(config) {
2167
+ const canvas = config.canvas;
2168
+ if (canvas === null) {
2169
+ return null;
2016
2170
  }
2017
- };
2171
+ const rect = canvas.getBoundingClientRect();
2172
+ const sx = canvas.width > 0 ? rect.width / canvas.width : 1;
2173
+ const sy = canvas.height > 0 ? rect.height / canvas.height : 1;
2174
+ const input = document.createElement("input");
2175
+ input.type = "text";
2176
+ input.value = config.value;
2177
+ input.style.cssText = [
2178
+ "position: fixed",
2179
+ `left: ${rect.left + config.box.Position.x * sx}px`,
2180
+ `top: ${rect.top + config.box.Position.y * sy}px`,
2181
+ `width: ${config.box.Size.x * sx}px`,
2182
+ `height: ${config.box.Size.y * sy}px`,
2183
+ "box-sizing: border-box",
2184
+ "margin: 0",
2185
+ `padding: 0 ${Theme.Widget.Padding * config.scale}px`,
2186
+ `font: ${Theme.Widget.FontSize * config.scale * sy}px ${Theme.FontFamily}`,
2187
+ `color: ${Theme.Widget.FontColor}`,
2188
+ `background: ${Theme.Widget.Hover.BackgroundColor}`,
2189
+ `border: ${Theme.Widget.Border.Size * config.scale}px solid ${Theme.Widget.Border.Color}`,
2190
+ `border-radius: ${Theme.Widget.Border.Radius * config.scale}px`,
2191
+ "outline: none",
2192
+ "z-index: 1000"
2193
+ ].join(";");
2194
+ let done = false;
2195
+ const finish = (keep) => {
2196
+ if (done) {
2197
+ return;
2198
+ }
2199
+ done = true;
2200
+ input.remove();
2201
+ if (keep) {
2202
+ config.onCommit(input.value);
2203
+ }
2204
+ requestRender();
2205
+ };
2206
+ input.addEventListener("keydown", (event) => {
2207
+ event.stopPropagation();
2208
+ if (event.key === "Enter") {
2209
+ event.preventDefault();
2210
+ finish(true);
2211
+ } else if (event.key === "Escape") {
2212
+ event.preventDefault();
2213
+ finish(false);
2214
+ }
2215
+ });
2216
+ input.addEventListener("blur", () => finish(true));
2217
+ document.body.appendChild(input);
2218
+ input.focus();
2219
+ input.select();
2220
+ return input;
2221
+ }
2018
2222
 
2019
2223
  // src/widgets/widget.ts
2020
2224
  var width = 150;
2021
2225
  var height = 25;
2022
- function startingWidgetValue(node, property, configured, fallback) {
2023
- if (configured !== void 0) {
2024
- return configured;
2226
+ var WidgetBase = class {
2227
+ #hidden = false;
2228
+ #disabled = false;
2229
+ #hovered = false;
2230
+ #pressed = false;
2231
+ #box = { Position: { x: 0, y: 0 }, Size: { x: 0, y: 0 } };
2232
+ #canvas = null;
2233
+ #scale = 1;
2234
+ Draw(ctx, box, scale, mousePosition) {
2235
+ this.#box = { Position: { x: box.Position.x, y: box.Position.y }, Size: { x: box.Size.x, y: box.Size.y } };
2236
+ this.#canvas = ctx.canvas ?? null;
2237
+ this.#scale = scale;
2238
+ this.#hovered = !this.#disabled && mousePosition !== void 0 && InBox(box, mousePosition);
2239
+ ctx.save();
2240
+ if (this.#disabled) {
2241
+ ctx.globalAlpha *= 0.4;
2242
+ }
2243
+ this.render(ctx, box, scale);
2244
+ ctx.restore();
2245
+ }
2246
+ PointerDown(pointer) {
2247
+ if (this.#disabled) {
2248
+ return;
2249
+ }
2250
+ this.#pressed = true;
2251
+ this.onPointerDown(pointer);
2252
+ requestRender();
2025
2253
  }
2026
- if (property !== void 0 && property !== null) {
2027
- const existing = node.getProperty(property);
2028
- if (existing !== void 0 && existing !== null) {
2029
- return existing;
2254
+ PointerMove(pointer) {
2255
+ if (!this.#pressed) {
2256
+ return;
2030
2257
  }
2258
+ this.onPointerMove(pointer);
2259
+ requestRender();
2031
2260
  }
2032
- return fallback;
2033
- }
2034
-
2035
- // src/widgets/number.ts
2036
- var NumberWidget = class {
2261
+ PointerUp(pointer) {
2262
+ if (!this.#pressed) {
2263
+ return;
2264
+ }
2265
+ this.#pressed = false;
2266
+ this.onPointerUp(pointer);
2267
+ if (pointer.inside) {
2268
+ this.onClick(pointer);
2269
+ }
2270
+ requestRender();
2271
+ }
2272
+ Cursor() {
2273
+ return "pointer" /* Pointer */;
2274
+ }
2275
+ Hidden() {
2276
+ return this.#hidden;
2277
+ }
2278
+ setHidden(hidden) {
2279
+ this.#hidden = hidden;
2280
+ requestRender();
2281
+ }
2282
+ Disabled() {
2283
+ return this.#disabled;
2284
+ }
2285
+ setDisabled(disabled) {
2286
+ this.#disabled = disabled;
2287
+ if (disabled) {
2288
+ this.#pressed = false;
2289
+ }
2290
+ requestRender();
2291
+ }
2292
+ hovered() {
2293
+ return this.#hovered;
2294
+ }
2295
+ pressed() {
2296
+ return this.#pressed;
2297
+ }
2298
+ /**
2299
+ * Where the widget was last drawn, in canvas pixels.
2300
+ */
2301
+ box() {
2302
+ return this.#box;
2303
+ }
2304
+ canvas() {
2305
+ return this.#canvas;
2306
+ }
2307
+ scale() {
2308
+ return this.#scale;
2309
+ }
2310
+ onPointerDown(pointer) {
2311
+ }
2312
+ onPointerMove(pointer) {
2313
+ }
2314
+ onPointerUp(pointer) {
2315
+ }
2316
+ /**
2317
+ * A press and release that both landed on the widget.
2318
+ */
2319
+ onClick(pointer) {
2320
+ }
2321
+ };
2322
+ function startingWidgetValue(node, property, configured, fallback) {
2323
+ if (configured !== void 0) {
2324
+ return configured;
2325
+ }
2326
+ if (property !== void 0 && property !== null) {
2327
+ const existing = node.getProperty(property);
2328
+ if (existing !== void 0 && existing !== null) {
2329
+ return existing;
2330
+ }
2331
+ }
2332
+ return fallback;
2333
+ }
2334
+
2335
+ // src/widgets/value.ts
2336
+ var ValueWidget = class extends WidgetBase {
2037
2337
  #node;
2038
- #nodeProperty;
2338
+ #property;
2039
2339
  #value;
2040
- #idleBoxStyle;
2041
- #highlightBoxStyle;
2042
- #text;
2340
+ #label;
2341
+ #width;
2043
2342
  #callback;
2044
- constructor(node, config) {
2045
- this.#value = 0;
2046
- this.#text = "0";
2343
+ #onInput;
2344
+ constructor(node, config, fallback) {
2345
+ super();
2047
2346
  this.#node = node;
2048
- this.#nodeProperty = config?.property;
2049
- this.#idleBoxStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.idleBoxStyle, {
2050
- box: {
2051
- color: Theme.Widget.BackgroundColor,
2052
- border: {
2053
- size: Theme.Widget.Border.Size,
2054
- color: Theme.Widget.Border.Color
2055
- },
2056
- radius: Theme.Widget.Border.Radius
2057
- },
2058
- text: { color: Theme.Widget.FontColor }
2059
- }));
2060
- this.#highlightBoxStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.highlightBoxStyle, {
2061
- box: {
2062
- color: Theme.Widget.Hover.BackgroundColor,
2063
- border: {
2064
- size: Theme.Widget.Border.Size,
2065
- color: Theme.Widget.Border.Color
2066
- },
2067
- radius: Theme.Widget.Border.Radius
2068
- },
2069
- text: { color: Theme.Widget.FontColor }
2070
- }));
2071
- this.Set(startingWidgetValue(node, this.#nodeProperty, config?.value, 0));
2347
+ this.#property = config?.property;
2348
+ this.#label = config?.label ?? "";
2349
+ this.#width = config?.width ?? width;
2350
+ this.#value = startingWidgetValue(node, this.#property, config?.value, fallback);
2072
2351
  this.#callback = config?.callback;
2073
- if (this.#nodeProperty !== void 0) {
2074
- this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
2075
- this.Set(newVal);
2076
- });
2352
+ this.#onInput = config?.onInput;
2353
+ if (this.#property !== void 0) {
2354
+ node.setProperty(this.#property, this.#value);
2355
+ node.addPropertyChangeListener(this.#property, (_, next) => this.setValue(next));
2077
2356
  }
2078
2357
  }
2079
2358
  Size() {
2080
- return { "x": width, "y": height };
2359
+ return { x: this.#width, y: height };
2360
+ }
2361
+ value() {
2362
+ return this.#value;
2363
+ }
2364
+ /**
2365
+ * Shows a new value without treating it as a person's edit: no callback
2366
+ * and no edit event.
2367
+ */
2368
+ setValue(value) {
2369
+ if (this.equal(value, this.#value)) {
2370
+ return;
2371
+ }
2372
+ this.#value = value;
2373
+ this.valueChanged();
2374
+ if (this.#property !== void 0) {
2375
+ this.#node.setProperty(this.#property, value);
2376
+ }
2377
+ requestRender();
2378
+ }
2379
+ label() {
2380
+ return this.#label;
2081
2381
  }
2082
- Set(newNumber) {
2083
- if (this.#value === newNumber) {
2382
+ setLabel(label) {
2383
+ this.#label = label;
2384
+ requestRender();
2385
+ }
2386
+ node() {
2387
+ return this.#node;
2388
+ }
2389
+ input(value) {
2390
+ this.setValue(value);
2391
+ this.#onInput?.(value);
2392
+ }
2393
+ /**
2394
+ * Ends a person's edit. Nothing is reported when they ended where they
2395
+ * started.
2396
+ */
2397
+ commit(value, startedAt) {
2398
+ this.setValue(value);
2399
+ if (this.equal(value, startedAt)) {
2084
2400
  return;
2085
2401
  }
2086
- this.#value = newNumber;
2087
- if (this.#nodeProperty !== void 0) {
2088
- this.#node.setProperty(this.#nodeProperty, this.#value);
2402
+ if (this.#property !== void 0) {
2403
+ this.#node.editProperty(this.#property, value);
2089
2404
  }
2090
- if (this.#callback !== void 0) {
2091
- this.#callback(this.#value);
2405
+ this.#callback?.(value);
2406
+ }
2407
+ valueChanged() {
2408
+ }
2409
+ equal(a, b) {
2410
+ return Object.is(a, b);
2411
+ }
2412
+ };
2413
+
2414
+ // src/widgets/number.ts
2415
+ var DRAG_THRESHOLD = 3;
2416
+ var NumberWidget = class extends ValueWidget {
2417
+ #step;
2418
+ #integer;
2419
+ #min;
2420
+ #max;
2421
+ #startX = 0;
2422
+ #startValue = 0;
2423
+ #dragging = false;
2424
+ #dragged = false;
2425
+ constructor(node, config) {
2426
+ super(node, config, 0);
2427
+ this.#step = config?.step;
2428
+ this.#integer = config?.integer === true;
2429
+ this.#min = config?.min ?? -Infinity;
2430
+ this.#max = config?.max ?? Infinity;
2431
+ }
2432
+ Cursor() {
2433
+ return "ew-resize" /* EWResize */;
2434
+ }
2435
+ render(ctx, box, scale) {
2436
+ drawField(ctx, box, scale, {
2437
+ input: true,
2438
+ hovered: this.hovered() || this.pressed(),
2439
+ label: this.label(),
2440
+ value: formatNumber(this.value())
2441
+ });
2442
+ }
2443
+ onPointerDown(pointer) {
2444
+ this.#startX = pointer.position.x;
2445
+ this.#startValue = this.value();
2446
+ this.#dragging = false;
2447
+ this.#dragged = false;
2448
+ }
2449
+ onPointerMove(pointer) {
2450
+ const dx = pointer.position.x - this.#startX;
2451
+ if (!this.#dragging && Math.abs(dx) < DRAG_THRESHOLD) {
2452
+ return;
2092
2453
  }
2093
- this.#text = "" + parseFloat(this.#value.toPrecision(6));
2454
+ this.#dragging = true;
2455
+ this.input(this.#clean(this.#startValue + dx * this.#stepFor(this.#startValue)));
2094
2456
  }
2095
- ClickStart() {
2457
+ onPointerUp() {
2458
+ this.#dragged = this.#dragging;
2459
+ if (this.#dragging) {
2460
+ this.#dragging = false;
2461
+ this.commit(this.value(), this.#startValue);
2462
+ }
2096
2463
  }
2097
- ClickEnd() {
2098
- let input = null;
2099
- const setOption = "Set";
2100
- const cancelOption = "Cancel";
2101
- const popup = new Popup({
2102
- title: "Set Number",
2103
- options: [setOption, cancelOption],
2104
- content: () => {
2105
- const container = document.createElement("div");
2106
- input = document.createElement("input");
2107
- input.type = "number";
2108
- input.valueAsNumber = this.#value;
2109
- container.append(input);
2110
- return container;
2111
- },
2112
- onClose: (button) => {
2113
- if (button !== setOption || input === null) {
2114
- return;
2464
+ onClick() {
2465
+ if (this.#dragged) {
2466
+ return;
2467
+ }
2468
+ const before = this.value();
2469
+ editInPlace({
2470
+ canvas: this.canvas(),
2471
+ box: this.box(),
2472
+ scale: this.scale(),
2473
+ value: String(before),
2474
+ onCommit: (text) => {
2475
+ const parsed = Number.parseFloat(text);
2476
+ if (Number.isFinite(parsed)) {
2477
+ this.commit(this.#clean(parsed), before);
2115
2478
  }
2116
- this.Set(input.valueAsNumber);
2117
2479
  }
2118
2480
  });
2119
- popup.Show();
2120
2481
  }
2121
- Draw(ctx, position, scale, mousePosition) {
2122
- const box = {
2123
- Position: { x: 0, y: 0 },
2124
- Size: {
2125
- x: width * scale,
2126
- y: height * scale
2127
- }
2128
- };
2129
- CopyVector2(box.Position, position);
2130
- let style = this.#idleBoxStyle;
2131
- if (mousePosition !== void 0) {
2132
- if (InBox(box, mousePosition)) {
2133
- style = this.#highlightBoxStyle;
2134
- }
2482
+ #stepFor(value) {
2483
+ if (this.#step !== void 0) {
2484
+ return this.#step;
2135
2485
  }
2136
- style.DrawUnderline(ctx, box, scale, this.#text);
2137
- return box;
2486
+ return Math.max(Math.abs(value) * 0.01, this.#integer ? 0.1 : 0.01);
2487
+ }
2488
+ #clean(value) {
2489
+ const rounded = this.#integer ? Math.round(value) : value;
2490
+ return Math.min(this.#max, Math.max(this.#min, rounded));
2138
2491
  }
2139
2492
  };
2493
+ function formatNumber(value) {
2494
+ return "" + parseFloat(value.toPrecision(6));
2495
+ }
2140
2496
 
2141
2497
  // src/widgets/button.ts
2142
- var ButtonWidget = class {
2498
+ var ButtonWidget = class extends WidgetBase {
2143
2499
  #text;
2144
- #idleStyle;
2145
- #hoverStyle;
2146
- #clickStyle;
2147
2500
  #callback;
2148
- #gettingClicked;
2501
+ #width;
2149
2502
  constructor(config) {
2150
- this.#text = config?.text === void 0 ? "Button" : config?.text;
2503
+ super();
2504
+ this.#text = config?.text ?? "Button";
2151
2505
  this.#callback = config?.callback;
2152
- this.#gettingClicked = false;
2153
- this.#idleStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.idleStyle, {
2154
- box: {
2155
- color: Theme.Widget.BackgroundColor,
2156
- border: {
2157
- size: Theme.Widget.Border.Size,
2158
- color: Theme.Widget.Border.Color
2159
- }
2160
- },
2161
- text: { color: Theme.Widget.FontColor }
2162
- }));
2163
- this.#hoverStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.hoverStyle, {
2164
- box: {
2165
- color: Theme.Widget.Hover.BackgroundColor,
2166
- border: {
2167
- size: Theme.Widget.Border.Size,
2168
- color: Theme.Widget.Border.Color
2169
- }
2170
- },
2171
- text: { color: Theme.Widget.FontColor }
2172
- }));
2173
- this.#clickStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.clickStyle, {
2174
- box: {
2175
- color: Theme.Widget.Button.Click.BackgroundColor,
2176
- border: {
2177
- size: Theme.Widget.Border.Size,
2178
- color: Theme.Widget.Border.Color
2179
- }
2180
- },
2181
- text: { color: Theme.Widget.FontColor }
2182
- }));
2506
+ this.#width = config?.width ?? width;
2183
2507
  }
2184
2508
  Size() {
2185
- return { "x": width, "y": height };
2509
+ return { x: this.#width, y: height };
2186
2510
  }
2187
- Draw(ctx, position, scale, mousePosition) {
2188
- const box = {
2189
- Position: { x: 0, y: 0 },
2190
- Size: {
2191
- x: width * scale,
2192
- y: height * scale
2193
- }
2194
- };
2195
- CopyVector2(box.Position, position);
2196
- let style = this.#idleStyle;
2197
- if (mousePosition !== void 0 && !this.#gettingClicked) {
2198
- if (InBox(box, mousePosition)) {
2199
- style = this.#hoverStyle;
2200
- }
2201
- }
2202
- if (this.#gettingClicked) {
2203
- style = this.#clickStyle;
2204
- }
2205
- style.Draw(ctx, box, scale, this.#text);
2206
- return box;
2511
+ text() {
2512
+ return this.#text;
2207
2513
  }
2208
- ClickStart() {
2209
- this.#gettingClicked = true;
2514
+ setText(text) {
2515
+ this.#text = text;
2516
+ requestRender();
2210
2517
  }
2211
- ClickEnd() {
2212
- this.#gettingClicked = false;
2213
- if (this.#callback !== void 0) {
2214
- this.#callback();
2215
- }
2518
+ render(ctx, box, scale) {
2519
+ drawField(ctx, box, scale, {
2520
+ hovered: this.hovered(),
2521
+ pressed: this.pressed(),
2522
+ value: this.#text
2523
+ });
2524
+ }
2525
+ onClick() {
2526
+ this.#callback?.();
2216
2527
  }
2217
2528
  };
2218
2529
 
@@ -2227,500 +2538,214 @@ function contrastColor(color) {
2227
2538
  }
2228
2539
  return "white";
2229
2540
  }
2230
- var ColorWidget = class {
2231
- #node;
2232
- #nodeProperty;
2233
- #value;
2541
+ var picker = null;
2542
+ function sharedPicker() {
2543
+ if (picker === null || !picker.isConnected) {
2544
+ picker = document.createElement("input");
2545
+ picker.type = "color";
2546
+ document.body.appendChild(picker);
2547
+ }
2548
+ return picker;
2549
+ }
2550
+ var ColorWidget = class extends ValueWidget {
2234
2551
  #contrast;
2235
- #textBoxStyle;
2236
- #callback;
2237
2552
  constructor(node, config) {
2238
- this.#node = node;
2239
- this.#nodeProperty = config?.property;
2240
- this.#callback = config?.callback;
2241
- this.#value = startingWidgetValue(node, this.#nodeProperty, config?.value, "#000000");
2242
- this.#contrast = contrastColor(this.#value);
2243
- this.#textBoxStyle = new TextBoxStyle({
2244
- box: {
2245
- color: this.#value,
2246
- border: {
2247
- color: this.#contrast,
2248
- size: Theme.Widget.Border.Size
2249
- }
2250
- },
2251
- text: config?.textStyle
2553
+ super(node, config, "#000000");
2554
+ this.#contrast = contrastColor(this.value());
2555
+ }
2556
+ valueChanged() {
2557
+ this.#contrast = contrastColor(this.value());
2558
+ }
2559
+ render(ctx, box, scale) {
2560
+ drawField(ctx, box, scale, {
2561
+ input: true,
2562
+ hovered: this.hovered(),
2563
+ label: this.label(),
2564
+ value: this.value(),
2565
+ background: this.value(),
2566
+ border: this.#contrast,
2567
+ textColor: this.#contrast
2252
2568
  });
2253
- if (this.#nodeProperty !== void 0) {
2254
- this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
2255
- this.Set(newVal);
2256
- });
2257
- }
2258
- }
2259
- Size() {
2260
- return { "x": width, "y": height };
2261
2569
  }
2262
- Set(value) {
2263
- if (this.#value === value) {
2570
+ onClick() {
2571
+ const canvas = this.canvas();
2572
+ if (canvas === null) {
2264
2573
  return;
2265
2574
  }
2266
- this.#value = value;
2267
- if (this.#callback !== void 0) {
2268
- this.#callback(this.#value);
2269
- }
2270
- if (this.#nodeProperty !== void 0) {
2271
- this.#node.setProperty(this.#nodeProperty, this.#value);
2272
- }
2273
- this.#contrast = contrastColor(this.#value);
2274
- this.#textBoxStyle.setBoxColor(this.#value);
2275
- this.#textBoxStyle.setBorderColor(this.#contrast);
2276
- this.#textBoxStyle.setTextColor(this.#contrast);
2277
- }
2278
- ClickStart() {
2279
- }
2280
- ClickEnd() {
2281
- let input = null;
2282
- const popup = new Popup({
2283
- title: "Set Color",
2284
- options: ["Set", "Cancel"],
2285
- content: () => {
2286
- const container = document.createElement("div");
2287
- input = document.createElement("input");
2288
- input.type = "color";
2289
- input.value = this.#value;
2290
- input.name = "color";
2291
- container.append(input);
2292
- const label = document.createElement("label");
2293
- label.htmlFor = "color";
2294
- label.textContent = "Color";
2295
- container.append(label);
2296
- return container;
2297
- },
2298
- onClose: (button) => {
2299
- if (button !== "Set" || input === null) {
2300
- return;
2301
- }
2302
- this.Set(input.value);
2303
- }
2304
- });
2305
- popup.Show();
2306
- }
2307
- Draw(ctx, position, scale, mousePosition) {
2308
- const box = {
2309
- Position: { x: 0, y: 0 },
2310
- Size: {
2311
- x: width * scale,
2312
- y: height * scale
2313
- }
2314
- };
2315
- CopyVector2(box.Position, position);
2316
- this.#textBoxStyle.Draw(ctx, box, scale, this.#value);
2317
- return box;
2575
+ const before = this.value();
2576
+ const alpha = before.slice(7);
2577
+ const box = this.box();
2578
+ const rect = canvas.getBoundingClientRect();
2579
+ const sx = canvas.width > 0 ? rect.width / canvas.width : 1;
2580
+ const sy = canvas.height > 0 ? rect.height / canvas.height : 1;
2581
+ const input = sharedPicker();
2582
+ input.style.cssText = [
2583
+ "position: fixed",
2584
+ `left: ${rect.left + box.Position.x * sx}px`,
2585
+ `top: ${rect.top + box.Position.y * sy}px`,
2586
+ `width: ${box.Size.x * sx}px`,
2587
+ `height: ${box.Size.y * sy}px`,
2588
+ "opacity: 0",
2589
+ "pointer-events: none",
2590
+ "border: 0",
2591
+ "padding: 0"
2592
+ ].join(";");
2593
+ input.value = before.slice(0, 7);
2594
+ input.oninput = () => this.input(input.value + alpha);
2595
+ input.onchange = () => this.commit(input.value + alpha, before);
2596
+ input.getBoundingClientRect();
2597
+ input.click();
2318
2598
  }
2319
2599
  };
2320
2600
 
2321
2601
  // src/widgets/slider.ts
2322
- var SliderWidget = class {
2323
- // Data Variables =========================================================
2602
+ var SliderWidget = class extends ValueWidget {
2324
2603
  #min;
2325
2604
  #max;
2326
- #value;
2327
- #text;
2328
2605
  #snapIncrement;
2329
- #node;
2330
- #nodeProperty;
2331
- // Callbacks ==============================================================
2332
- #change;
2333
- #release;
2334
- // Styling Variables ======================================================
2335
- #textStyle;
2336
- #backgroundColor;
2337
- #borderColor;
2338
2606
  #fillColor;
2339
- // Runtime Variables ======================================================
2340
- #lastMousePosition;
2341
- #clickStartMousePosition;
2342
- #clicking;
2607
+ #startValue = 0;
2343
2608
  constructor(node, config) {
2344
- this.#node = node;
2345
- this.#value = 0;
2346
- this.#min = config?.min === void 0 ? 0 : config?.min;
2347
- this.#max = config?.max === void 0 ? 1 : config?.max;
2609
+ const min = config?.min ?? 0;
2610
+ const max = config?.max ?? 1;
2611
+ super(node, { ...config, value: config?.value === void 0 ? void 0 : Clamp(config.value, min, max) }, min);
2612
+ this.#min = min;
2613
+ this.#max = max;
2348
2614
  this.#snapIncrement = config?.snapIncrement;
2349
- this.#nodeProperty = config?.property;
2350
- this.#text = "";
2351
- this.#release = config?.release;
2352
- this.#backgroundColor = config?.backgroundColor === void 0 ? Theme.Widget.BackgroundColor : config?.backgroundColor;
2353
- this.#fillColor = config?.fillColor === void 0 ? Theme.Widget.Slider.FillColor : config?.fillColor;
2354
- this.#borderColor = config?.borderColor === void 0 ? Theme.Widget.Border.Color : config?.borderColor;
2355
- this.#textStyle = new TextStyle(TextStyleFallback(config?.textStyle, {
2356
- color: Theme.Widget.FontColor
2357
- }));
2358
- this.#lastMousePosition = Zero();
2359
- this.#clickStartMousePosition = Zero();
2360
- this.#clicking = false;
2361
- if (config?.property !== void 0 && config?.property !== null) {
2362
- node.addPropertyChangeListener(config.property, (oldVal, newVal) => {
2363
- this.SetValue(newVal);
2364
- });
2365
- }
2366
- this.SetValue(startingWidgetValue(node, this.#nodeProperty, config?.value, 0));
2367
- this.#change = config?.change;
2368
- }
2369
- SetValue(newValue) {
2370
- const cleanedValue = Clamp(newValue, this.#min, this.#max);
2371
- if (this.#value === cleanedValue) {
2372
- return;
2373
- }
2374
- this.#value = cleanedValue;
2375
- if (this.#nodeProperty) {
2376
- this.#node.setProperty(this.#nodeProperty, this.#value);
2377
- }
2378
- this.#text = this.#value.toFixed(3);
2379
- if (this.#change !== void 0) {
2380
- this.#change(this.#value);
2381
- }
2615
+ this.#fillColor = config?.fillColor;
2382
2616
  }
2383
- Size() {
2384
- return { "x": width, "y": height };
2617
+ Cursor() {
2618
+ return "ew-resize" /* EWResize */;
2385
2619
  }
2386
- ClickStart() {
2387
- this.#clicking = true;
2388
- CopyVector2(this.#clickStartMousePosition, this.#lastMousePosition);
2620
+ setValue(value) {
2621
+ super.setValue(Clamp(value, this.#min, this.#max));
2389
2622
  }
2390
- ClickEnd() {
2391
- this.#clicking = false;
2392
- if (this.#release !== void 0) {
2393
- this.#release(this.#value);
2394
- }
2395
- }
2396
- Draw(ctx, position, scale, mousePosition) {
2397
- const scaledWidth = width * scale;
2398
- const scaledHeight = height * scale;
2399
- const scaledBorderThickness = Theme.Widget.Border.Size * scale;
2400
- ctx.fillStyle = this.#backgroundColor;
2401
- ctx.strokeStyle = this.#borderColor;
2402
- ctx.lineWidth = scaledBorderThickness;
2623
+ render(ctx, box, scale) {
2624
+ drawFieldBox(ctx, box, scale, { input: true, hovered: this.hovered() || this.pressed(), value: "" });
2625
+ const border = Theme.Widget.Border.Size * scale;
2626
+ const fill = Clamp01((this.value() - this.#min) / (this.#max - this.#min));
2627
+ ctx.fillStyle = this.#fillColor ?? Theme.Widget.Slider.FillColor;
2403
2628
  ctx.beginPath();
2404
2629
  ctx.roundRect(
2405
- position.x,
2406
- position.y,
2407
- scaledWidth,
2408
- scaledHeight,
2630
+ box.Position.x + border / 2,
2631
+ box.Position.y + border / 2,
2632
+ Math.max(0, box.Size.x * fill - border),
2633
+ box.Size.y - border,
2409
2634
  Theme.Widget.Border.Radius * scale
2410
2635
  );
2411
2636
  ctx.fill();
2412
- ctx.stroke();
2413
- const fill = Clamp01((this.#value - this.#min) / (this.#max - this.#min));
2414
- ctx.fillStyle = this.#fillColor;
2415
- ctx.beginPath();
2416
- ctx.roundRect(
2417
- position.x + scaledBorderThickness / 2,
2418
- position.y + scaledBorderThickness / 2,
2419
- scaledWidth * fill - scaledBorderThickness,
2420
- scaledHeight - scaledBorderThickness,
2421
- Theme.Widget.Border.Radius * scale
2422
- );
2423
- ctx.fill();
2424
- ctx.textAlign = "center" /* Center */;
2425
- ctx.textBaseline = "middle" /* Middle */;
2426
- this.#textStyle.setupStyle(ctx, scale);
2427
- ctx.fillText(
2428
- this.#text,
2429
- position.x + scaledWidth / 2,
2430
- position.y + scaledHeight / 2
2431
- );
2432
- if (mousePosition !== void 0) {
2433
- CopyVector2(this.#lastMousePosition, mousePosition);
2434
- if (this.#clicking) {
2435
- const min = position.x + scaledBorderThickness / 2;
2436
- const max = min + scaledWidth - scaledBorderThickness;
2437
- const p = Clamp01((this.#lastMousePosition.x - min) / (max - min));
2438
- let value = p * (this.#max - this.#min) + this.#min;
2439
- if (this.#snapIncrement !== void 0) {
2440
- value = Math.round(value / this.#snapIncrement) * this.#snapIncrement;
2441
- }
2442
- this.SetValue(value);
2443
- }
2637
+ drawFieldText(ctx, box, scale, {
2638
+ input: true,
2639
+ hovered: false,
2640
+ label: this.label(),
2641
+ value: formatNumber(Number(this.value().toFixed(3)))
2642
+ });
2643
+ }
2644
+ onPointerDown(pointer) {
2645
+ this.#startValue = this.value();
2646
+ this.input(this.#valueAt(pointer.position.x));
2647
+ }
2648
+ onPointerMove(pointer) {
2649
+ this.input(this.#valueAt(pointer.position.x));
2650
+ }
2651
+ onPointerUp() {
2652
+ this.commit(this.value(), this.#startValue);
2653
+ }
2654
+ #valueAt(x) {
2655
+ const box = this.box();
2656
+ const border = Theme.Widget.Border.Size * this.scale();
2657
+ const min = box.Position.x + border / 2;
2658
+ const max = min + box.Size.x - border;
2659
+ const p = Clamp01((x - min) / (max - min));
2660
+ let value = p * (this.#max - this.#min) + this.#min;
2661
+ if (this.#snapIncrement !== void 0) {
2662
+ value = Math.round(value / this.#snapIncrement) * this.#snapIncrement;
2444
2663
  }
2445
- return {
2446
- Position: { x: position.x, y: position.y },
2447
- Size: {
2448
- x: scaledWidth,
2449
- y: scaledHeight
2450
- }
2451
- };
2664
+ return Clamp(value, this.#min, this.#max);
2452
2665
  }
2453
2666
  };
2454
2667
 
2455
- // src/popups/string.ts
2456
- function SetStringPopup(config) {
2457
- let input = null;
2458
- return new Popup({
2459
- title: config.title,
2460
- options: ["Set", "Cancel"],
2461
- content: () => {
2462
- const container = document.createElement("div");
2463
- input = document.createElement("input");
2464
- input.value = config.startingValue;
2465
- container.append(input);
2466
- return container;
2467
- },
2468
- onClose: (button) => {
2469
- if (button !== "Set" || input === null) {
2470
- if (config.onCancel) {
2471
- config.onCancel();
2472
- }
2473
- return;
2474
- }
2475
- config.onUpdate(input.value);
2476
- }
2477
- });
2478
- }
2479
-
2480
2668
  // src/widgets/string.ts
2481
- var StringWidget = class {
2482
- #value;
2483
- #idleStyle;
2484
- #hoverStyle;
2485
- #callback;
2486
- #node;
2487
- #nodeProperty;
2669
+ var StringWidget = class extends ValueWidget {
2488
2670
  constructor(node, config) {
2489
- this.#value = "";
2490
- this.#node = node;
2491
- this.#nodeProperty = config?.property;
2492
- this.#idleStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.textBoxStyle, {
2493
- box: {
2494
- color: Theme.Widget.BackgroundColor,
2495
- border: {
2496
- size: Theme.Widget.Border.Size,
2497
- color: Theme.Widget.Border.Color
2498
- },
2499
- radius: Theme.Widget.Border.Radius
2500
- },
2501
- text: { color: Theme.Widget.FontColor }
2502
- }));
2503
- this.#hoverStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.textBoxStyle, {
2504
- box: {
2505
- color: Theme.Widget.Hover.BackgroundColor,
2506
- border: {
2507
- size: Theme.Widget.Border.Size,
2508
- color: Theme.Widget.Border.Color
2509
- },
2510
- radius: Theme.Widget.Border.Radius
2511
- },
2512
- text: { color: Theme.Widget.FontColor }
2513
- }));
2514
- this.Set(startingWidgetValue(node, this.#nodeProperty, config?.value, ""));
2515
- this.#callback = config?.callback;
2516
- if (this.#nodeProperty !== void 0) {
2517
- this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
2518
- this.Set(newVal);
2519
- });
2520
- }
2671
+ super(node, config, "");
2521
2672
  }
2522
- Size() {
2523
- return { "x": width, "y": height };
2524
- }
2525
- Set(value) {
2526
- if (this.#value === value) {
2527
- return;
2528
- }
2529
- this.#value = value;
2530
- if (this.#nodeProperty !== void 0) {
2531
- this.#node.setProperty(this.#nodeProperty, this.#value);
2532
- }
2533
- if (this.#callback !== void 0) {
2534
- this.#callback(this.#value);
2535
- }
2673
+ Cursor() {
2674
+ return "text" /* Text */;
2536
2675
  }
2537
- ClickStart() {
2538
- }
2539
- ClickEnd() {
2540
- const popup = SetStringPopup({
2541
- title: "Set String",
2542
- startingValue: this.#value,
2543
- onUpdate: (value) => {
2544
- this.Set(value);
2545
- }
2676
+ render(ctx, box, scale) {
2677
+ drawField(ctx, box, scale, {
2678
+ input: true,
2679
+ hovered: this.hovered(),
2680
+ label: this.label(),
2681
+ value: this.value()
2546
2682
  });
2547
- popup.Show();
2548
2683
  }
2549
- Draw(ctx, position, scale, mousePosition) {
2550
- const box = {
2551
- Position: { x: 0, y: 0 },
2552
- Size: {
2553
- x: width * scale,
2554
- y: height * scale
2555
- }
2556
- };
2557
- CopyVector2(box.Position, position);
2558
- let style = this.#idleStyle;
2559
- if (mousePosition !== void 0) {
2560
- if (InBox(box, mousePosition)) {
2561
- style = this.#hoverStyle;
2562
- }
2563
- }
2564
- style.DrawUnderline(ctx, box, scale, fitString(ctx, this.#value, box.Size.x - 20 * scale));
2565
- return box;
2684
+ onClick() {
2685
+ const before = this.value();
2686
+ editInPlace({
2687
+ canvas: this.canvas(),
2688
+ box: this.box(),
2689
+ scale: this.scale(),
2690
+ value: before,
2691
+ onCommit: (text) => this.commit(text, before)
2692
+ });
2566
2693
  }
2567
2694
  };
2568
2695
 
2569
2696
  // src/widgets/toggle.ts
2570
- var ToggleStyle = class {
2571
- #idleStyle;
2572
- #hoverStyle;
2573
- #lightColor;
2574
- #lightBorderColor;
2575
- #lightBlur;
2576
- #box;
2577
- constructor(config) {
2578
- this.#idleStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.idle, {
2579
- box: {
2580
- color: Theme.Widget.BackgroundColor,
2581
- border: {
2582
- size: Theme.Widget.Border.Size,
2583
- color: Theme.Widget.Border.Color
2584
- }
2585
- },
2586
- text: { color: Theme.Widget.FontColor }
2587
- }));
2588
- this.#hoverStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.hover, {
2589
- box: {
2590
- color: Theme.Widget.Hover.BackgroundColor,
2591
- border: {
2592
- size: Theme.Widget.Border.Size,
2593
- color: Theme.Widget.Border.Color
2594
- }
2595
- },
2596
- text: { color: Theme.Widget.FontColor }
2597
- }));
2598
- this.#lightColor = config?.lightColor === void 0 ? "#222222" : config.lightColor;
2599
- this.#lightBorderColor = config?.lightBorderColor === void 0 ? "black" : config.lightBorderColor;
2600
- this.#lightBlur = config?.lightBlur;
2601
- this.#box = { Position: { x: 0, y: 0 }, Size: { x: 0, y: 0 } };
2602
- }
2603
- Draw(ctx, pos, scale, text, mousePosition) {
2604
- const scaledWidth = width * scale;
2605
- const scaledHeight = height * scale;
2606
- this.#box.Size.x = scaledWidth;
2607
- this.#box.Size.y = scaledHeight;
2608
- CopyVector2(this.#box.Position, pos);
2609
- let style = this.#idleStyle;
2610
- if (mousePosition !== void 0) {
2611
- if (InBox(this.#box, mousePosition)) {
2612
- style = this.#hoverStyle;
2613
- }
2614
- }
2615
- style.Draw(ctx, this.#box, scale, text);
2616
- const lightScale = Math.min(scaledWidth, scaledHeight);
2617
- ctx.strokeStyle = this.#lightBorderColor;
2618
- ctx.fillStyle = this.#lightColor;
2619
- ctx.beginPath();
2620
- ctx.roundRect(
2621
- pos.x + lightScale * 0.2,
2622
- pos.y + lightScale * 0.2,
2623
- lightScale * 0.6,
2624
- lightScale * 0.6,
2625
- Theme.Widget.Border.Radius * scale
2626
- );
2627
- if (this.#lightBlur) {
2628
- ctx.shadowBlur = this.#lightBlur * scale;
2629
- ctx.shadowColor = this.#lightColor;
2630
- }
2631
- ctx.fill();
2632
- if (this.#lightBlur) {
2633
- ctx.shadowBlur = 0;
2634
- }
2635
- return this.#box;
2636
- }
2637
- };
2638
- var ToggleWidget = class {
2639
- #node;
2640
- #nodeProperty;
2641
- #value;
2642
- #text;
2643
- #enabledStyle;
2644
- #disabledStyle;
2645
- #callback;
2697
+ var ToggleWidget = class extends ValueWidget {
2698
+ #onColor;
2699
+ #offColor;
2646
2700
  constructor(node, config) {
2647
- this.#value = false;
2648
- this.#node = node;
2649
- this.#nodeProperty = config?.property;
2650
- this.#text = config?.text === void 0 ? "Toggle" : config?.text;
2651
- this.#enabledStyle = new ToggleStyle({
2652
- idle: TextBoxStyleWithFallback(config?.enabledStyle?.idle, {
2653
- box: {
2654
- color: Theme.Widget.BackgroundColor,
2655
- border: {
2656
- size: Theme.Widget.Border.Size,
2657
- color: Theme.Widget.Border.Color
2658
- }
2659
- },
2660
- text: { color: Theme.Widget.FontColor }
2661
- }),
2662
- lightBorderColor: config?.enabledStyle?.lightBorderColor,
2663
- lightColor: config?.enabledStyle?.lightColor === void 0 ? "#00FF00" : config?.enabledStyle?.lightColor,
2664
- lightBlur: config?.enabledStyle?.lightBlur === void 0 ? 15 : config?.enabledStyle?.lightBlur
2665
- });
2666
- this.#disabledStyle = new ToggleStyle({
2667
- idle: TextBoxStyleWithFallback(config?.disabledStyle?.idle, {
2668
- box: {
2669
- color: Theme.Widget.BackgroundColor,
2670
- border: {
2671
- size: Theme.Widget.Border.Size,
2672
- color: Theme.Widget.Border.Color
2673
- }
2674
- },
2675
- text: { color: Theme.Widget.FontColor }
2676
- }),
2677
- lightBorderColor: config?.disabledStyle?.lightBorderColor,
2678
- lightColor: config?.disabledStyle?.lightColor === void 0 ? "#004400" : config?.enabledStyle?.lightColor
2679
- });
2680
- this.Set(startingWidgetValue(node, this.#nodeProperty, config?.value, false));
2681
- this.#callback = config?.callback;
2682
- if (this.#nodeProperty !== void 0) {
2683
- this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
2684
- this.Set(newVal);
2685
- });
2686
- }
2687
- }
2688
- Size() {
2689
- return { "x": width, "y": height };
2690
- }
2691
- Draw(ctx, position, scale, mousePosition) {
2692
- let style = this.#value ? this.#enabledStyle : this.#disabledStyle;
2693
- return style.Draw(ctx, position, scale, this.#text, mousePosition);
2701
+ super(node, { ...config, label: config?.label ?? "Toggle" }, false);
2702
+ this.#onColor = config?.onColor ?? "#00FF00";
2703
+ this.#offColor = config?.offColor ?? "#004400";
2694
2704
  }
2695
2705
  Toggle() {
2696
- this.Set(!this.#value);
2697
- }
2698
- Set(value) {
2699
- if (this.#value === value) {
2700
- return;
2701
- }
2702
- this.#value = value;
2703
- if (this.#nodeProperty !== void 0) {
2704
- this.#node.setProperty(this.#nodeProperty, this.#value);
2705
- }
2706
- if (this.#callback !== void 0) {
2707
- this.#callback(this.#value);
2706
+ const before = this.value();
2707
+ this.commit(!before, before);
2708
+ }
2709
+ render(ctx, box, scale) {
2710
+ drawFieldBox(ctx, box, scale, { hovered: this.hovered(), value: "" });
2711
+ const light = Math.min(box.Size.x, box.Size.y) * 0.6;
2712
+ const inset = (box.Size.y - light) / 2;
2713
+ const on = this.value();
2714
+ ctx.fillStyle = on ? this.#onColor : this.#offColor;
2715
+ ctx.beginPath();
2716
+ ctx.roundRect(box.Position.x + inset, box.Position.y + inset, light, light, Theme.Widget.Border.Radius * scale);
2717
+ if (on) {
2718
+ ctx.shadowBlur = 15 * scale;
2719
+ ctx.shadowColor = this.#onColor;
2708
2720
  }
2721
+ ctx.fill();
2722
+ ctx.shadowBlur = 0;
2723
+ const textLeft = box.Position.x + inset * 2 + light;
2724
+ ctx.font = widgetFont(scale);
2725
+ ctx.textAlign = "center";
2726
+ ctx.textBaseline = "middle";
2727
+ ctx.fillStyle = Theme.Widget.FontColor;
2728
+ const room = box.Position.x + box.Size.x - textLeft - inset;
2729
+ ctx.fillText(
2730
+ fitString(ctx, this.label(), room),
2731
+ textLeft + room / 2,
2732
+ box.Position.y + box.Size.y / 2
2733
+ );
2709
2734
  }
2710
- ClickStart() {
2735
+ onClick() {
2711
2736
  this.Toggle();
2712
2737
  }
2713
- ClickEnd() {
2714
- }
2715
2738
  };
2716
2739
 
2717
2740
  // src/widgets/image.ts
2718
- var ImageWidget = class {
2741
+ var ImageWidget = class extends WidgetBase {
2719
2742
  #url;
2743
+ #ownedUrl;
2720
2744
  #image;
2721
2745
  #maxWidth;
2722
2746
  #maxHeight;
2723
2747
  constructor(config) {
2748
+ super();
2724
2749
  this.#maxWidth = config?.maxWidth === void 0 ? 150 : config?.maxWidth;
2725
2750
  this.#maxHeight = config?.maxHeight === void 0 ? 150 : config?.maxHeight;
2726
2751
  if (config?.image) {
@@ -2730,16 +2755,23 @@ var ImageWidget = class {
2730
2755
  }
2731
2756
  }
2732
2757
  SetBlob(blob) {
2733
- const urlCreator = window.URL || window.webkitURL;
2734
- const imageUrl = urlCreator.createObjectURL(blob);
2735
- this.SetUrl(imageUrl);
2758
+ const url = URL.createObjectURL(blob);
2759
+ this.SetUrl(url);
2760
+ this.#ownedUrl = url;
2736
2761
  }
2737
2762
  SetUrl(url) {
2763
+ if (this.#ownedUrl !== void 0) {
2764
+ URL.revokeObjectURL(this.#ownedUrl);
2765
+ this.#ownedUrl = void 0;
2766
+ }
2738
2767
  this.#image = void 0;
2739
2768
  this.#url = url;
2740
2769
  const img = document.createElement("img");
2741
2770
  img.src = url;
2742
2771
  img.onload = () => {
2772
+ if (this.#url !== url) {
2773
+ return;
2774
+ }
2743
2775
  this.#image = img;
2744
2776
  requestRender();
2745
2777
  };
@@ -2747,6 +2779,9 @@ var ImageWidget = class {
2747
2779
  console.log("error loading image:", url, event);
2748
2780
  };
2749
2781
  }
2782
+ Cursor() {
2783
+ return "default" /* Default */;
2784
+ }
2750
2785
  Size() {
2751
2786
  if (this.#image === void 0) {
2752
2787
  return { "x": 0, "y": 0 };
@@ -2766,88 +2801,178 @@ var ImageWidget = class {
2766
2801
  "y": adjust * this.#image.height
2767
2802
  };
2768
2803
  }
2769
- ClickStart() {
2770
- }
2771
- ClickEnd() {
2772
- }
2773
- Draw(ctx, position, scale, mousePosition) {
2774
- const size = this.Size();
2775
- const box = {
2776
- Position: { x: 0, y: 0 },
2777
- Size: {
2778
- x: size.x * scale,
2779
- y: size.y * scale
2780
- }
2781
- };
2782
- CopyVector2(box.Position, position);
2804
+ render(ctx, box, scale) {
2783
2805
  if (!this.#image) {
2784
- return box;
2806
+ return;
2785
2807
  }
2808
+ const size = this.Size();
2809
+ const w = size.x * scale;
2810
+ const h = size.y * scale;
2786
2811
  ctx.drawImage(
2787
2812
  this.#image,
2788
- position.x,
2789
- position.y,
2790
- box.Size.x,
2791
- box.Size.y
2813
+ box.Position.x + (box.Size.x - w) / 2,
2814
+ box.Position.y,
2815
+ w,
2816
+ h
2792
2817
  );
2793
- return box;
2794
2818
  }
2795
2819
  };
2796
2820
 
2797
2821
  // src/widgets/text.ts
2798
- var TextWidget = class {
2822
+ var TextWidget = class extends WidgetBase {
2799
2823
  #value;
2800
- #callback;
2801
- #node;
2802
- #nodeProperty;
2803
2824
  #size;
2825
+ #styleConfig;
2826
+ #styledForTheme = -1;
2827
+ #followTheme() {
2828
+ if (this.#styledForTheme === themeVersion()) {
2829
+ return;
2830
+ }
2831
+ this.#styledForTheme = themeVersion();
2832
+ const style = this.#value.style();
2833
+ style.setColor(this.#styleConfig?.color ?? Theme.Widget.FontColor);
2834
+ style.setSize(this.#styleConfig?.size ?? Theme.Note.FontSize);
2835
+ }
2804
2836
  constructor(node, config) {
2837
+ super();
2805
2838
  this.#size = Zero();
2806
- this.#node = node;
2807
- this.#nodeProperty = config?.property;
2808
- this.#value = new Text("", TextStyleFallback(config?.textBoxStyle, {
2809
- font: Theme.FontFamily,
2810
- color: Theme.Widget.FontColor,
2811
- size: Theme.Note.FontSize
2812
- }));
2813
- this.Set(config?.value === void 0 ? "" : config?.value);
2814
- this.#callback = config?.callback;
2815
- if (this.#nodeProperty !== void 0) {
2816
- this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
2817
- this.Set(newVal);
2818
- });
2839
+ this.#styleConfig = config?.textBoxStyle;
2840
+ this.#value = new Text("", TextStyleFallback(config?.textBoxStyle, {}));
2841
+ this.#followTheme();
2842
+ this.setText(config?.value ?? "");
2843
+ const property = config?.property;
2844
+ if (property !== void 0) {
2845
+ const existing = node.getProperty(property);
2846
+ if (existing !== void 0 && existing !== null) {
2847
+ this.setText(String(existing));
2848
+ }
2849
+ node.addPropertyChangeListener(property, (_, next) => this.setText(String(next)));
2819
2850
  }
2820
2851
  }
2852
+ Cursor() {
2853
+ return "default" /* Default */;
2854
+ }
2821
2855
  Size() {
2822
2856
  return this.#size;
2823
2857
  }
2824
- Set(value) {
2858
+ text() {
2859
+ return this.#value.get();
2860
+ }
2861
+ setText(value) {
2825
2862
  if (this.#value.get() === value) {
2826
2863
  return;
2827
2864
  }
2828
2865
  this.#value.set(value);
2829
- if (this.#nodeProperty !== void 0) {
2830
- this.#node.setProperty(this.#nodeProperty, value);
2866
+ requestRender();
2867
+ }
2868
+ render(ctx, box, scale) {
2869
+ this.#followTheme();
2870
+ this.#value.size(ctx, 1, this.#size);
2871
+ ctx.textAlign = "left";
2872
+ this.#value.render(ctx, scale, box.Position);
2873
+ }
2874
+ };
2875
+
2876
+ // src/widgets/row.ts
2877
+ var RowWidget = class extends WidgetBase {
2878
+ #children;
2879
+ #grow;
2880
+ #gap;
2881
+ #boxes = [];
2882
+ #captured = null;
2883
+ #capturedBox = null;
2884
+ #mouse;
2885
+ constructor(children, config) {
2886
+ super();
2887
+ this.#children = children;
2888
+ this.#grow = config?.grow ?? children.map(() => 1);
2889
+ this.#gap = config?.gap ?? Theme.Widget.Gap;
2890
+ }
2891
+ children() {
2892
+ return this.#children;
2893
+ }
2894
+ Size() {
2895
+ const shown = this.#shown();
2896
+ let x = 0;
2897
+ let y = 0;
2898
+ for (const child of shown) {
2899
+ const size = child.Size();
2900
+ x += size.x;
2901
+ y = Math.max(y, size.y);
2902
+ }
2903
+ x += Math.max(0, shown.length - 1) * this.#gap;
2904
+ return { x, y };
2905
+ }
2906
+ Cursor() {
2907
+ const over = this.#captured ?? this.#childAt(this.#mouse);
2908
+ return over?.Cursor?.() ?? "default" /* Default */;
2909
+ }
2910
+ Draw(ctx, box, scale, mousePosition) {
2911
+ this.#mouse = mousePosition;
2912
+ super.Draw(ctx, box, scale, mousePosition);
2913
+ }
2914
+ render(ctx, box, scale) {
2915
+ const shown = this.#shown();
2916
+ const gap = this.#gap * scale;
2917
+ const natural = shown.reduce((sum, child) => sum + child.Size().x * scale, 0);
2918
+ const extra = Math.max(0, box.Size.x - natural - gap * Math.max(0, shown.length - 1));
2919
+ const totalGrow = shown.reduce((sum, child) => sum + this.#growOf(child), 0);
2920
+ this.#boxes = this.#children.map(() => null);
2921
+ let x = box.Position.x;
2922
+ for (const child of shown) {
2923
+ const size = child.Size();
2924
+ const share = totalGrow > 0 ? extra * (this.#growOf(child) / totalGrow) : 0;
2925
+ const childBox = {
2926
+ Position: { x, y: box.Position.y + (box.Size.y - size.y * scale) / 2 },
2927
+ Size: { x: size.x * scale + share, y: size.y * scale }
2928
+ };
2929
+ this.#boxes[this.#children.indexOf(child)] = childBox;
2930
+ child.Draw(ctx, childBox, scale, this.#captured === null || this.#captured === child ? this.#mouse : void 0);
2931
+ x += childBox.Size.x + gap;
2831
2932
  }
2832
- if (this.#callback !== void 0) {
2833
- this.#callback(value);
2933
+ }
2934
+ onPointerDown(pointer) {
2935
+ const child = this.#childAt(pointer.position);
2936
+ if (child === null || child.Disabled?.()) {
2937
+ return;
2834
2938
  }
2939
+ this.#captured = child;
2940
+ this.#capturedBox = this.#boxes[this.#children.indexOf(child)];
2941
+ child.PointerDown?.(this.#forChild(pointer));
2835
2942
  }
2836
- ClickStart() {
2943
+ onPointerMove(pointer) {
2944
+ this.#captured?.PointerMove?.(this.#forChild(pointer));
2837
2945
  }
2838
- ClickEnd() {
2946
+ onPointerUp(pointer) {
2947
+ const child = this.#captured;
2948
+ const forChild = this.#forChild(pointer);
2949
+ this.#captured = null;
2950
+ this.#capturedBox = null;
2951
+ child?.PointerUp?.(forChild);
2839
2952
  }
2840
- Draw(ctx, position, scale, mousePosition) {
2841
- this.#value.size(ctx, scale, this.#size);
2842
- const box = {
2843
- Position: { x: 0, y: 0 },
2844
- Size: this.#size
2953
+ #forChild(pointer) {
2954
+ return {
2955
+ position: pointer.position,
2956
+ inside: this.#capturedBox !== null && InBox(this.#capturedBox, pointer.position)
2845
2957
  };
2846
- CopyVector2(box.Position, position);
2847
- this.#value.size(ctx, 1, this.#size);
2848
- ctx.textAlign = "left";
2849
- this.#value.render(ctx, scale, box.Position);
2850
- return box;
2958
+ }
2959
+ #childAt(position) {
2960
+ if (position === void 0) {
2961
+ return null;
2962
+ }
2963
+ for (let i = 0; i < this.#children.length; i++) {
2964
+ const box = this.#boxes[i];
2965
+ if (box && InBox(box, position)) {
2966
+ return this.#children[i];
2967
+ }
2968
+ }
2969
+ return null;
2970
+ }
2971
+ #shown() {
2972
+ return this.#children.filter((child) => !child.Hidden?.());
2973
+ }
2974
+ #growOf(child) {
2975
+ return this.#grow[this.#children.indexOf(child)] ?? 1;
2851
2976
  }
2852
2977
  };
2853
2978
 
@@ -2867,6 +2992,9 @@ var WidgetFactory = class {
2867
2992
  }
2868
2993
  return builder(node, config);
2869
2994
  }
2995
+ createFromConfig(node, config) {
2996
+ return this.create(node, config.type, config.config);
2997
+ }
2870
2998
  };
2871
2999
  var GlobalWidgetFactory = new WidgetFactory();
2872
3000
  GlobalWidgetFactory.register("button", (node, config) => new ButtonWidget(config));
@@ -2877,6 +3005,10 @@ GlobalWidgetFactory.register("string", (node, config) => new StringWidget(node,
2877
3005
  GlobalWidgetFactory.register("toggle", (node, config) => new ToggleWidget(node, config));
2878
3006
  GlobalWidgetFactory.register("image", (node, config) => new ImageWidget(config));
2879
3007
  GlobalWidgetFactory.register("text", (node, config) => new TextWidget(node, config));
3008
+ GlobalWidgetFactory.register("row", (node, config) => new RowWidget(
3009
+ (config?.widgets ?? []).map((child) => GlobalWidgetFactory.createFromConfig(node, child)),
3010
+ config
3011
+ ));
2880
3012
 
2881
3013
  // src/connection.ts
2882
3014
  var BACKWARD_PORT_STUB = 30;
@@ -2961,7 +3093,7 @@ var Connection = class {
2961
3093
  return;
2962
3094
  }
2963
3095
  if (this.#inNode !== null) {
2964
- const inPortBox = this.#inNode.inputPortPosition(this.#inNodePortIndex);
3096
+ const inPortBox = this.#inSlotBox();
2965
3097
  if (inPortBox === void 0) {
2966
3098
  return;
2967
3099
  }
@@ -3020,14 +3152,14 @@ var Connection = class {
3020
3152
  this.clearInput();
3021
3153
  this.clearOutput();
3022
3154
  }
3023
- setInput(node, portIndex, replace) {
3155
+ setInput(node, portIndex, replace, insertAt) {
3024
3156
  this.#inNode = node;
3025
3157
  this.#inNodePortIndex = portIndex;
3026
3158
  const port = this.#inNode.inputPort(portIndex);
3027
3159
  if (!!replace && port.connections().length > 0) {
3028
3160
  port.replaceConnection(this, 0);
3029
3161
  } else {
3030
- port.addConnection(this);
3162
+ port.addConnection(this, insertAt);
3031
3163
  }
3032
3164
  }
3033
3165
  setOutput(node, portIndex) {
@@ -3045,9 +3177,33 @@ var Connection = class {
3045
3177
  this.#outNode = null;
3046
3178
  this.#outNodePortIndex = -1;
3047
3179
  }
3180
+ /**
3181
+ * The part of the input port this wire lands on: the whole port, or its
3182
+ * own slot on a port holding several wires.
3183
+ */
3184
+ #inSlotBox() {
3185
+ if (this.#inNode === null) {
3186
+ return void 0;
3187
+ }
3188
+ const box = this.#inNode.inputPortPosition(this.#inNodePortIndex);
3189
+ const port = this.#inNode.inputPort(this.#inNodePortIndex);
3190
+ const slots = port.slots();
3191
+ if (box === void 0 || slots === 1) {
3192
+ return box;
3193
+ }
3194
+ const index = port.connections().indexOf(this);
3195
+ if (index === -1) {
3196
+ return box;
3197
+ }
3198
+ const height2 = box.Size.y / slots;
3199
+ return {
3200
+ Position: { x: box.Position.x, y: box.Position.y + index * height2 },
3201
+ Size: { x: box.Size.x, y: height2 }
3202
+ };
3203
+ }
3048
3204
  mouseOverPort(mousePosition) {
3049
3205
  if (this.#inNode !== null) {
3050
- const inPortBox = this.#inNode.inputPortPosition(this.#inNodePortIndex);
3206
+ const inPortBox = this.#inSlotBox();
3051
3207
  if (inPortBox !== void 0 && InBox(inPortBox, mousePosition)) {
3052
3208
  return this.#inNode.inputPort(this.#inNodePortIndex);
3053
3209
  }
@@ -3124,7 +3280,10 @@ var Camera = class {
3124
3280
  // src/organize.ts
3125
3281
  var widthSpacing = 100;
3126
3282
  var heightSpacing = 50;
3283
+ var laneSpacing = 25;
3284
+ var pieceSpacing = 150;
3127
3285
  var orderingSweeps = 8;
3286
+ var placementSweeps = 8;
3128
3287
  function topologicalOrder(sorting, upstream) {
3129
3288
  const visited = /* @__PURE__ */ new Set();
3130
3289
  const order = new Array();
@@ -3146,11 +3305,7 @@ function topologicalOrder(sorting, upstream) {
3146
3305
  continue;
3147
3306
  }
3148
3307
  visited.add(top.node);
3149
- const feeders = upstream.get(top.node);
3150
- if (feeders === void 0) {
3151
- continue;
3152
- }
3153
- for (const feeder of feeders) {
3308
+ for (const feeder of upstream.get(top.node) ?? []) {
3154
3309
  if (!visited.has(feeder)) {
3155
3310
  stack.push({ node: feeder, expanded: false });
3156
3311
  }
@@ -3159,43 +3314,103 @@ function topologicalOrder(sorting, upstream) {
3159
3314
  }
3160
3315
  return order;
3161
3316
  }
3317
+ function consumersOf(sorting, upstream) {
3318
+ const downstream = /* @__PURE__ */ new Map();
3319
+ for (const node of sorting) {
3320
+ downstream.set(node, []);
3321
+ }
3322
+ for (const node of sorting) {
3323
+ for (const feeder of upstream.get(node) ?? []) {
3324
+ const consumers = downstream.get(feeder);
3325
+ if (consumers !== void 0 && !consumers.includes(node)) {
3326
+ consumers.push(node);
3327
+ }
3328
+ }
3329
+ }
3330
+ return downstream;
3331
+ }
3162
3332
  function assignLayers(sorting, upstream) {
3163
- const order = topologicalOrder(sorting, upstream);
3333
+ const downstream = consumersOf(sorting, upstream);
3334
+ const order = topologicalOrder(sorting, downstream);
3164
3335
  const placed = /* @__PURE__ */ new Set();
3165
- const layerOf = /* @__PURE__ */ new Map();
3336
+ const rank = /* @__PURE__ */ new Map();
3166
3337
  for (const node of order) {
3167
- let layer = 0;
3168
- for (const feeder of upstream.get(node) ?? []) {
3169
- if (placed.has(feeder)) {
3170
- layer = Math.max(layer, layerOf.get(feeder) + 1);
3338
+ let r = 0;
3339
+ for (const consumer of downstream.get(node) ?? []) {
3340
+ if (placed.has(consumer)) {
3341
+ r = Math.max(r, rank.get(consumer) + 1);
3171
3342
  }
3172
3343
  }
3173
- layerOf.set(node, layer);
3344
+ rank.set(node, r);
3174
3345
  placed.add(node);
3175
3346
  }
3176
3347
  let deepest = 0;
3177
- for (const layer of layerOf.values()) {
3178
- deepest = Math.max(deepest, layer);
3348
+ for (const r of rank.values()) {
3349
+ deepest = Math.max(deepest, r);
3179
3350
  }
3180
- const layers = new Array(deepest + 1);
3181
- for (let i = 0; i < layers.length; i++) {
3182
- layers[i] = new Array();
3351
+ const layerOf = /* @__PURE__ */ new Map();
3352
+ for (const node of sorting) {
3353
+ layerOf.set(node, deepest - rank.get(node));
3183
3354
  }
3184
- for (const node of order) {
3185
- layers[layerOf.get(node)].push(node);
3355
+ for (const node of sorting) {
3356
+ const layer = layerOf.get(node);
3357
+ if ((downstream.get(node) ?? []).some((c) => layerOf.get(c) > layer)) {
3358
+ continue;
3359
+ }
3360
+ let pulled = -1;
3361
+ for (const feeder of upstream.get(node) ?? []) {
3362
+ const feederLayer = layerOf.get(feeder);
3363
+ if (feederLayer !== void 0 && feederLayer < layer) {
3364
+ pulled = Math.max(pulled, feederLayer + 1);
3365
+ }
3366
+ }
3367
+ if (pulled >= 0) {
3368
+ layerOf.set(node, pulled);
3369
+ }
3370
+ }
3371
+ const used = [...new Set(layerOf.values())].sort((a, b) => a - b);
3372
+ const compact = /* @__PURE__ */ new Map();
3373
+ used.forEach((layer, i) => compact.set(layer, i));
3374
+ const layers = used.map(() => new Array());
3375
+ for (const node of sorting) {
3376
+ const layer = compact.get(layerOf.get(node));
3377
+ layerOf.set(node, layer);
3378
+ layers[layer].push(node);
3186
3379
  }
3187
3380
  return { layers, layerOf };
3188
3381
  }
3189
- function orderWithinLayers(layout, upstream, downstream) {
3382
+ function crossingsOf(layout, downstream) {
3190
3383
  const slotOf = /* @__PURE__ */ new Map();
3191
- const recordSlots = () => {
3192
- for (const layer of layout.layers) {
3193
- for (let i = 0; i < layer.length; i++) {
3194
- slotOf.set(layer[i], i);
3384
+ for (const layer of layout.layers) {
3385
+ layer.forEach((node, slot) => slotOf.set(node, slot));
3386
+ }
3387
+ let total = 0;
3388
+ for (let i = 0; i + 1 < layout.layers.length; i++) {
3389
+ const spans = new Array();
3390
+ for (const node of layout.layers[i]) {
3391
+ for (const consumer of downstream.get(node) ?? []) {
3392
+ if (layout.layerOf.get(consumer) === i + 1) {
3393
+ spans.push([slotOf.get(node), slotOf.get(consumer)]);
3394
+ }
3195
3395
  }
3196
3396
  }
3197
- };
3198
- recordSlots();
3397
+ for (let a = 0; a < spans.length; a++) {
3398
+ for (let b = a + 1; b < spans.length; b++) {
3399
+ if ((spans[a][0] - spans[b][0]) * (spans[a][1] - spans[b][1]) < 0) {
3400
+ total++;
3401
+ }
3402
+ }
3403
+ }
3404
+ }
3405
+ return total;
3406
+ }
3407
+ function orderWithinLayers(layout, upstream, downstream) {
3408
+ const slotOf = /* @__PURE__ */ new Map();
3409
+ for (const layer of layout.layers) {
3410
+ for (let i = 0; i < layer.length; i++) {
3411
+ slotOf.set(layer[i], i);
3412
+ }
3413
+ }
3199
3414
  const meanSlot = (node, neighbours, layer) => {
3200
3415
  let total = 0;
3201
3416
  let count = 0;
@@ -3206,31 +3421,386 @@ function orderWithinLayers(layout, upstream, downstream) {
3206
3421
  total += slotOf.get(neighbour);
3207
3422
  count++;
3208
3423
  }
3209
- return count === 0 ? -1 : total / count;
3424
+ return count === 0 ? -1 : total / count;
3425
+ };
3426
+ const sweep = (neighbours, towards) => {
3427
+ for (let i = 0; i < layout.layers.length; i++) {
3428
+ const index = towards > 0 ? i : layout.layers.length - 1 - i;
3429
+ const adjacent = index + towards;
3430
+ if (adjacent < 0 || adjacent >= layout.layers.length) {
3431
+ continue;
3432
+ }
3433
+ const layer = layout.layers[index];
3434
+ const keys = /* @__PURE__ */ new Map();
3435
+ for (const node of layer) {
3436
+ const mean = meanSlot(node, neighbours, adjacent);
3437
+ keys.set(node, mean === -1 ? slotOf.get(node) : mean);
3438
+ }
3439
+ layer.sort((a, b) => keys.get(a) - keys.get(b));
3440
+ for (let s = 0; s < layer.length; s++) {
3441
+ slotOf.set(layer[s], s);
3442
+ }
3443
+ }
3444
+ };
3445
+ let best = layout.layers.map((layer) => layer.slice());
3446
+ let fewest = crossingsOf(layout, downstream);
3447
+ for (let i = 0; i < orderingSweeps && fewest > 0; i++) {
3448
+ sweep(upstream, -1);
3449
+ sweep(downstream, 1);
3450
+ const crossings = crossingsOf(layout, downstream);
3451
+ if (crossings < fewest) {
3452
+ fewest = crossings;
3453
+ best = layout.layers.map((layer) => layer.slice());
3454
+ }
3455
+ }
3456
+ for (let i = 0; i < best.length; i++) {
3457
+ layout.layers[i] = best[i];
3458
+ }
3459
+ }
3460
+ function findPieces(sorting, upstream, downstream) {
3461
+ const rankOf = /* @__PURE__ */ new Map();
3462
+ sorting.forEach((node, i) => rankOf.set(node, i));
3463
+ const seen = /* @__PURE__ */ new Set();
3464
+ const pieces = new Array();
3465
+ for (const start of sorting) {
3466
+ if (seen.has(start)) {
3467
+ continue;
3468
+ }
3469
+ seen.add(start);
3470
+ const piece = new Array();
3471
+ const stack = [start];
3472
+ while (stack.length > 0) {
3473
+ const node = stack.pop();
3474
+ piece.push(node);
3475
+ for (const neighbour of [...upstream.get(node) ?? [], ...downstream.get(node) ?? []]) {
3476
+ if (rankOf.has(neighbour) && !seen.has(neighbour)) {
3477
+ seen.add(neighbour);
3478
+ stack.push(neighbour);
3479
+ }
3480
+ }
3481
+ }
3482
+ piece.sort((a, b) => rankOf.get(a) - rankOf.get(b));
3483
+ pieces.push(piece);
3484
+ }
3485
+ return pieces;
3486
+ }
3487
+ function treeRoot(piece, downstream) {
3488
+ let root;
3489
+ for (const node of piece) {
3490
+ const consumers = downstream.get(node) ?? [];
3491
+ if (consumers.length > 1) {
3492
+ return void 0;
3493
+ }
3494
+ if (consumers.length === 0) {
3495
+ if (root !== void 0) {
3496
+ return void 0;
3497
+ }
3498
+ root = node;
3499
+ }
3500
+ }
3501
+ return root;
3502
+ }
3503
+ function columnXs(widths) {
3504
+ const xs = new Array();
3505
+ let x = 0;
3506
+ for (const width2 of widths) {
3507
+ xs.push(x);
3508
+ x += width2 + widthSpacing;
3509
+ }
3510
+ return xs;
3511
+ }
3512
+ function layoutTree(root, upstream, sizes) {
3513
+ const children = (node) => [...new Set(upstream.get(node) ?? [])];
3514
+ const depthOf = /* @__PURE__ */ new Map([[root, 0]]);
3515
+ const preorder = new Array();
3516
+ const stack = [root];
3517
+ let deepest = 0;
3518
+ while (stack.length > 0) {
3519
+ const node = stack.pop();
3520
+ preorder.push(node);
3521
+ const kids = children(node);
3522
+ for (let i = kids.length - 1; i >= 0; i--) {
3523
+ const depth = depthOf.get(node) + 1;
3524
+ depthOf.set(kids[i], depth);
3525
+ deepest = Math.max(deepest, depth);
3526
+ stack.push(kids[i]);
3527
+ }
3528
+ }
3529
+ const widths = new Array(deepest + 1).fill(0);
3530
+ for (const [node, depth] of depthOf) {
3531
+ widths[deepest - depth] = Math.max(widths[deepest - depth], sizes.get(node).x);
3532
+ }
3533
+ const xs = columnXs(widths);
3534
+ const band = /* @__PURE__ */ new Map();
3535
+ const topInBand = /* @__PURE__ */ new Map();
3536
+ const childOffset = /* @__PURE__ */ new Map();
3537
+ for (let i = preorder.length - 1; i >= 0; i--) {
3538
+ const node = preorder[i];
3539
+ const height2 = sizes.get(node).y;
3540
+ const kids = children(node);
3541
+ if (kids.length === 0) {
3542
+ band.set(node, height2);
3543
+ topInBand.set(node, 0);
3544
+ continue;
3545
+ }
3546
+ let offset = 0;
3547
+ for (const kid of kids) {
3548
+ childOffset.set(kid, offset);
3549
+ offset += band.get(kid) + heightSpacing;
3550
+ }
3551
+ const centre = (kid) => childOffset.get(kid) + topInBand.get(kid) + sizes.get(kid).y / 2;
3552
+ let top = (centre(kids[0]) + centre(kids[kids.length - 1])) / 2 - height2 / 2;
3553
+ let shift = 0;
3554
+ if (top < 0) {
3555
+ shift = -top;
3556
+ top = 0;
3557
+ for (const kid of kids) {
3558
+ childOffset.set(kid, childOffset.get(kid) + shift);
3559
+ }
3560
+ }
3561
+ band.set(node, Math.max(offset - heightSpacing + shift, top + height2));
3562
+ topInBand.set(node, top);
3563
+ }
3564
+ const positions = /* @__PURE__ */ new Map();
3565
+ const bandTop = /* @__PURE__ */ new Map([[root, 0]]);
3566
+ for (const node of preorder) {
3567
+ const column = deepest - depthOf.get(node);
3568
+ const size = sizes.get(node);
3569
+ positions.set(node, {
3570
+ x: xs[column] + (widths[column] - size.x) / 2,
3571
+ y: bandTop.get(node) + topInBand.get(node)
3572
+ });
3573
+ for (const kid of children(node)) {
3574
+ bandTop.set(kid, bandTop.get(node) + childOffset.get(kid));
3575
+ }
3576
+ }
3577
+ return positions;
3578
+ }
3579
+ function isotonic(targets) {
3580
+ const blocks = new Array();
3581
+ for (const target of targets) {
3582
+ blocks.push({ sum: target, count: 1 });
3583
+ while (blocks.length > 1) {
3584
+ const last = blocks[blocks.length - 1];
3585
+ const before = blocks[blocks.length - 2];
3586
+ if (before.sum / before.count <= last.sum / last.count) {
3587
+ break;
3588
+ }
3589
+ before.sum += last.sum;
3590
+ before.count += last.count;
3591
+ blocks.pop();
3592
+ }
3593
+ }
3594
+ const result = new Array();
3595
+ for (const block of blocks) {
3596
+ for (let i = 0; i < block.count; i++) {
3597
+ result.push(block.sum / block.count);
3598
+ }
3599
+ }
3600
+ return result;
3601
+ }
3602
+ function median(values) {
3603
+ const sorted = values.slice().sort((a, b) => a - b);
3604
+ const middle = Math.floor(sorted.length / 2);
3605
+ return sorted.length % 2 === 1 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
3606
+ }
3607
+ function layoutLayered(piece, upstream, downstream, sizes) {
3608
+ const layout = assignLayers(piece, upstream);
3609
+ const up = /* @__PURE__ */ new Map();
3610
+ const down = /* @__PURE__ */ new Map();
3611
+ const heightOf = /* @__PURE__ */ new Map();
3612
+ for (const node of piece) {
3613
+ up.set(node, [...new Set(upstream.get(node) ?? [])]);
3614
+ down.set(node, [...new Set(downstream.get(node) ?? [])]);
3615
+ heightOf.set(node, sizes.get(node).y);
3616
+ }
3617
+ let nextLane = -1;
3618
+ for (const from of piece) {
3619
+ const fromLayer = layout.layerOf.get(from);
3620
+ const consumers = down.get(from);
3621
+ for (let c = 0; c < consumers.length; c++) {
3622
+ const to = consumers[c];
3623
+ const toLayer = layout.layerOf.get(to);
3624
+ if (toLayer - fromLayer < 2) {
3625
+ continue;
3626
+ }
3627
+ let previous = from;
3628
+ for (let layer = fromLayer + 1; layer < toLayer; layer++) {
3629
+ const lane = nextLane--;
3630
+ layout.layerOf.set(lane, layer);
3631
+ layout.layers[layer].push(lane);
3632
+ heightOf.set(lane, 0);
3633
+ up.set(lane, [previous]);
3634
+ down.set(lane, []);
3635
+ if (previous === from) {
3636
+ consumers[c] = lane;
3637
+ } else {
3638
+ down.get(previous).push(lane);
3639
+ }
3640
+ previous = lane;
3641
+ }
3642
+ down.get(previous).push(to);
3643
+ const feeders = up.get(to);
3644
+ feeders[feeders.indexOf(from)] = previous;
3645
+ }
3646
+ }
3647
+ const sequence = /* @__PURE__ */ new Map();
3648
+ for (const sink of piece) {
3649
+ if (down.get(sink).length > 0) {
3650
+ continue;
3651
+ }
3652
+ const stack = [sink];
3653
+ while (stack.length > 0) {
3654
+ const node = stack.pop();
3655
+ if (sequence.has(node)) {
3656
+ continue;
3657
+ }
3658
+ sequence.set(node, sequence.size);
3659
+ const feeders = up.get(node) ?? [];
3660
+ for (let i = feeders.length - 1; i >= 0; i--) {
3661
+ stack.push(feeders[i]);
3662
+ }
3663
+ }
3664
+ }
3665
+ for (const layer of layout.layers) {
3666
+ layer.sort((a, b) => (sequence.get(a) ?? Infinity) - (sequence.get(b) ?? Infinity));
3667
+ }
3668
+ orderWithinLayers(layout, up, down);
3669
+ const gapAfter = (above, below) => above < 0 || below < 0 ? laneSpacing : heightSpacing;
3670
+ const topOf = /* @__PURE__ */ new Map();
3671
+ for (const layer of layout.layers) {
3672
+ let y = 0;
3673
+ for (let i = 0; i < layer.length; i++) {
3674
+ topOf.set(layer[i], y);
3675
+ y += heightOf.get(layer[i]) + (i + 1 < layer.length ? gapAfter(layer[i], layer[i + 1]) : 0);
3676
+ }
3677
+ for (const node of layer) {
3678
+ topOf.set(node, topOf.get(node) - y / 2);
3679
+ }
3680
+ }
3681
+ const centreOf = (node) => topOf.get(node) + heightOf.get(node) / 2;
3682
+ const place = (layer, neighbours, adjacent) => {
3683
+ const offsets = new Array();
3684
+ const targets = new Array();
3685
+ let offset = 0;
3686
+ for (let i = 0; i < layer.length; i++) {
3687
+ const node = layer[i];
3688
+ const centres = (neighbours.get(node) ?? []).filter((n) => layout.layerOf.get(n) === adjacent).map(centreOf);
3689
+ const desired = centres.length > 0 ? median(centres) - heightOf.get(node) / 2 : topOf.get(node);
3690
+ offsets.push(offset);
3691
+ targets.push(desired - offset);
3692
+ if (i + 1 < layer.length) {
3693
+ offset += heightOf.get(node) + gapAfter(node, layer[i + 1]);
3694
+ }
3695
+ }
3696
+ const solved = isotonic(targets);
3697
+ for (let i = 0; i < layer.length; i++) {
3698
+ topOf.set(layer[i], solved[i] + offsets[i]);
3699
+ }
3210
3700
  };
3211
- const sweep = (neighbours, towards) => {
3212
- for (let i = 0; i < layout.layers.length; i++) {
3213
- const index = towards > 0 ? i : layout.layers.length - 1 - i;
3214
- const adjacent = index + towards;
3215
- if (adjacent < 0 || adjacent >= layout.layers.length) {
3216
- continue;
3701
+ for (let s = 0; s < placementSweeps; s++) {
3702
+ if (s % 2 === 0) {
3703
+ for (let i = 1; i < layout.layers.length; i++) {
3704
+ place(layout.layers[i], up, i - 1);
3217
3705
  }
3218
- const layer = layout.layers[index];
3219
- const keys = /* @__PURE__ */ new Map();
3220
- for (const node of layer) {
3221
- const mean = meanSlot(node, neighbours, adjacent);
3222
- keys.set(node, mean === -1 ? slotOf.get(node) : mean);
3223
- }
3224
- layer.sort((a, b) => keys.get(a) - keys.get(b));
3225
- for (let s = 0; s < layer.length; s++) {
3226
- slotOf.set(layer[s], s);
3706
+ } else {
3707
+ for (let i = layout.layers.length - 2; i >= 0; i--) {
3708
+ place(layout.layers[i], down, i + 1);
3227
3709
  }
3228
3710
  }
3229
- };
3230
- for (let i = 0; i < orderingSweeps; i++) {
3231
- sweep(upstream, -1);
3232
- sweep(downstream, 1);
3233
3711
  }
3712
+ const widths = layout.layers.map((layer) => layer.reduce((widest, node) => node < 0 ? widest : Math.max(widest, sizes.get(node).x), 0));
3713
+ const xs = columnXs(widths);
3714
+ const positions = /* @__PURE__ */ new Map();
3715
+ for (const node of piece) {
3716
+ const layer = layout.layerOf.get(node);
3717
+ positions.set(node, {
3718
+ x: xs[layer] + (widths[layer] - sizes.get(node).x) / 2,
3719
+ y: topOf.get(node)
3720
+ });
3721
+ }
3722
+ return positions;
3723
+ }
3724
+ function layoutLoose(loose, sizes) {
3725
+ const columns = Math.ceil(Math.sqrt(loose.length));
3726
+ const widths = new Array(columns).fill(0);
3727
+ const heights = new Array(Math.ceil(loose.length / columns)).fill(0);
3728
+ loose.forEach((node, i) => {
3729
+ const size = sizes.get(node);
3730
+ widths[i % columns] = Math.max(widths[i % columns], size.x);
3731
+ heights[Math.floor(i / columns)] = Math.max(heights[Math.floor(i / columns)], size.y);
3732
+ });
3733
+ const xs = new Array();
3734
+ let x = 0;
3735
+ for (const width2 of widths) {
3736
+ xs.push(x);
3737
+ x += width2 + heightSpacing;
3738
+ }
3739
+ const ys = new Array();
3740
+ let y = 0;
3741
+ for (const height2 of heights) {
3742
+ ys.push(y);
3743
+ y += height2 + heightSpacing;
3744
+ }
3745
+ const positions = /* @__PURE__ */ new Map();
3746
+ loose.forEach((node, i) => positions.set(node, { x: xs[i % columns], y: ys[Math.floor(i / columns)] }));
3747
+ return positions;
3748
+ }
3749
+ function toBlock(positions, sizes) {
3750
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
3751
+ for (const [node, position] of positions) {
3752
+ const size = sizes.get(node);
3753
+ minX = Math.min(minX, position.x);
3754
+ minY = Math.min(minY, position.y);
3755
+ maxX = Math.max(maxX, position.x + size.x);
3756
+ maxY = Math.max(maxY, position.y + size.y);
3757
+ }
3758
+ const shifted = /* @__PURE__ */ new Map();
3759
+ for (const [node, position] of positions) {
3760
+ shifted.set(node, { x: position.x - minX, y: position.y - minY });
3761
+ }
3762
+ return { positions: shifted, size: { x: maxX - minX, y: maxY - minY } };
3763
+ }
3764
+ function layoutGraph(sorting, upstream, downstream, sizes) {
3765
+ const blocks = new Array();
3766
+ const loose = new Array();
3767
+ for (const piece of findPieces(sorting, upstream, downstream)) {
3768
+ if (piece.length === 1) {
3769
+ loose.push(piece[0]);
3770
+ continue;
3771
+ }
3772
+ const root = treeRoot(piece, downstream);
3773
+ const positions2 = root !== void 0 ? layoutTree(root, upstream, sizes) : layoutLayered(piece, upstream, downstream, sizes);
3774
+ blocks.push(toBlock(positions2, sizes));
3775
+ }
3776
+ blocks.sort((a, b) => b.size.y - a.size.y || b.size.x - a.size.x);
3777
+ if (loose.length > 0) {
3778
+ blocks.push(toBlock(layoutLoose(loose, sizes), sizes));
3779
+ }
3780
+ let area = 0;
3781
+ let widest = 0;
3782
+ for (const block of blocks) {
3783
+ area += (block.size.x + pieceSpacing) * (block.size.y + pieceSpacing);
3784
+ widest = Math.max(widest, block.size.x);
3785
+ }
3786
+ const rowWidth = Math.max(widest, Math.sqrt(area));
3787
+ const positions = /* @__PURE__ */ new Map();
3788
+ let x = 0;
3789
+ let y = 0;
3790
+ let rowHeight = 0;
3791
+ for (const block of blocks) {
3792
+ if (x > 0 && x + block.size.x > rowWidth) {
3793
+ x = 0;
3794
+ y += rowHeight + pieceSpacing;
3795
+ rowHeight = 0;
3796
+ }
3797
+ for (const [node, position] of block.positions) {
3798
+ positions.set(node, { x: position.x + x, y: position.y + y });
3799
+ }
3800
+ x += block.size.x + pieceSpacing;
3801
+ rowHeight = Math.max(rowHeight, block.size.y);
3802
+ }
3803
+ return positions;
3234
3804
  }
3235
3805
  function Organize(ctx, graph, nodesToSort) {
3236
3806
  const nodes = graph.getNodes();
@@ -3239,9 +3809,6 @@ function Organize(ctx, graph, nodesToSort) {
3239
3809
  }
3240
3810
  const sorting = new Array();
3241
3811
  if (nodesToSort) {
3242
- if (nodesToSort.length < 2) {
3243
- return;
3244
- }
3245
3812
  for (const index of nodesToSort) {
3246
3813
  if (index >= 0 && index < nodes.length) {
3247
3814
  sorting.push(index);
@@ -3261,33 +3828,28 @@ function Organize(ctx, graph, nodesToSort) {
3261
3828
  nodeLUT.set(nodes[i], i);
3262
3829
  }
3263
3830
  const camera = new Camera();
3264
- const bounds = /* @__PURE__ */ new Map();
3831
+ const sizes = /* @__PURE__ */ new Map();
3265
3832
  for (const index of sorting) {
3266
- bounds.set(index, nodes[index].calculateBounds(ctx, camera));
3833
+ sizes.set(index, nodes[index].calculateBounds(ctx, camera).Size);
3267
3834
  }
3268
3835
  const upstream = /* @__PURE__ */ new Map();
3269
- const downstream = /* @__PURE__ */ new Map();
3270
3836
  for (const index of sorting) {
3837
+ const node = nodes[index];
3271
3838
  const feeders = new Array();
3272
- for (const feeder of graph.connectedInputsNodeReferencesByIndex(index)) {
3273
- const feederIndex = nodeLUT.get(feeder);
3274
- if (feederIndex !== void 0 && inLayout.has(feederIndex) && feederIndex !== index) {
3275
- feeders.push(feederIndex);
3839
+ for (let i = 0; i < node.inputs(); i++) {
3840
+ for (const connection of node.inputPort(i).connections()) {
3841
+ const feeder = connection.outNode();
3842
+ const feederIndex = feeder === null ? void 0 : nodeLUT.get(feeder);
3843
+ if (feederIndex !== void 0 && inLayout.has(feederIndex) && feederIndex !== index && !feeders.includes(feederIndex)) {
3844
+ feeders.push(feederIndex);
3845
+ }
3276
3846
  }
3277
3847
  }
3278
3848
  upstream.set(index, feeders);
3279
- const consumers = new Array();
3280
- for (const consumer of graph.connectedOutputsNodeReferences(index)) {
3281
- const consumerIndex = nodeLUT.get(consumer);
3282
- if (consumerIndex !== void 0 && inLayout.has(consumerIndex) && consumerIndex !== index) {
3283
- consumers.push(consumerIndex);
3284
- }
3285
- }
3286
- downstream.set(index, consumers);
3287
3849
  }
3288
- const layout = assignLayers(sorting, upstream);
3289
- orderWithinLayers(layout, upstream, downstream);
3290
- let anchor = { x: 0, y: 0 };
3850
+ const downstream = consumersOf(sorting, upstream);
3851
+ const positions = layoutGraph(sorting, upstream, downstream, sizes);
3852
+ const anchor = { x: 0, y: 0 };
3291
3853
  for (const index of sorting) {
3292
3854
  const position = nodes[index].getPosition();
3293
3855
  anchor.x += position.x;
@@ -3295,19 +3857,14 @@ function Organize(ctx, graph, nodesToSort) {
3295
3857
  }
3296
3858
  anchor.x /= sorting.length;
3297
3859
  anchor.y /= sorting.length;
3298
- const layerWidths = layout.layers.map((layer) => layer.reduce((widest, node) => Math.max(widest, bounds.get(node).Size.x), 0));
3299
- const layerHeights = layout.layers.map((layer) => layer.reduce((total, node) => total + bounds.get(node).Size.y + heightSpacing, -heightSpacing));
3300
- const totalWidth = layerWidths.reduce((total, width2) => total + width2 + widthSpacing, -widthSpacing);
3301
- const tallest = layerHeights.reduce((tallest2, height2) => Math.max(tallest2, height2), 0);
3302
- let x = anchor.x - totalWidth / 2;
3303
- for (let i = 0; i < layout.layers.length; i++) {
3304
- let y = anchor.y - tallest / 2 + (tallest - layerHeights[i]) / 2;
3305
- for (const node of layout.layers[i]) {
3306
- const size = bounds.get(node).Size;
3307
- nodes[node].setPosition({ x: x + (layerWidths[i] - size.x) / 2, y });
3308
- y += size.y + heightSpacing;
3309
- }
3310
- x += layerWidths[i] + widthSpacing;
3860
+ let right = 0;
3861
+ let bottom = 0;
3862
+ for (const [index, position] of positions) {
3863
+ right = Math.max(right, position.x + sizes.get(index).x);
3864
+ bottom = Math.max(bottom, position.y + sizes.get(index).y);
3865
+ }
3866
+ for (const [index, position] of positions) {
3867
+ nodes[index].setPosition({ x: anchor.x - right / 2 + position.x, y: anchor.y - bottom / 2 + position.y });
3311
3868
  }
3312
3869
  }
3313
3870
 
@@ -3499,14 +4056,17 @@ var NodeSubsystem2 = class {
3499
4056
  #portHovering;
3500
4057
  #widgetHovering;
3501
4058
  #widgetCurrentlyClicking;
4059
+ #widgetHoveringBox = null;
4060
+ #capturedWidgetBox = null;
4061
+ #capturedPointer = Zero();
3502
4062
  #cursor;
3503
4063
  #nodeFactory;
3504
4064
  #boxSelect;
3505
4065
  #boxSelectStart_graphSpace;
3506
4066
  #boxSelectEnd_graphSpace;
3507
4067
  #boxSelectionNodes;
3508
- #boxSelectStyle;
3509
4068
  #lastCtx;
4069
+ #lastMousePosition;
3510
4070
  // LISTENERS ==============================================================
3511
4071
  #nodeRemovedCallbacks;
3512
4072
  #registeredNodeAddedCallbacks;
@@ -3531,14 +4091,6 @@ var NodeSubsystem2 = class {
3531
4091
  this.#boxSelectEnd_graphSpace = Zero();
3532
4092
  this.#boxSelectionNodes = new List();
3533
4093
  this.#lastCtx = null;
3534
- this.#boxSelectStyle = new BoxStyle({
3535
- border: {
3536
- color: Theme.BoxSelect.Color,
3537
- size: Theme.BoxSelect.Size
3538
- },
3539
- color: "rgba(0,0,0,0)",
3540
- radius: Theme.BoxSelect.Radius
3541
- });
3542
4094
  }
3543
4095
  addPublisher(identifier, publisher) {
3544
4096
  this.#nodeFactory.addPublisher(identifier, publisher);
@@ -3575,6 +4127,7 @@ var NodeSubsystem2 = class {
3575
4127
  }
3576
4128
  if (intersection.Widget !== void 0) {
3577
4129
  this.#widgetHovering = intersection.Widget;
4130
+ this.#widgetHoveringBox = intersection.WidgetBox ?? null;
3578
4131
  }
3579
4132
  if (intersection.Port !== void 0 && intersection.Node !== void 0 && intersection.PortIndex !== void 0 && intersection.PortIsInput !== void 0) {
3580
4133
  this.#portHovering = {
@@ -3597,8 +4150,15 @@ var NodeSubsystem2 = class {
3597
4150
  hoveringSomething = true;
3598
4151
  }
3599
4152
  if (this.#widgetHovering !== null) {
3600
- this.#widgetHovering.ClickStart();
3601
- this.#widgetCurrentlyClicking = this.#widgetHovering;
4153
+ if (!this.#widgetHovering.Disabled?.()) {
4154
+ this.#widgetCurrentlyClicking = this.#widgetHovering;
4155
+ this.#capturedWidgetBox = this.#widgetHoveringBox === null ? null : {
4156
+ Position: { x: this.#widgetHoveringBox.Position.x, y: this.#widgetHoveringBox.Position.y },
4157
+ Size: { x: this.#widgetHoveringBox.Size.x, y: this.#widgetHoveringBox.Size.y }
4158
+ };
4159
+ CopyVector2(this.#capturedPointer, mousePosition);
4160
+ this.#widgetCurrentlyClicking.PointerDown?.(this.#widgetPointer());
4161
+ }
3602
4162
  hoveringSomething = true;
3603
4163
  }
3604
4164
  if (this.#portHovering === null) {
@@ -3613,12 +4173,12 @@ var NodeSubsystem2 = class {
3613
4173
  return hoveringSomething || ctrlKey;
3614
4174
  }
3615
4175
  if (this.#portHovering.InputPort) {
3616
- for (let i = 0; i < this.#connections.length; i++) {
3617
- if (this.#connections[i].inPort() === this.#portHovering.Port) {
3618
- this.#connections[i].clearInput();
3619
- this.#connectionSelected = this.#connections[i];
3620
- break;
3621
- }
4176
+ const port = this.#portHovering.Port;
4177
+ const wires = port.connections();
4178
+ const picked = wires.find((wire) => wire.mouseOverPort(mousePosition) === port) ?? wires[0];
4179
+ if (picked !== void 0) {
4180
+ picked.clearInput();
4181
+ this.#connectionSelected = picked;
3622
4182
  }
3623
4183
  } else {
3624
4184
  let inNode = this.#portHovering.Node;
@@ -3638,7 +4198,19 @@ var NodeSubsystem2 = class {
3638
4198
  }
3639
4199
  return true;
3640
4200
  }
4201
+ #widgetPointer() {
4202
+ return {
4203
+ position: { x: this.#capturedPointer.x, y: this.#capturedPointer.y },
4204
+ inside: this.#capturedWidgetBox !== null && InBox(this.#capturedWidgetBox, this.#capturedPointer)
4205
+ };
4206
+ }
3641
4207
  mouseDragEvent(delta, scale) {
4208
+ if (this.#widgetCurrentlyClicking !== null) {
4209
+ this.#capturedPointer.x += delta.x;
4210
+ this.#capturedPointer.y += delta.y;
4211
+ this.#widgetCurrentlyClicking.PointerMove?.(this.#widgetPointer());
4212
+ return true;
4213
+ }
3642
4214
  let nodeMoved = false;
3643
4215
  VectorPool.run(() => {
3644
4216
  const scaledDelta = VectorPool.get();
@@ -3757,13 +4329,20 @@ var NodeSubsystem2 = class {
3757
4329
  }
3758
4330
  #firstCompatiblePort(node, side, against) {
3759
4331
  const count = side === "INPUT" /* Input */ ? node.inputs() : node.outputs();
4332
+ let firstHidden = -1;
3760
4333
  for (let i = 0; i < count; i++) {
3761
4334
  const port = side === "INPUT" /* Input */ ? node.inputPort(i) : node.outputPort(i);
3762
- if (portsCompatible(port, against)) {
4335
+ if (!portsCompatible(port, against)) {
4336
+ continue;
4337
+ }
4338
+ if (!port.hidden()) {
3763
4339
  return i;
3764
4340
  }
4341
+ if (firstHidden === -1) {
4342
+ firstHidden = i;
4343
+ }
3765
4344
  }
3766
- return -1;
4345
+ return firstHidden;
3767
4346
  }
3768
4347
  clickEnd() {
3769
4348
  for (let i = 0; i < this.#nodesGrabbed.Count(); i++) {
@@ -3779,10 +4358,11 @@ var NodeSubsystem2 = class {
3779
4358
  this.#boxSelect = false;
3780
4359
  }
3781
4360
  if (this.#widgetCurrentlyClicking !== null) {
3782
- if (this.#widgetHovering === this.#widgetCurrentlyClicking) {
3783
- this.#widgetCurrentlyClicking.ClickEnd();
3784
- }
4361
+ const widget = this.#widgetCurrentlyClicking;
4362
+ const pointer = this.#widgetPointer();
3785
4363
  this.#widgetCurrentlyClicking = null;
4364
+ this.#capturedWidgetBox = null;
4365
+ widget.PointerUp?.(pointer);
3786
4366
  }
3787
4367
  if (this.#connectionSelected === null) {
3788
4368
  return;
@@ -3827,7 +4407,7 @@ var NodeSubsystem2 = class {
3827
4407
  inputPort.connections()[0].clearOutput();
3828
4408
  this.#removeConnection(inputPort.connections()[0], false);
3829
4409
  }
3830
- conn.setInput(this.#portHovering.Node, this.#portHovering.Index, replace);
4410
+ conn.setInput(this.#portHovering.Node, this.#portHovering.Index, replace, this.#insertIndex());
3831
4411
  } else {
3832
4412
  conn.setOutput(this.#portHovering.Node, this.#portHovering.Index);
3833
4413
  }
@@ -3967,6 +4547,14 @@ var NodeSubsystem2 = class {
3967
4547
  this.#removeNodeConnections(nodeIndex);
3968
4548
  const node = this.#nodes[nodeIndex];
3969
4549
  this.#nodes.splice(nodeIndex, 1);
4550
+ if (this.#nodeHovering === nodeIndex) {
4551
+ this.#nodeHovering = -1;
4552
+ } else if (this.#nodeHovering > nodeIndex) {
4553
+ this.#nodeHovering--;
4554
+ }
4555
+ if (this.#portHovering?.Node === node) {
4556
+ this.#portHovering = null;
4557
+ }
3970
4558
  requestRender();
3971
4559
  for (let i = 0; i < this.#nodeRemovedCallbacks.length; i++) {
3972
4560
  this.#nodeRemovedCallbacks[i](node);
@@ -4191,7 +4779,8 @@ var NodeSubsystem2 = class {
4191
4779
  }
4192
4780
  if (intersection.Widget !== void 0) {
4193
4781
  this.#widgetHovering = intersection.Widget;
4194
- this.#cursor = "pointer" /* Pointer */;
4782
+ this.#widgetHoveringBox = intersection.WidgetBox ?? null;
4783
+ this.#cursor = intersection.Widget.Disabled?.() ? "default" /* Default */ : intersection.Widget.Cursor?.() ?? "pointer" /* Pointer */;
4195
4784
  }
4196
4785
  if (intersection.Port !== void 0 && intersection.Node !== void 0 && intersection.PortIndex !== void 0 && intersection.PortIsInput !== void 0) {
4197
4786
  this.#portHovering = {
@@ -4214,6 +4803,9 @@ var NodeSubsystem2 = class {
4214
4803
  }
4215
4804
  this.#nodes[i].render(ctx, camera, state, mousePosition, this.#postProcessPass);
4216
4805
  }
4806
+ if (this.#widgetCurrentlyClicking !== null) {
4807
+ this.#cursor = this.#widgetCurrentlyClicking.Cursor?.() ?? "pointer" /* Pointer */;
4808
+ }
4217
4809
  }
4218
4810
  #boxSelectionScreenspaceBox(camera) {
4219
4811
  const box = { Position: Zero(), Size: Zero() };
@@ -4222,29 +4814,174 @@ var NodeSubsystem2 = class {
4222
4814
  SubVector2(box.Size, box.Size, box.Position);
4223
4815
  return box;
4224
4816
  }
4817
+ /**
4818
+ * Where a wire being dropped on the hovered array input would go, or
4819
+ * undefined when it is not over one.
4820
+ */
4821
+ #insertIndex() {
4822
+ const hovering = this.#portHovering;
4823
+ if (hovering === null || !hovering.InputPort || this.#lastMousePosition === void 0 || hovering.Port.getPortType() !== "INPUTARRAY" /* InputArray */) {
4824
+ return void 0;
4825
+ }
4826
+ const box = hovering.Node.inputPortPosition(hovering.Index);
4827
+ return arrayInsertIndex(box, hovering.Port.connections().length, this.#lastMousePosition.y);
4828
+ }
4829
+ #renderInsertMarker(ctx, camera) {
4830
+ const hovering = this.#portHovering;
4831
+ if (this.#connectionSelected === null || this.#connectionSelected.inPort() !== null || hovering === null) {
4832
+ return;
4833
+ }
4834
+ const index = this.#insertIndex();
4835
+ const count = hovering.Port.connections().length;
4836
+ if (index === void 0 || count === 0) {
4837
+ return;
4838
+ }
4839
+ const box = hovering.Node.inputPortPosition(hovering.Index);
4840
+ const y = box.Position.y + index * box.Size.y / count;
4841
+ const overhang = 6 * camera.zoom;
4842
+ ctx.save();
4843
+ ctx.strokeStyle = "#ffffff";
4844
+ ctx.lineWidth = Math.max(1, 2 * camera.zoom);
4845
+ ctx.beginPath();
4846
+ ctx.moveTo(box.Position.x - overhang, y);
4847
+ ctx.lineTo(box.Position.x + box.Size.x + overhang, y);
4848
+ ctx.stroke();
4849
+ ctx.restore();
4850
+ }
4225
4851
  render(ctx, camera, mousePosition) {
4226
4852
  this.#lastCtx = ctx;
4853
+ this.#lastMousePosition = mousePosition;
4227
4854
  this.#cursor = "default" /* Default */;
4228
4855
  const awaitingLayout = this.#nodes.some((node) => !node.hasBeenLaidOut());
4856
+ for (const node of this.#nodes) {
4857
+ if (node.hasBeenLaidOut()) {
4858
+ node.placePorts(node.calculateBounds(ctx, camera), camera.zoom);
4859
+ }
4860
+ }
4229
4861
  TimeExecution("Render_Connections", () => {
4230
4862
  this.#renderConnections(ctx, camera, mousePosition);
4231
4863
  });
4232
4864
  TimeExecution("Render_Nodes", () => {
4233
4865
  this.#renderNodes(ctx, camera, mousePosition);
4234
4866
  });
4867
+ this.#renderInsertMarker(ctx, camera);
4235
4868
  if (awaitingLayout) {
4236
4869
  requestRender();
4237
4870
  }
4238
4871
  if (this.#boxSelect) {
4239
4872
  const box = this.#boxSelectionScreenspaceBox(camera);
4240
4873
  ctx.setLineDash([Theme.BoxSelect.LineDashLength]);
4241
- this.#boxSelectStyle.Draw(ctx, box, 1);
4874
+ new BoxStyle({
4875
+ border: { color: Theme.BoxSelect.Color, size: Theme.BoxSelect.Size },
4876
+ color: "rgba(0,0,0,0)",
4877
+ radius: Theme.BoxSelect.Radius
4878
+ }).Draw(ctx, box, 1);
4242
4879
  ctx.setLineDash([]);
4243
4880
  }
4244
4881
  return { cursorStyle: this.#cursor };
4245
4882
  }
4246
4883
  };
4247
4884
 
4885
+ // src/popup.ts
4886
+ var Popup = class {
4887
+ #title;
4888
+ #options;
4889
+ #content;
4890
+ #onClose;
4891
+ // Styling ================================================================
4892
+ #buttonCSS;
4893
+ // Runtime ================================================================
4894
+ #popup;
4895
+ #keyHandler;
4896
+ constructor(config) {
4897
+ this.#title = config.title;
4898
+ this.#options = config.options;
4899
+ this.#content = config.content;
4900
+ this.#onClose = config.onClose;
4901
+ this.#buttonCSS = config.buttonCSS === void 0 ? "flex-grow: 1; margin-right: 8px;" : config.buttonCSS;
4902
+ this.#popup = null;
4903
+ this.#keyHandler = null;
4904
+ }
4905
+ Hide() {
4906
+ if (this.#popup === null) {
4907
+ return;
4908
+ }
4909
+ if (this.#keyHandler !== null) {
4910
+ document.removeEventListener("keydown", this.#keyHandler, true);
4911
+ this.#keyHandler = null;
4912
+ }
4913
+ document.body.removeChild(this.#popup);
4914
+ this.#popup = null;
4915
+ }
4916
+ #close(option) {
4917
+ this.Hide();
4918
+ if (this.#onClose !== void 0) {
4919
+ this.#onClose(option);
4920
+ }
4921
+ }
4922
+ Show() {
4923
+ this.#popup = document.createElement("div");
4924
+ 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};`;
4925
+ document.body.appendChild(this.#popup);
4926
+ const container = document.createElement("div");
4927
+ 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;`;
4928
+ this.#popup.appendChild(container);
4929
+ const title = document.createElement("h2");
4930
+ title.textContent = this.#title;
4931
+ title.style.cssText = "margin-top:0;margin-bottom:16px;";
4932
+ container.appendChild(title);
4933
+ if (this.#content !== void 0) {
4934
+ container.appendChild(this.#content());
4935
+ }
4936
+ const buttonContainer = document.createElement("div");
4937
+ buttonContainer.style.cssText = "margin-top:16px;justify-content:space-around;align-items:center;flex-direction:row;display:flex;";
4938
+ container.appendChild(buttonContainer);
4939
+ for (let i = 0; i < this.#options.length; i++) {
4940
+ const button = document.createElement("button");
4941
+ button.style.cssText = this.#buttonCSS;
4942
+ button.textContent = this.#options[i];
4943
+ button.onclick = () => this.#close(this.#options[i]);
4944
+ buttonContainer.appendChild(button);
4945
+ }
4946
+ this.#keyHandler = (e) => {
4947
+ if (e.key === "Enter") {
4948
+ e.preventDefault();
4949
+ this.#close(this.#options.length > 0 ? this.#options[0] : null);
4950
+ } else if (e.key === "Escape") {
4951
+ e.preventDefault();
4952
+ this.#close(null);
4953
+ }
4954
+ e.stopPropagation();
4955
+ };
4956
+ document.addEventListener("keydown", this.#keyHandler, true);
4957
+ }
4958
+ };
4959
+
4960
+ // src/popups/string.ts
4961
+ function SetStringPopup(config) {
4962
+ let input = null;
4963
+ return new Popup({
4964
+ title: config.title,
4965
+ options: ["Set", "Cancel"],
4966
+ content: () => {
4967
+ const container = document.createElement("div");
4968
+ input = document.createElement("input");
4969
+ input.value = config.startingValue;
4970
+ container.append(input);
4971
+ return container;
4972
+ },
4973
+ onClose: (button) => {
4974
+ if (button !== "Set" || input === null) {
4975
+ if (config.onCancel) {
4976
+ config.onCancel();
4977
+ }
4978
+ return;
4979
+ }
4980
+ config.onUpdate(input.value);
4981
+ }
4982
+ });
4983
+ }
4984
+
4248
4985
  // src/popups/form.ts
4249
4986
  function FormPopup(config) {
4250
4987
  let input = new Array();
@@ -4361,9 +5098,11 @@ var FlowNode = class {
4361
5098
  #titleChangeCallback;
4362
5099
  #infoChangeCallback;
4363
5100
  // Styling ================================================================
5101
+ #styleConfig;
5102
+ #styledForTheme = -1;
4364
5103
  #titleColor;
4365
5104
  #selectedStyle;
4366
- #stateStyles;
5105
+ #stateStyles = /* @__PURE__ */ new Map();
4367
5106
  #padding;
4368
5107
  #portTextStyle;
4369
5108
  #elementSpacing;
@@ -4375,6 +5114,7 @@ var FlowNode = class {
4375
5114
  #data;
4376
5115
  #registeredAnyPropertyChangeCallbacks;
4377
5116
  #registeredPropertyChangeCallbacks;
5117
+ #registeredPropertyEditCallbacks = /* @__PURE__ */ new Map();
4378
5118
  constructor(config) {
4379
5119
  this.#input = new Array();
4380
5120
  this.#output = new Array();
@@ -4427,58 +5167,17 @@ var FlowNode = class {
4427
5167
  this.#position = config?.position === void 0 ? { x: 0, y: 0 } : config.position;
4428
5168
  this.#title = new Text(
4429
5169
  config?.title === void 0 ? "" : config.title,
4430
- TextStyleFallback(config?.style?.title?.textStyle, {
4431
- size: 16,
4432
- weight: "bold" /* Bold */,
4433
- color: Theme.Node.FontColor
4434
- })
5170
+ TextStyleFallback(config?.style?.title?.textStyle, { size: 16, weight: "bold" /* Bold */ })
4435
5171
  );
4436
5172
  this.#subTitle = new Text(
4437
5173
  config?.subTitle === void 0 ? "" : config.subTitle,
4438
- TextStyleFallback(config?.style?.title?.textStyle, {
4439
- size: 10,
4440
- weight: "bold" /* Bold */,
4441
- color: Theme.Node.FontColor
4442
- })
4443
- );
4444
- this.#infoSymbol = new Text(
4445
- "i",
4446
- {
4447
- size: 13,
4448
- weight: "bold" /* Bold */,
4449
- color: Theme.Node.FontColor
4450
- }
5174
+ TextStyleFallback(config?.style?.title?.textStyle, { size: 10, weight: "bold" /* Bold */ })
4451
5175
  );
5176
+ this.#infoSymbol = new Text("i", { size: 13, weight: "bold" /* Bold */ });
4452
5177
  this.#infoText = config?.info ? config?.info : "";
4453
- this.#titleColor = config?.style?.title?.color === void 0 ? "#154050" : config?.style?.title?.color;
4454
5178
  this.#padding = config?.style?.title?.padding === void 0 ? 15 : config?.style?.title?.padding;
4455
- this.#stateStyles = /* @__PURE__ */ new Map();
4456
- this.#stateStyles.set(0 /* Idle */, new BoxStyle(BoxStyleWithFallback(config?.style?.idle, {
4457
- border: { color: Theme.Node.Border.Idle, size: 1 },
4458
- radius: Theme.Node.BorderRadius,
4459
- color: Theme.Node.BackgroundColor
4460
- })));
4461
- this.#stateStyles.set(1 /* MouseOver */, new BoxStyle(BoxStyleWithFallback(config?.style?.mouseOver, {
4462
- border: { color: Theme.Node.Border.MouseOver, size: 1.1 },
4463
- radius: Theme.Node.BorderRadius,
4464
- color: Theme.Node.BackgroundColor
4465
- })));
4466
- this.#stateStyles.set(2 /* Grabbed */, new BoxStyle(BoxStyleWithFallback(config?.style?.grabbed, {
4467
- border: { color: Theme.Node.Border.Grabbed, size: 2 },
4468
- radius: Theme.Node.BorderRadius,
4469
- color: Theme.Node.BackgroundColor
4470
- })));
4471
- this.#selectedStyle = new BoxStyle(BoxStyleWithFallback(config?.style?.selected, {
4472
- border: { color: Theme.Node.Border.Selected, size: 1 },
4473
- radius: Theme.Node.BorderRadius,
4474
- color: Theme.Node.BackgroundColor
4475
- }));
4476
- this.#portTextStyle = new TextStyle({
4477
- size: config?.style?.portText?.size === void 0 ? 14 : config?.style?.portText?.size,
4478
- color: config?.style?.portText?.color === void 0 ? Theme.Node.Port.FontColor : config?.style?.portText?.color,
4479
- font: config?.style?.portText?.font,
4480
- weight: config?.style?.portText?.weight
4481
- });
5179
+ this.#styleConfig = config?.style;
5180
+ this.#applyStyle();
4482
5181
  if (config?.inputs !== void 0) {
4483
5182
  for (let i = 0; i < config.inputs.length; i++) {
4484
5183
  this.addInput(config.inputs[i]);
@@ -4490,12 +5189,8 @@ var FlowNode = class {
4490
5189
  }
4491
5190
  }
4492
5191
  if (config?.widgets !== void 0) {
4493
- for (let i = 0; i < config.widgets.length; i++) {
4494
- const widget = config.widgets[i];
4495
- if (widget.type === void 0) {
4496
- continue;
4497
- }
4498
- this.addWidget(GlobalWidgetFactory.create(this, widget.type, widget.config));
5192
+ for (const widget of config.widgets) {
5193
+ this.addWidget(GlobalWidgetFactory.createFromConfig(this, widget));
4499
5194
  }
4500
5195
  }
4501
5196
  }
@@ -4552,7 +5247,7 @@ var FlowNode = class {
4552
5247
  }
4553
5248
  setProperty(name, value) {
4554
5249
  const oldValue = this.#data[name];
4555
- if (oldValue === value) {
5250
+ if (Object.is(oldValue, value)) {
4556
5251
  return;
4557
5252
  }
4558
5253
  this.#data[name] = value;
@@ -4568,6 +5263,32 @@ var FlowNode = class {
4568
5263
  callbacks[i](oldValue, value);
4569
5264
  }
4570
5265
  }
5266
+ /**
5267
+ * Listens for a person changing the property through a widget, but not
5268
+ * for the property being set from code.
5269
+ */
5270
+ addPropertyEditListener(name, callback) {
5271
+ let callbacks = this.#registeredPropertyEditCallbacks.get(name);
5272
+ if (callbacks === void 0) {
5273
+ callbacks = [];
5274
+ this.#registeredPropertyEditCallbacks.set(name, callbacks);
5275
+ }
5276
+ callbacks.push(callback);
5277
+ }
5278
+ /**
5279
+ * Sets the property as a person's finished edit, so edit listeners hear
5280
+ * it even when the value was already showing.
5281
+ */
5282
+ editProperty(name, value) {
5283
+ this.setProperty(name, value);
5284
+ const callbacks = this.#registeredPropertyEditCallbacks.get(name);
5285
+ if (callbacks === void 0) {
5286
+ return;
5287
+ }
5288
+ for (const callback of [...callbacks]) {
5289
+ callback(value);
5290
+ }
5291
+ }
4571
5292
  getProperty(name) {
4572
5293
  return this.#data[name];
4573
5294
  }
@@ -4856,6 +5577,7 @@ var FlowNode = class {
4856
5577
  // return this.#titleTextStyle.measure(ctx, scale, this.#title);
4857
5578
  // }
4858
5579
  calculateBounds(ctx, camera) {
5580
+ this.#followTheme();
4859
5581
  const tempMeasurement = Zero();
4860
5582
  const doublePadding = this.#padding * 2;
4861
5583
  const screenSpacePosition = Zero();
@@ -4869,25 +5591,21 @@ var FlowNode = class {
4869
5591
  size.x += size.y * 4;
4870
5592
  }
4871
5593
  size.x += doublePadding;
4872
- size.y += doublePadding + this.#elementSpacing * this.#input.length;
4873
- for (let i = 0; i < this.#input.length; i++) {
4874
- const port = this.#input[i];
4875
- this.#portTextStyle.measure(ctx, 1, port.getDisplayName(), tempMeasurement);
4876
- size.y += tempMeasurement.y;
4877
- size.x = Math.max(size.x, tempMeasurement.x + doublePadding);
4878
- }
4879
- size.y += this.#elementSpacing * this.#output.length;
4880
- for (let i = 0; i < this.#output.length; i++) {
4881
- const port = this.#output[i];
5594
+ size.y += doublePadding;
5595
+ for (const port of [...this.#input, ...this.#output]) {
5596
+ if (port.hidden()) {
5597
+ continue;
5598
+ }
4882
5599
  this.#portTextStyle.measure(ctx, 1, port.getDisplayName(), tempMeasurement);
4883
- size.y += tempMeasurement.y;
5600
+ size.y += this.#portRowHeight(port, tempMeasurement.y) + this.#elementSpacing;
4884
5601
  size.x = Math.max(size.x, tempMeasurement.x + doublePadding);
4885
5602
  }
4886
- size.y += this.#elementSpacing * this.#widgets.length;
4887
- for (let i = 0; i < this.#widgets.length; i++) {
4888
- const element = this.#widgets[i];
4889
- const eleSize = element.Size();
4890
- size.y += eleSize.y;
5603
+ for (const widget of this.#widgets) {
5604
+ if (widget.Hidden?.()) {
5605
+ continue;
5606
+ }
5607
+ const eleSize = widget.Size();
5608
+ size.y += eleSize.y + this.#elementSpacing;
4891
5609
  size.x = Math.max(size.x, eleSize.x + doublePadding);
4892
5610
  }
4893
5611
  size.y += this.#elementSpacing;
@@ -4971,7 +5689,7 @@ var FlowNode = class {
4971
5689
  intersection.Node = this;
4972
5690
  }
4973
5691
  for (let i = 0; i < this.#inputPortPositions.Count(); i++) {
4974
- if (InBox(this.#inputPortPositions.At(i), position)) {
5692
+ if (!this.#input[i].hidden() && InBox(this.#inputPortPositions.At(i), position)) {
4975
5693
  intersection.Node = this;
4976
5694
  intersection.PortIndex = i;
4977
5695
  intersection.PortIsInput = true;
@@ -4979,7 +5697,7 @@ var FlowNode = class {
4979
5697
  }
4980
5698
  }
4981
5699
  for (let i = 0; i < this.#outputPortPositions.Count(); i++) {
4982
- if (InBox(this.#outputPortPositions.At(i), position)) {
5700
+ if (!this.#output[i].hidden() && InBox(this.#outputPortPositions.At(i), position)) {
4983
5701
  intersection.Node = this;
4984
5702
  intersection.PortIndex = i;
4985
5703
  intersection.PortIsInput = false;
@@ -4987,10 +5705,12 @@ var FlowNode = class {
4987
5705
  }
4988
5706
  }
4989
5707
  for (let i = 0; i < this.#widgetPositions.Count(); i++) {
4990
- if (InBox(this.#widgetPositions.At(i), position)) {
5708
+ const widget = this.#widgets[i];
5709
+ if (widget !== void 0 && !widget.Hidden?.() && InBox(this.#widgetPositions.At(i), position)) {
4991
5710
  intersection.Node = this;
4992
5711
  intersection.WidgetIndex = i;
4993
- intersection.Widget = this.#widgets[i];
5712
+ intersection.Widget = widget;
5713
+ intersection.WidgetBox = this.#widgetPositions.At(i);
4994
5714
  }
4995
5715
  }
4996
5716
  return intersection;
@@ -5005,10 +5725,6 @@ var FlowNode = class {
5005
5725
  return this.#title.get();
5006
5726
  }
5007
5727
  setTitle(newTitle) {
5008
- if (!this.#canEditTitle) {
5009
- console.warn("setTitle instruction ignored, as node has been marked un-editable");
5010
- return;
5011
- }
5012
5728
  let cleaned = newTitle;
5013
5729
  if (cleaned === null || cleaned === void 0) {
5014
5730
  cleaned = "";
@@ -5025,9 +5741,6 @@ var FlowNode = class {
5025
5741
  }
5026
5742
  }
5027
5743
  setInfo(newInfo) {
5028
- if (!this.#canEditInfo) {
5029
- console.warn("setInfo instruction ignored, as node has been marked un-editable");
5030
- }
5031
5744
  let cleaned = newInfo;
5032
5745
  if (cleaned === null || cleaned === void 0) {
5033
5746
  cleaned = "";
@@ -5046,6 +5759,12 @@ var FlowNode = class {
5046
5759
  inputPortPosition(index) {
5047
5760
  return this.#inputPortPositions.At(index);
5048
5761
  }
5762
+ widgetPosition(index) {
5763
+ if (index < 0 || index >= this.#widgetPositions.Count()) {
5764
+ return void 0;
5765
+ }
5766
+ return this.#widgetPositions.At(index);
5767
+ }
5049
5768
  inputPort(index) {
5050
5769
  return this.#input[index];
5051
5770
  }
@@ -5061,6 +5780,35 @@ var FlowNode = class {
5061
5780
  outputs() {
5062
5781
  return this.#output.length;
5063
5782
  }
5783
+ #applyStyle() {
5784
+ const style = this.#styleConfig;
5785
+ const titleText = style?.title?.textStyle?.color ?? Theme.Node.Title.FontColor;
5786
+ this.#title.style().setColor(titleText);
5787
+ this.#subTitle.style().setColor(titleText);
5788
+ this.#infoSymbol.style().setColor(titleText);
5789
+ this.#titleColor = style?.title?.color ?? Theme.Node.Title.Color;
5790
+ const box = (config, border, size) => new BoxStyle(BoxStyleWithFallback(config, {
5791
+ border: { color: border, size },
5792
+ radius: Theme.Node.BorderRadius,
5793
+ color: Theme.Node.BackgroundColor
5794
+ }));
5795
+ this.#stateStyles.set(0 /* Idle */, box(style?.idle, Theme.Node.Border.Idle, 1));
5796
+ this.#stateStyles.set(1 /* MouseOver */, box(style?.mouseOver, Theme.Node.Border.MouseOver, 1.1));
5797
+ this.#stateStyles.set(2 /* Grabbed */, box(style?.grabbed, Theme.Node.Border.Grabbed, 2));
5798
+ this.#selectedStyle = box(style?.selected, Theme.Node.Border.Selected, 1);
5799
+ this.#portTextStyle = new TextStyle({
5800
+ size: style?.portText?.size ?? 14,
5801
+ color: style?.portText?.color ?? Theme.Node.Port.FontColor,
5802
+ font: style?.portText?.font,
5803
+ weight: style?.portText?.weight
5804
+ });
5805
+ this.#styledForTheme = themeVersion();
5806
+ }
5807
+ #followTheme() {
5808
+ if (this.#styledForTheme !== themeVersion()) {
5809
+ this.#applyStyle();
5810
+ }
5811
+ }
5064
5812
  #calculateStyle(state) {
5065
5813
  if (this.#selected && state === 0 /* Idle */) {
5066
5814
  return this.#selectedStyle;
@@ -5077,9 +5825,22 @@ var FlowNode = class {
5077
5825
  clearMessages() {
5078
5826
  this.#messages = [];
5079
5827
  }
5828
+ #portRowHeight(port, textHeight) {
5829
+ const slots = port.slots();
5830
+ return slots > 1 ? Math.max(textHeight, slots * ARRAY_SLOT_PITCH) : textHeight;
5831
+ }
5080
5832
  hasBeenLaidOut() {
5081
5833
  return this.#laidOut;
5082
5834
  }
5835
+ /**
5836
+ * Throws away where the ports were last placed, so wires wait for the
5837
+ * next draw instead of pointing at the old spots.
5838
+ */
5839
+ invalidateLayout() {
5840
+ this.#laidOut = false;
5841
+ this.#portOffsets = { input: [], output: [] };
5842
+ requestRender();
5843
+ }
5083
5844
  #laidOut = false;
5084
5845
  render(ctx, camera, state, mousePosition, postProcess) {
5085
5846
  this.#laidOut = true;
@@ -5117,6 +5878,9 @@ var FlowNode = class {
5117
5878
  [nodeStyle.radius() * camera.zoom, nodeStyle.radius() * camera.zoom, 0, 0]
5118
5879
  );
5119
5880
  ctx.fill();
5881
+ if (Theme.Node.Bevel !== null) {
5882
+ drawBevel(ctx, nodeBounds, camera.zoom, Theme.Node.Bevel, false);
5883
+ }
5120
5884
  const titlePosition = VectorPool.get();
5121
5885
  titlePosition.x = nodeBounds.Position.x + nodeBounds.Size.x / 2;
5122
5886
  titlePosition.y = nodeBounds.Position.y + scaledPadding + titleSize.y / 2;
@@ -5177,8 +5941,12 @@ var FlowNode = class {
5177
5941
  for (let i = 0; i < this.#input.length; i++) {
5178
5942
  ctx.textAlign = "left" /* Left */;
5179
5943
  const port = this.#input[i];
5944
+ if (port.hidden()) {
5945
+ this.#inputPortPositions.Push({ Position: { x: nodeBounds.Position.x, y: startY }, Size: { x: 0, y: 0 } });
5946
+ continue;
5947
+ }
5180
5948
  this.#portTextStyle.measure(ctx, 1, port.getDisplayName(), tempMeasurement);
5181
- const portHeight = tempMeasurement.y * camera.zoom;
5949
+ const portHeight = this.#portRowHeight(port, tempMeasurement.y) * camera.zoom;
5182
5950
  const position = VectorPool.get();
5183
5951
  position.x = nodeBounds.Position.x;
5184
5952
  position.y = startY + portHeight / 2;
@@ -5191,6 +5959,10 @@ var FlowNode = class {
5191
5959
  ctx.textAlign = "right" /* Right */;
5192
5960
  for (let i = 0; i < this.#output.length; i++) {
5193
5961
  const port = this.#output[i];
5962
+ if (port.hidden()) {
5963
+ this.#outputPortPositions.Push({ Position: { x: rightSide, y: startY }, Size: { x: 0, y: 0 } });
5964
+ continue;
5965
+ }
5194
5966
  this.#portTextStyle.measure(ctx, 1, port.getDisplayName(), tempMeasurement);
5195
5967
  const portHeight = tempMeasurement.y * camera.zoom;
5196
5968
  const position = VectorPool.get();
@@ -5203,13 +5975,18 @@ var FlowNode = class {
5203
5975
  }
5204
5976
  for (let i = 0; i < this.#widgets.length; i++) {
5205
5977
  const widget = this.#widgets[i];
5978
+ if (widget.Hidden?.()) {
5979
+ this.#widgetPositions.Push({ Position: { x: nodeBounds.Position.x, y: startY }, Size: { x: 0, y: 0 } });
5980
+ continue;
5981
+ }
5206
5982
  const widgetSize = widget.Size();
5207
- const scaledWidgetWidth = widgetSize.x * camera.zoom;
5208
- const position = VectorPool.get();
5209
- position.x = nodeBounds.Position.x + (nodeBounds.Size.x - scaledWidgetWidth) / 2;
5210
- position.y = startY;
5211
- this.#widgetPositions.Push(widget.Draw(ctx, position, camera.zoom, mousePosition));
5212
- startY += widgetSize.y * camera.zoom + scaledElementSpacing;
5983
+ const box = {
5984
+ Position: { x: nodeBounds.Position.x + scaledPadding, y: startY },
5985
+ Size: { x: nodeBounds.Size.x - scaledPadding * 2, y: widgetSize.y * camera.zoom }
5986
+ };
5987
+ widget.Draw(ctx, box, camera.zoom, mousePosition);
5988
+ this.#widgetPositions.Push(box);
5989
+ startY += box.Size.y + scaledElementSpacing;
5213
5990
  }
5214
5991
  const messageStart = VectorPool.get();
5215
5992
  messageStart.x = nodeBounds.Position.x + nodeBounds.Size.x / 2;
@@ -5246,6 +6023,13 @@ var FlowNode = class {
5246
6023
  record(this.#outputPortPositions, this.#portOffsets.output);
5247
6024
  }
5248
6025
  layoutPortsWithoutDrawing(bounds, zoom) {
6026
+ if (!this.placePorts(bounds, zoom)) {
6027
+ return false;
6028
+ }
6029
+ this.#widgetPositions.Clear();
6030
+ return true;
6031
+ }
6032
+ placePorts(bounds, zoom) {
5249
6033
  if (this.#portOffsets.input.length === 0 && this.#portOffsets.output.length === 0) {
5250
6034
  return false;
5251
6035
  }
@@ -5267,7 +6051,6 @@ var FlowNode = class {
5267
6051
  };
5268
6052
  project(this.#portOffsets.input, this.#inputPortPositions);
5269
6053
  project(this.#portOffsets.output, this.#outputPortPositions);
5270
- this.#widgetPositions.Clear();
5271
6054
  return true;
5272
6055
  }
5273
6056
  };
@@ -5459,33 +6242,33 @@ var CodeBlockEntry = class {
5459
6242
  }
5460
6243
  render(ctx, position, scale, maxWidth) {
5461
6244
  this.#calculateLayout(ctx, maxWidth);
5462
- let padding = Theme.Note.CodeBlock.Padding * scale;
5463
- let max = 0;
5464
- for (let i = 0; i < this.#calculatedEntries.Count(); i++) {
5465
- const pos = this.#calculatedPositions.At(i);
5466
- max = Math.max(max, pos.y * scale);
5467
- }
5468
- const y = position.y + max + scale * 5;
6245
+ const padding = Theme.Note.CodeBlock.Padding * scale;
6246
+ const lineCount = this.#calculatedEntries.Count();
6247
+ const lastTop = lineCount === 0 ? 0 : this.#calculatedPositions.At(lineCount - 1).y;
6248
+ const contentHeight = lineCount === 0 ? 0 : (lastTop + this.#text.style().getSize()) * scale;
6249
+ const height2 = contentHeight + padding * 2;
5469
6250
  ctx.fillStyle = Theme.Note.CodeBlock.BackgroundColor;
5470
6251
  ctx.beginPath();
5471
6252
  ctx.roundRect(
5472
6253
  position.x,
5473
6254
  position.y,
5474
6255
  maxWidth * scale,
5475
- max + padding * 2,
6256
+ height2,
5476
6257
  Theme.Note.CodeBlock.BorderRadius * scale
5477
6258
  );
5478
6259
  ctx.fill();
5479
- for (let i = 0; i < this.#calculatedEntries.Count(); i++) {
6260
+ const baseline = ctx.textBaseline;
6261
+ ctx.textBaseline = "top";
6262
+ for (let i = 0; i < lineCount; i++) {
5480
6263
  const entry = this.#calculatedEntries.At(i);
5481
6264
  const pos = this.#calculatedPositions.At(i);
5482
6265
  entry.render(ctx, scale, {
5483
6266
  x: pos.x * scale + position.x + padding,
5484
6267
  y: pos.y * scale + position.y + padding
5485
6268
  });
5486
- max = Math.max(max, pos.y * scale);
5487
6269
  }
5488
- return max + padding * 2;
6270
+ ctx.textBaseline = baseline;
6271
+ return height2;
5489
6272
  }
5490
6273
  };
5491
6274
  var UnorderedListMarkdownEntry = class {
@@ -5863,9 +6646,10 @@ var MarkdownSyntaxParser = class {
5863
6646
  this.#inc();
5864
6647
  this.#inc();
5865
6648
  this.#inc();
5866
- const codeBlockText = this.#originalText.substring(start, end);
6649
+ const codeBlockText = this.#originalText.substring(start, end).replace(/\r?\n$/, "");
5867
6650
  const entryText = new Text(codeBlockText);
5868
6651
  this.#assignStandardStyling([entryText]);
6652
+ entryText.setColor(Theme.Note.CodeBlock.FontColor);
5869
6653
  return new CodeBlockEntry(entryText);
5870
6654
  }
5871
6655
  parse() {
@@ -5920,6 +6704,7 @@ var BOX_SIZE = 10;
5920
6704
  var FlowNote = class {
5921
6705
  #originalText;
5922
6706
  #document;
6707
+ #parsedForTheme = -1;
5923
6708
  #width;
5924
6709
  #edittingStyle;
5925
6710
  // Runtime ================================================================
@@ -5962,6 +6747,7 @@ var FlowNote = class {
5962
6747
  setText(text) {
5963
6748
  this.#originalText = text;
5964
6749
  this.#document = BuildMarkdown(this.#originalText);
6750
+ this.#parsedForTheme = themeVersion();
5965
6751
  for (let i = 0; i < this.#contentChangeCallbacks.length; i++) {
5966
6752
  this.#contentChangeCallbacks[i](this, text);
5967
6753
  }
@@ -6049,6 +6835,10 @@ var FlowNote = class {
6049
6835
  }
6050
6836
  camera.graphSpaceToScreenSpace(this.#position, this.#tempPosition);
6051
6837
  CopyVector2(this.#lastRenderedBox.Position, this.#tempPosition);
6838
+ if (this.#parsedForTheme !== themeVersion()) {
6839
+ this.#document = BuildMarkdown(this.#originalText);
6840
+ this.#parsedForTheme = themeVersion();
6841
+ }
6052
6842
  const startY = this.#tempPosition.y;
6053
6843
  const lineSpacing = Theme.Note.EntrySpacing * camera.zoom;
6054
6844
  ctx.textAlign = "left" /* Left */;
@@ -6744,13 +7534,15 @@ var Minimap = class {
6744
7534
  // src/graph.ts
6745
7535
  function BuildBackgroundRenderer(backgroundColor) {
6746
7536
  return (canvas, context, position, scale) => {
6747
- context.fillStyle = backgroundColor;
7537
+ context.fillStyle = backgroundColor ?? Theme.Graph.BackgroundColor;
6748
7538
  context.fillRect(0, 0, canvas.width, canvas.height);
6749
- const alpha = Math.round(Clamp01(scale - 0.3) * 255);
7539
+ const alpha = Clamp01(scale - 0.3);
6750
7540
  if (alpha <= 0) {
6751
7541
  return;
6752
7542
  }
6753
- context.fillStyle = `rgba(41, 54, 57, ${alpha})`;
7543
+ context.save();
7544
+ context.globalAlpha = alpha;
7545
+ context.fillStyle = Theme.Graph.DotColor;
6754
7546
  const spacing = 100;
6755
7547
  const pi2 = 2 * Math.PI;
6756
7548
  const dotScale = 2 * scale;
@@ -6763,6 +7555,7 @@ function BuildBackgroundRenderer(backgroundColor) {
6763
7555
  context.fill();
6764
7556
  }
6765
7557
  }
7558
+ context.restore();
6766
7559
  };
6767
7560
  }
6768
7561
  var contextMenuGroup = "graph-context-menu";
@@ -6813,9 +7606,9 @@ var GraphView = class {
6813
7606
  const results = {};
6814
7607
  for (let i = 0; i < this.#subsystems.length; i++) {
6815
7608
  TimeExecution("Render_Subsystem_" + i, () => {
6816
- let results2 = this.#subsystems[i].render(ctx, camera, mousePosition);
6817
- if (results2?.cursorStyle) {
6818
- results2.cursorStyle = results2?.cursorStyle;
7609
+ const layer = this.#subsystems[i].render(ctx, camera, mousePosition);
7610
+ if (layer?.cursorStyle) {
7611
+ results.cursorStyle = layer.cursorStyle;
6819
7612
  }
6820
7613
  });
6821
7614
  }
@@ -6875,8 +7668,7 @@ var NodeFlowGraph = class {
6875
7668
  if (config?.backgroundRenderer !== void 0) {
6876
7669
  this.#backgroundRenderer = config?.backgroundRenderer;
6877
7670
  } else {
6878
- const backgroundColor = config?.backgroundColor === void 0 ? Theme.Graph.BackgroundColor : config.backgroundColor;
6879
- this.#backgroundRenderer = BuildBackgroundRenderer(backgroundColor);
7671
+ this.#backgroundRenderer = BuildBackgroundRenderer(config?.backgroundColor);
6880
7672
  }
6881
7673
  this.#minimap = new Minimap(config?.minimap);
6882
7674
  this.#mainNodeSubsystem.setConnectionReleasedHandler((dangling) => {
@@ -7343,11 +8135,15 @@ var NodeFlowGraph = class {
7343
8135
  ColorWidget,
7344
8136
  Connection,
7345
8137
  ContextMenuItemState,
8138
+ CursorStyle,
7346
8139
  DefaultConnectionRenderer,
8140
+ DefaultTheme,
7347
8141
  FlowNode,
7348
8142
  FlowNote,
7349
8143
  GlobalWidgetFactory,
8144
+ GraphView,
7350
8145
  ImageWidget,
8146
+ InBox,
7351
8147
  MessageType,
7352
8148
  Minimap,
7353
8149
  NodeFactory,
@@ -7357,16 +8153,28 @@ var NodeFlowGraph = class {
7357
8153
  Port,
7358
8154
  PortType,
7359
8155
  Publisher,
8156
+ RowWidget,
7360
8157
  SliderWidget,
7361
8158
  StringWidget,
7362
8159
  TextWidget,
7363
8160
  Theme,
7364
8161
  ToggleWidget,
8162
+ ValueWidget,
8163
+ WidgetBase,
7365
8164
  WidgetFactory,
8165
+ drawField,
8166
+ drawFieldBox,
8167
+ drawFieldText,
8168
+ editInPlace,
8169
+ formatNumber,
8170
+ onThemeChange,
7366
8171
  portConfigTypes,
7367
8172
  portsCompatible,
7368
8173
  renderContinuously,
7369
8174
  requestRender,
7370
- typeSetsCompatible
8175
+ setTheme,
8176
+ themeVersion,
8177
+ typeSetsCompatible,
8178
+ widgetFont
7371
8179
  });
7372
8180
  //# sourceMappingURL=index.js.map