@elicdavis/node-flow 0.1.20 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/index.d.mts +393 -226
  2. package/dist/index.d.ts +393 -226
  3. package/dist/index.js +1905 -1058
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +1888 -1057
  6. package/dist/index.mjs.map +1 -1
  7. package/index.html +1134 -329
  8. package/package.json +1 -1
  9. package/src/connection.ts +30 -5
  10. package/src/contextMenu.ts +66 -34
  11. package/src/graph.ts +19 -14
  12. package/src/index.ts +26 -12
  13. package/src/markdown/entry.ts +11 -15
  14. package/src/markdown/syntaxParser.ts +2 -1
  15. package/src/node.ts +160 -101
  16. package/src/nodes/subsystem.ts +113 -29
  17. package/src/notes/note.ts +9 -1
  18. package/src/organize.ts +540 -111
  19. package/src/popup.ts +4 -2
  20. package/src/port.ts +83 -6
  21. package/src/styles/bevel.ts +31 -0
  22. package/src/styles/cursor.ts +1 -0
  23. package/src/styles/text.ts +4 -4
  24. package/src/theme.ts +179 -92
  25. package/src/types/text.ts +5 -1
  26. package/src/utils/color.ts +1 -1
  27. package/src/widgets/button.ts +52 -107
  28. package/src/widgets/color.ts +91 -150
  29. package/src/widgets/factory.ts +89 -46
  30. package/src/widgets/field.ts +99 -0
  31. package/src/widgets/image.ts +107 -165
  32. package/src/widgets/inplace.ts +136 -0
  33. package/src/widgets/number.ts +115 -154
  34. package/src/widgets/row.ts +138 -0
  35. package/src/widgets/slider.ts +101 -216
  36. package/src/widgets/string.ts +32 -130
  37. package/src/widgets/text.ts +81 -92
  38. package/src/widgets/toggle.ts +62 -207
  39. package/src/widgets/value.ts +127 -0
  40. package/src/widgets/widget.ts +173 -6
  41. package/test/context_menu.test.ts +82 -0
  42. package/test/cursor.test.ts +17 -0
  43. package/test/hidden_port.test.ts +125 -0
  44. package/test/markdown.test.ts +2 -2
  45. package/test/markdown_code_block.test.ts +58 -0
  46. package/test/multi_input.test.ts +124 -0
  47. package/test/organize.test.ts +117 -1
  48. package/test/support.ts +188 -0
  49. package/test/theme.test.ts +86 -0
  50. package/test/widget_click.test.ts +111 -97
  51. package/test/widget_construction.test.ts +5 -4
  52. package/test/widgets.test.ts +386 -0
  53. package/test/wire_follows_camera.test.ts +57 -0
package/dist/index.mjs CHANGED
@@ -1,8 +1,46 @@
1
+ // src/render_scheduler.ts
2
+ var subscribers = /* @__PURE__ */ new Set();
3
+ var frameQueued = false;
4
+ var continuous = 0;
5
+ var canSchedule = () => typeof requestAnimationFrame === "function";
6
+ function onRenderRequested(redraw) {
7
+ subscribers.add(redraw);
8
+ return () => {
9
+ subscribers.delete(redraw);
10
+ };
11
+ }
12
+ function requestRender() {
13
+ if (frameQueued || !canSchedule()) {
14
+ return;
15
+ }
16
+ frameQueued = true;
17
+ requestAnimationFrame(() => {
18
+ frameQueued = false;
19
+ subscribers.forEach((redraw) => redraw());
20
+ if (continuous > 0) {
21
+ requestRender();
22
+ }
23
+ });
24
+ }
25
+ function renderContinuously() {
26
+ continuous++;
27
+ requestRender();
28
+ let released = false;
29
+ return () => {
30
+ if (released) {
31
+ return;
32
+ }
33
+ released = true;
34
+ continuous--;
35
+ };
36
+ }
37
+
1
38
  // src/theme.ts
2
- var Theme = {
39
+ var defaults = {
3
40
  FontFamily: "Courier New",
4
41
  Graph: {
5
- BackgroundColor: "#07212a"
42
+ BackgroundColor: "#07212a",
43
+ DotColor: "rgb(41, 54, 57)"
6
44
  },
7
45
  Node: {
8
46
  FontColor: "#afb9bb",
@@ -19,6 +57,7 @@ var Theme = {
19
57
  Grabbed: "white",
20
58
  Selected: "#6e6e6e"
21
59
  },
60
+ Bevel: null,
22
61
  Port: {
23
62
  FontColor: "#afb9bb"
24
63
  },
@@ -36,15 +75,25 @@ var Theme = {
36
75
  },
37
76
  Widget: {
38
77
  FontColor: "#afb9bb",
78
+ LabelColor: "#7f9497",
79
+ FontSize: 14,
80
+ Padding: 6,
81
+ Gap: 4,
39
82
  BackgroundColor: "#0f313c",
40
83
  Border: {
41
84
  Color: "#154050",
42
85
  Size: 2,
43
86
  Radius: 2
44
87
  },
88
+ Bevel: null,
45
89
  Hover: {
46
90
  BackgroundColor: "#195366"
47
91
  },
92
+ Input: {
93
+ BackgroundColor: "#0f313c",
94
+ HoverBackgroundColor: "#195366",
95
+ FontColor: "#afb9bb"
96
+ },
48
97
  Slider: {
49
98
  FillColor: "#07212A"
50
99
  },
@@ -57,8 +106,10 @@ var Theme = {
57
106
  ContextMenu: {
58
107
  BackgroundColor: "#07212A",
59
108
  HighlightColor: "#205A6D",
109
+ HighlightFontColor: "#afb9bb",
60
110
  FontColor: "#afb9bb",
61
- DisabledFontColor: "#4a585c"
111
+ DisabledFontColor: "#4a585c",
112
+ BorderRadius: 5
62
113
  },
63
114
  Minimap: {
64
115
  BackgroundColor: "rgba(10, 16, 18, 0.85)",
@@ -68,6 +119,13 @@ var Theme = {
68
119
  NodeColor: "#6b7f85",
69
120
  ViewportColor: "rgba(175, 185, 187, 0.25)"
70
121
  },
122
+ Popup: {
123
+ BackgroundColor: "#000000",
124
+ FontColor: "#ffffff",
125
+ FontFamily: "Verdana",
126
+ BorderRadius: 8,
127
+ Backdrop: "#00000070"
128
+ },
71
129
  Note: {
72
130
  FontColor: "#afb9bb",
73
131
  FontSize: 16,
@@ -86,11 +144,47 @@ var Theme = {
86
144
  },
87
145
  CodeBlock: {
88
146
  BackgroundColor: "#0c2b35",
147
+ FontColor: "#afb9bb",
89
148
  Padding: 16,
90
149
  BorderRadius: 4
91
150
  }
92
151
  }
93
152
  };
153
+ function clone(value) {
154
+ return JSON.parse(JSON.stringify(value));
155
+ }
156
+ function merge(target, source) {
157
+ if (source === void 0) {
158
+ return target;
159
+ }
160
+ if (source === null || typeof source !== "object" || target === null || typeof target !== "object") {
161
+ return source;
162
+ }
163
+ for (const key of Object.keys(source)) {
164
+ target[key] = merge(target[key], source[key]);
165
+ }
166
+ return target;
167
+ }
168
+ var DefaultTheme = Object.freeze(clone(defaults));
169
+ var Theme = clone(defaults);
170
+ var version = 0;
171
+ var listeners = /* @__PURE__ */ new Set();
172
+ function setTheme(config) {
173
+ const next = merge(clone(defaults), config);
174
+ for (const key of Object.keys(next)) {
175
+ Theme[key] = next[key];
176
+ }
177
+ version++;
178
+ listeners.forEach((listener) => listener());
179
+ requestRender();
180
+ }
181
+ function themeVersion() {
182
+ return version;
183
+ }
184
+ function onThemeChange(listener) {
185
+ listeners.add(listener);
186
+ return () => listeners.delete(listener);
187
+ }
94
188
 
95
189
  // src/styles/text.ts
96
190
  var DefaultSize = 16;
@@ -113,19 +207,19 @@ var TextStyle = class {
113
207
  constructor(config) {
114
208
  this.#size = config?.size === void 0 ? DefaultSize : config.size;
115
209
  this.#color = config?.color === void 0 ? DefaultColor : config.color;
116
- this.#font = config?.font === void 0 ? Theme.FontFamily : config.font;
210
+ this.#font = config?.font;
117
211
  this.#weight = config?.weight === void 0 ? "" /* Normal */ : config.weight;
118
212
  this.#fontStyle = config?.style === void 0 ? "" /* Normal */ : config.style;
119
213
  }
120
214
  setupStyle(ctx, scale) {
121
215
  ctx.fillStyle = this.#color;
122
- ctx.font = this.#fontStyle + " " + this.#weight + " " + this.#size * scale + "px " + this.#font;
216
+ ctx.font = this.#fontStyle + " " + this.#weight + " " + this.#size * scale + "px " + this.getFont();
123
217
  }
124
218
  getSize() {
125
219
  return this.#size;
126
220
  }
127
221
  getFont() {
128
- return this.#font;
222
+ return this.#font ?? Theme.FontFamily;
129
223
  }
130
224
  getWeight() {
131
225
  return this.#weight;
@@ -496,7 +590,18 @@ var ContextMenu = class _ContextMenu {
496
590
  }
497
591
  #tempBox = { Position: Zero(), Size: Zero() };
498
592
  #openSubMenu;
499
- render(ctx, pppp, graphScale, mousePosition, openRight) {
593
+ #bounds = null;
594
+ #contains(point) {
595
+ if (this.#bounds !== null && InBox(this.#bounds, point)) {
596
+ return true;
597
+ }
598
+ return this.#openSubMenu !== void 0 && this.#openSubMenu.#contains(point);
599
+ }
600
+ /**
601
+ * A submenu is anchored to its parent's left edge and told the parent's
602
+ * width, so it can open on whichever side has room.
603
+ */
604
+ render(ctx, anchor, graphScale, mousePosition, openRight, parentWidth = 0) {
500
605
  const menuScale = 1.25;
501
606
  const scaledEntryHeight = menuScale * contextEntryHeight;
502
607
  const scaledEntryWidth = menuScale * 40 + this.#getMaxWidthForText(ctx, menuScale);
@@ -505,19 +610,35 @@ var ContextMenu = class _ContextMenu {
505
610
  totalScaledHeight += this.#groups.At(i).height();
506
611
  }
507
612
  totalScaledHeight *= menuScale;
508
- const position = { x: 0, y: 0 };
509
- CopyVector2(position, pppp);
510
- if (!openRight) {
511
- position.x -= scaledEntryWidth;
613
+ const canvasWidth = ctx.canvas.clientWidth;
614
+ const position = { x: anchor.x, y: anchor.y };
615
+ let opensRight = openRight;
616
+ if (parentWidth === 0) {
617
+ if (!openRight) {
618
+ position.x -= scaledEntryWidth;
619
+ }
620
+ if (openRight && position.x + scaledEntryWidth > canvasWidth) {
621
+ position.x = canvasWidth - scaledEntryWidth;
622
+ opensRight = false;
623
+ }
624
+ } else {
625
+ const rightOfParent = anchor.x + parentWidth;
626
+ const leftOfParent = anchor.x - scaledEntryWidth;
627
+ const fitsRight = rightOfParent + scaledEntryWidth <= canvasWidth;
628
+ const fitsLeft = leftOfParent >= 0;
629
+ opensRight = openRight ? fitsRight || !fitsLeft : !fitsLeft && fitsRight;
630
+ position.x = opensRight ? rightOfParent : leftOfParent;
631
+ position.x = Math.max(0, Math.min(position.x, canvasWidth - scaledEntryWidth));
512
632
  }
513
633
  if (position.y + totalScaledHeight > ctx.canvas.clientHeight) {
514
634
  position.y = ctx.canvas.clientHeight - totalScaledHeight;
515
635
  }
516
- let submenuOpenRight = openRight;
517
- if (openRight && position.x + scaledEntryWidth > ctx.canvas.clientWidth) {
518
- position.x = ctx.canvas.clientWidth - scaledEntryWidth;
519
- submenuOpenRight = !submenuOpenRight;
520
- }
636
+ position.y = Math.max(0, position.y);
637
+ this.#bounds = {
638
+ Position: { x: position.x, y: position.y },
639
+ Size: { x: scaledEntryWidth, y: totalScaledHeight }
640
+ };
641
+ const pointer = mousePosition !== void 0 && this.#openSubMenu !== void 0 && this.#openSubMenu.#contains(mousePosition) ? void 0 : mousePosition;
521
642
  this.#tempBox.Size.x = scaledEntryWidth;
522
643
  this.#tempBox.Size.y = scaledEntryHeight;
523
644
  CopyVector2(this.#tempBox.Position, position);
@@ -531,36 +652,36 @@ var ContextMenu = class _ContextMenu {
531
652
  this.#tempBox.Position.y,
532
653
  scaledEntryWidth,
533
654
  totalScaledHeight,
534
- 5 * menuScale
655
+ Theme.ContextMenu.BorderRadius * menuScale
535
656
  );
536
657
  ctx.fill();
537
658
  ctx.shadowBlur = 0;
538
659
  let mouseIsOver = null;
539
- let subOpenedThisFrame = false;
660
+ if (pointer !== void 0) {
661
+ let row = 0;
662
+ for (let groupIndex = 0; groupIndex < this.#groups.Count(); groupIndex++) {
663
+ for (const entry of this.#groups.At(groupIndex).entries) {
664
+ this.#tempBox.Position.y = position.y + scaledEntryHeight * row;
665
+ if (InBox(this.#tempBox, pointer)) {
666
+ mouseIsOver = entry;
667
+ this.#openSubMenu = entry.subMenu;
668
+ this.#submenuPosition.x = position.x;
669
+ this.#submenuPosition.y = this.#tempBox.Position.y;
670
+ }
671
+ row++;
672
+ }
673
+ }
674
+ }
540
675
  let optionsRendered = 0;
541
676
  for (let groupIndex = 0; groupIndex < this.#groups.Count(); groupIndex++) {
542
677
  const group = this.#groups.At(groupIndex);
543
678
  for (let entryIndex = 0; entryIndex < group.entries.length; entryIndex++) {
544
679
  const entry = group.entries[entryIndex];
545
680
  this.#tempBox.Position.y = position.y + scaledEntryHeight * optionsRendered;
546
- let entryMousedOver = false;
547
- if (mousePosition !== void 0 && InBox(this.#tempBox, mousePosition)) {
548
- mouseIsOver = entry;
549
- entryMousedOver = true;
550
- if (entry.subMenu !== void 0) {
551
- this.#openSubMenu = entry.subMenu;
552
- this.#submenuPosition.x = position.x;
553
- this.#submenuPosition.y = this.#tempBox.Position.y;
554
- if (submenuOpenRight) {
555
- this.#submenuPosition.x += scaledEntryWidth;
556
- }
557
- subOpenedThisFrame = true;
558
- } else {
559
- this.#openSubMenu = void 0;
560
- }
561
- }
681
+ const entryMousedOver = entry === mouseIsOver;
562
682
  const itemDisabled = entry.item !== void 0 && entry.item.state() === 1 /* Disabled */;
563
- if (!itemDisabled && (entryMousedOver || this.#openSubMenu !== void 0 && entry.subMenu === this.#openSubMenu)) {
683
+ const highlighted = !itemDisabled && (entryMousedOver || this.#openSubMenu !== void 0 && entry.subMenu === this.#openSubMenu);
684
+ if (highlighted) {
564
685
  ctx.fillStyle = Theme.ContextMenu.HighlightColor;
565
686
  ctx.beginPath();
566
687
  ctx.roundRect(
@@ -568,18 +689,20 @@ var ContextMenu = class _ContextMenu {
568
689
  this.#tempBox.Position.y + scaledEntryHeight / 10,
569
690
  scaledEntryWidth - scaledEntryHeight / 5,
570
691
  scaledEntryHeight - scaledEntryHeight / 5,
571
- 5 * menuScale
692
+ Theme.ContextMenu.BorderRadius * menuScale
572
693
  );
573
694
  ctx.fill();
574
695
  }
575
696
  this.#textStyle.setupStyle(ctx, menuScale);
576
697
  if (itemDisabled) {
577
698
  ctx.fillStyle = Theme.ContextMenu.DisabledFontColor;
699
+ } else if (highlighted) {
700
+ ctx.fillStyle = Theme.ContextMenu.HighlightFontColor;
578
701
  }
579
702
  ctx.fillText(entry.text, position.x + scaledEntryHeight / 5, this.#tempBox.Position.y + scaledEntryHeight / 2);
580
703
  if (entry.subMenu !== void 0) {
581
704
  ctx.beginPath();
582
- ctx.strokeStyle = Theme.ContextMenu.FontColor;
705
+ ctx.strokeStyle = highlighted ? Theme.ContextMenu.HighlightFontColor : Theme.ContextMenu.FontColor;
583
706
  ctx.lineWidth = 1 * menuScale;
584
707
  ctx.lineTo(position.x + scaledEntryWidth - scaledEntryHeight / 2.5, this.#tempBox.Position.y + scaledEntryHeight / 3);
585
708
  ctx.lineTo(position.x + scaledEntryWidth - scaledEntryHeight / 4, this.#tempBox.Position.y + scaledEntryHeight / 2);
@@ -600,7 +723,7 @@ var ContextMenu = class _ContextMenu {
600
723
  }
601
724
  }
602
725
  if (this.#openSubMenu !== void 0) {
603
- const mouseOverSub = this.#openSubMenu.render(ctx, this.#submenuPosition, menuScale, mousePosition, submenuOpenRight);
726
+ const mouseOverSub = this.#openSubMenu.render(ctx, this.#submenuPosition, menuScale, mousePosition, opensRight, scaledEntryWidth);
604
727
  if (mouseOverSub !== null) {
605
728
  mouseIsOver = mouseOverSub;
606
729
  }
@@ -609,43 +732,6 @@ var ContextMenu = class _ContextMenu {
609
732
  }
610
733
  };
611
734
 
612
- // src/render_scheduler.ts
613
- var subscribers = /* @__PURE__ */ new Set();
614
- var frameQueued = false;
615
- var continuous = 0;
616
- var canSchedule = () => typeof requestAnimationFrame === "function";
617
- function onRenderRequested(redraw) {
618
- subscribers.add(redraw);
619
- return () => {
620
- subscribers.delete(redraw);
621
- };
622
- }
623
- function requestRender() {
624
- if (frameQueued || !canSchedule()) {
625
- return;
626
- }
627
- frameQueued = true;
628
- requestAnimationFrame(() => {
629
- frameQueued = false;
630
- subscribers.forEach((redraw) => redraw());
631
- if (continuous > 0) {
632
- requestRender();
633
- }
634
- });
635
- }
636
- function renderContinuously() {
637
- continuous++;
638
- requestRender();
639
- let released = false;
640
- return () => {
641
- if (released) {
642
- return;
643
- }
644
- released = true;
645
- continuous--;
646
- };
647
- }
648
-
649
735
  // src/input.ts
650
736
  var LONG_PRESS_MS = 500;
651
737
  var LONG_PRESS_SLOP = 12;
@@ -1230,6 +1316,7 @@ function splitStringIntoLines(ctx, str, maxWidth) {
1230
1316
  // src/types/text.ts
1231
1317
  var Text = class _Text {
1232
1318
  #measured;
1319
+ #measuredForTheme = -1;
1233
1320
  #size;
1234
1321
  #style;
1235
1322
  #value;
@@ -1309,9 +1396,10 @@ var Text = class _Text {
1309
1396
  return results;
1310
1397
  }
1311
1398
  #measure(ctx) {
1312
- if (this.#measured) {
1399
+ if (this.#measured && this.#measuredForTheme === themeVersion()) {
1313
1400
  return;
1314
1401
  }
1402
+ this.#measuredForTheme = themeVersion();
1315
1403
  this.#style.setupStyle(ctx, 1);
1316
1404
  if (this.#maxWidth == -1) {
1317
1405
  const measurements = ctx.measureText(this.#value);
@@ -1387,7 +1475,7 @@ function RgbToHex(c) {
1387
1475
  return "#" + componentToHex(Math.round(c.r * 255)) + componentToHex(Math.round(c.g * 255)) + componentToHex(Math.round(c.b * 255));
1388
1476
  }
1389
1477
  function HexToColor(hex) {
1390
- var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
1478
+ var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})(?:[a-f\d]{2})?$/i.exec(hex);
1391
1479
  return result ? {
1392
1480
  r: parseInt(result[1], 16) / 255,
1393
1481
  g: parseInt(result[2], 16) / 255,
@@ -1477,6 +1565,21 @@ function portConfigTypes(config) {
1477
1565
  }
1478
1566
  return [config.type === void 0 ? "" : config.type];
1479
1567
  }
1568
+ var ARRAY_SLOT_PITCH = 12;
1569
+ function arrayInsertIndex(box, count, y) {
1570
+ if (count === 0) {
1571
+ return 0;
1572
+ }
1573
+ const index = Math.round((y - box.Position.y) / (box.Size.y / count));
1574
+ return Math.min(count, Math.max(0, index));
1575
+ }
1576
+ function arraySlotCenter(box, count, index) {
1577
+ const slots = Math.max(1, count);
1578
+ return {
1579
+ x: box.Position.x + box.Size.x / 2,
1580
+ y: box.Position.y + (index + 0.5) * box.Size.y / slots
1581
+ };
1582
+ }
1480
1583
  function fallbackColor(type, s) {
1481
1584
  let value = 0;
1482
1585
  for (var i = 0; i < type.length; i++) {
@@ -1501,6 +1604,7 @@ var Port = class {
1501
1604
  #acceptedTypes;
1502
1605
  #description;
1503
1606
  #array;
1607
+ #hidden;
1504
1608
  #configuredEmptyFill;
1505
1609
  #configuredFilledFill;
1506
1610
  #dataTypePopupElement;
@@ -1516,6 +1620,7 @@ var Port = class {
1516
1620
  this.#acceptedTypes = config?.acceptedTypes === void 0 ? [] : config.acceptedTypes;
1517
1621
  this.#description = config?.description === void 0 ? "" : config.description;
1518
1622
  this.#array = config?.array === true;
1623
+ this.#hidden = config?.hidden === true;
1519
1624
  this.#configuredEmptyFill = config?.emptyStyle?.fillColor;
1520
1625
  this.#configuredFilledFill = config?.filledStyle?.fillColor;
1521
1626
  this.#emptyStyle = {
@@ -1614,9 +1719,11 @@ var Port = class {
1614
1719
  this.setDataType(type);
1615
1720
  this.#refreshDataType();
1616
1721
  }
1617
- addConnection(connection) {
1618
- const c = this.#connections.length;
1619
- this.#connections.push(connection);
1722
+ addConnection(connection, index) {
1723
+ const c = index === void 0 ? this.#connections.length : Math.min(Math.max(index, 0), this.#connections.length);
1724
+ const was = this.hidden();
1725
+ this.#connections.splice(c, 0, connection);
1726
+ this.#visibilityMayHaveChanged(was);
1620
1727
  for (let i = 0; i < this.#onConnectionAdded.length; i++) {
1621
1728
  this.#onConnectionAdded[i](connection, c, this, this.#portType, this.#node);
1622
1729
  }
@@ -1637,6 +1744,32 @@ var Port = class {
1637
1744
  connections() {
1638
1745
  return this.#connections;
1639
1746
  }
1747
+ /**
1748
+ * How many wires the port has room for: one per connection on an array
1749
+ * input, so each can be told apart and picked up on its own.
1750
+ */
1751
+ slots() {
1752
+ if (this.#portType !== "INPUTARRAY" /* InputArray */) {
1753
+ return 1;
1754
+ }
1755
+ return Math.max(1, this.#connections.length);
1756
+ }
1757
+ setHidden(hidden) {
1758
+ const was = this.hidden();
1759
+ this.#hidden = hidden;
1760
+ this.#visibilityMayHaveChanged(was);
1761
+ }
1762
+ #visibilityMayHaveChanged(was) {
1763
+ if (was !== this.hidden() || this.#portType === "INPUTARRAY" /* InputArray */) {
1764
+ this.#node?.invalidateLayout();
1765
+ }
1766
+ }
1767
+ /**
1768
+ * A port asked to hide still shows while anything is connected to it.
1769
+ */
1770
+ hidden() {
1771
+ return this.#hidden && this.#connections.length === 0;
1772
+ }
1640
1773
  addConnectionRemovedListener(callback) {
1641
1774
  if (callback === void 0) {
1642
1775
  return;
@@ -1646,7 +1779,9 @@ var Port = class {
1646
1779
  clearConnection(connection) {
1647
1780
  const index = this.#connections.indexOf(connection);
1648
1781
  if (index > -1) {
1782
+ const was = this.hidden();
1649
1783
  this.#connections.splice(index, 1);
1784
+ this.#visibilityMayHaveChanged(was);
1650
1785
  for (let i = 0; i < this.#onConnectionRemoved.length; i++) {
1651
1786
  this.#onConnectionRemoved[i](connection, index, this, this.#portType, this.#node);
1652
1787
  }
@@ -1705,20 +1840,31 @@ var Port = class {
1705
1840
  this.#dataTypePopupElement.render(ctx, { x: xPos - size.x / 2, y: yPos }, 1, size);
1706
1841
  });
1707
1842
  }
1843
+ const slots = this.slots();
1844
+ const height2 = slots === 1 ? scaledRadius * 2 : slots * ARRAY_SLOT_PITCH * camera.zoom;
1708
1845
  this.#box.Position.x = position.x - scaledRadius;
1709
- this.#box.Position.y = position.y - scaledRadius;
1846
+ this.#box.Position.y = position.y - height2 / 2;
1710
1847
  this.#box.Size.x = scaledRadius * 2;
1711
- this.#box.Size.y = scaledRadius * 2;
1848
+ this.#box.Size.y = height2;
1712
1849
  ctx.strokeStyle = style.borderColor;
1713
1850
  ctx.fillStyle = style.fillColor;
1714
1851
  ctx.beginPath();
1715
1852
  if (this.#portType === "INPUTARRAY" /* InputArray */) {
1716
- ctx.rect(position.x - scaledRadius, position.y - scaledRadius, scaledRadius * 2, scaledRadius * 2);
1853
+ ctx.roundRect(this.#box.Position.x, this.#box.Position.y, this.#box.Size.x, this.#box.Size.y, slots === 1 ? 0 : scaledRadius);
1717
1854
  } else {
1718
1855
  ctx.arc(position.x, position.y, scaledRadius, 0, 2 * Math.PI);
1719
1856
  }
1720
1857
  ctx.fill();
1721
1858
  ctx.stroke();
1859
+ if (slots > 1) {
1860
+ ctx.fillStyle = style.borderColor;
1861
+ for (let i = 0; i < slots; i++) {
1862
+ const slot = arraySlotCenter(this.#box, slots, i);
1863
+ ctx.beginPath();
1864
+ ctx.arc(slot.x, slot.y, scaledRadius * 0.4, 0, 2 * Math.PI);
1865
+ ctx.fill();
1866
+ }
1867
+ }
1722
1868
  return this.#box;
1723
1869
  }
1724
1870
  };
@@ -1834,142 +1980,330 @@ var BoxStyle = class {
1834
1980
  }
1835
1981
  };
1836
1982
 
1837
- // src/popup.ts
1838
- var Popup = class {
1839
- #title;
1840
- #options;
1841
- #content;
1842
- #onClose;
1843
- // Styling ================================================================
1844
- #buttonCSS;
1845
- // Runtime ================================================================
1846
- #popup;
1847
- #keyHandler;
1848
- constructor(config) {
1849
- this.#title = config.title;
1850
- this.#options = config.options;
1851
- this.#content = config.content;
1852
- this.#onClose = config.onClose;
1853
- this.#buttonCSS = config.buttonCSS === void 0 ? "flex-grow: 1; margin-right: 8px;" : config.buttonCSS;
1854
- this.#popup = null;
1855
- this.#keyHandler = null;
1856
- }
1857
- Hide() {
1858
- if (this.#popup === null) {
1859
- return;
1860
- }
1861
- if (this.#keyHandler !== null) {
1862
- document.removeEventListener("keydown", this.#keyHandler, true);
1863
- this.#keyHandler = null;
1864
- }
1865
- document.body.removeChild(this.#popup);
1866
- this.#popup = null;
1983
+ // src/styles/cursor.ts
1984
+ var CursorStyle = /* @__PURE__ */ ((CursorStyle2) => {
1985
+ CursorStyle2["Default"] = "default";
1986
+ CursorStyle2["Pointer"] = "pointer";
1987
+ CursorStyle2["Grab"] = "grab";
1988
+ CursorStyle2["Grabbing"] = "grabbing";
1989
+ CursorStyle2["Wait"] = "wait";
1990
+ CursorStyle2["Help"] = "help";
1991
+ CursorStyle2["Crosshair"] = "crosshair";
1992
+ CursorStyle2["Cell"] = "cell";
1993
+ CursorStyle2["Text"] = "text";
1994
+ CursorStyle2["Move"] = "move";
1995
+ CursorStyle2["NotAllowed"] = "not-allowed";
1996
+ CursorStyle2["ZoomIn"] = "zoom-in";
1997
+ CursorStyle2["ZoomOut"] = "zoom-out";
1998
+ CursorStyle2["None"] = "none";
1999
+ CursorStyle2["ContextMenu"] = "context-menu";
2000
+ CursorStyle2["EWResize"] = "ew-resize";
2001
+ return CursorStyle2;
2002
+ })(CursorStyle || {});
2003
+
2004
+ // src/styles/bevel.ts
2005
+ function drawBevel(ctx, box, scale, colors, sunken) {
2006
+ const width2 = Math.max(1, 2 * scale);
2007
+ const half = width2 / 2;
2008
+ const left = box.Position.x + half;
2009
+ const top = box.Position.y + half;
2010
+ const right = box.Position.x + box.Size.x - half;
2011
+ const bottom = box.Position.y + box.Size.y - half;
2012
+ ctx.save();
2013
+ ctx.lineWidth = width2;
2014
+ ctx.lineCap = "square";
2015
+ ctx.strokeStyle = sunken ? colors.Dark : colors.Light;
2016
+ ctx.beginPath();
2017
+ ctx.moveTo(left, bottom);
2018
+ ctx.lineTo(left, top);
2019
+ ctx.lineTo(right, top);
2020
+ ctx.stroke();
2021
+ ctx.strokeStyle = sunken ? colors.Light : colors.Dark;
2022
+ ctx.beginPath();
2023
+ ctx.moveTo(right, top);
2024
+ ctx.lineTo(right, bottom);
2025
+ ctx.lineTo(left, bottom);
2026
+ ctx.stroke();
2027
+ ctx.restore();
2028
+ }
2029
+
2030
+ // src/widgets/field.ts
2031
+ function widgetFont(scale) {
2032
+ return `${Theme.Widget.FontSize * scale}px ${Theme.FontFamily}`;
2033
+ }
2034
+ function fieldBackground(look) {
2035
+ if (look.background !== void 0) {
2036
+ return look.background;
1867
2037
  }
1868
- #close(option) {
1869
- this.Hide();
1870
- if (this.#onClose !== void 0) {
1871
- this.#onClose(option);
1872
- }
2038
+ if (look.input) {
2039
+ return look.hovered ? Theme.Widget.Input.HoverBackgroundColor : Theme.Widget.Input.BackgroundColor;
1873
2040
  }
1874
- Show() {
1875
- this.#popup = document.createElement("div");
1876
- this.#popup.style.cssText = "position:absolute;top:0;width:100%;display:flex;height:100%;justify-content:center;align-items:center;background-color:#00000070;";
1877
- document.body.appendChild(this.#popup);
1878
- const container = document.createElement("div");
1879
- container.style.cssText = "background-color:#000;color:white;font-family:Verdana;border-radius:8px;padding:16px;";
1880
- this.#popup.appendChild(container);
1881
- const title = document.createElement("h2");
1882
- title.textContent = this.#title;
1883
- title.style.cssText = "margin-top:0;margin-bottom:16px;";
1884
- container.appendChild(title);
1885
- if (this.#content !== void 0) {
1886
- container.appendChild(this.#content());
1887
- }
1888
- const buttonContainer = document.createElement("div");
1889
- buttonContainer.style.cssText = "margin-top:16px;justify-content:space-around;align-items:center;flex-direction:row;display:flex;";
1890
- container.appendChild(buttonContainer);
1891
- for (let i = 0; i < this.#options.length; i++) {
1892
- const button = document.createElement("button");
1893
- button.style.cssText = this.#buttonCSS;
1894
- button.textContent = this.#options[i];
1895
- button.onclick = () => this.#close(this.#options[i]);
1896
- buttonContainer.appendChild(button);
1897
- }
1898
- this.#keyHandler = (e) => {
1899
- if (e.key === "Enter") {
1900
- e.preventDefault();
1901
- this.#close(this.#options.length > 0 ? this.#options[0] : null);
1902
- } else if (e.key === "Escape") {
1903
- e.preventDefault();
1904
- this.#close(null);
1905
- }
1906
- e.stopPropagation();
1907
- };
1908
- document.addEventListener("keydown", this.#keyHandler, true);
2041
+ if (look.pressed) {
2042
+ return Theme.Widget.Button.Click.BackgroundColor;
1909
2043
  }
1910
- };
1911
-
1912
- // src/styles/textBox.ts
1913
- function TextBoxStyleWithFallback(input, fallback) {
1914
- return {
1915
- box: BoxStyleWithFallback(input?.box, fallback?.box),
1916
- text: TextStyleFallback(input?.text, fallback?.text),
1917
- textAlign: input?.textAlign === void 0 ? fallback?.textAlign : input?.textAlign,
1918
- textBaseline: input?.textBaseline === void 0 ? fallback?.textBaseline : input?.textBaseline
1919
- };
2044
+ return look.hovered ? Theme.Widget.Hover.BackgroundColor : Theme.Widget.BackgroundColor;
1920
2045
  }
1921
- var TextBoxStyle = class {
1922
- #box;
1923
- #text;
1924
- #textAlign;
1925
- #textBaseline;
1926
- constructor(config) {
1927
- this.#box = new BoxStyle(config?.box);
1928
- this.#text = new TextStyle(config?.text);
1929
- this.#textAlign = config?.textAlign === void 0 ? "center" /* Center */ : config.textAlign;
1930
- this.#textBaseline = config?.textBaseline === void 0 ? "middle" /* Middle */ : config.textBaseline;
1931
- }
1932
- Draw(ctx, box, scale, text) {
1933
- this.#box.Draw(ctx, box, scale);
1934
- ctx.textAlign = this.#textAlign;
1935
- ctx.textBaseline = this.#textBaseline;
1936
- this.#text.setupStyle(ctx, scale);
1937
- ctx.fillText(
1938
- text,
1939
- box.Position.x + box.Size.x / 2,
1940
- box.Position.y + box.Size.y / 2
1941
- );
2046
+ function drawFieldBox(ctx, box, scale, look) {
2047
+ ctx.fillStyle = fieldBackground(look);
2048
+ ctx.beginPath();
2049
+ ctx.roundRect(box.Position.x, box.Position.y, box.Size.x, box.Size.y, Theme.Widget.Border.Radius * scale);
2050
+ ctx.fill();
2051
+ if (Theme.Widget.Border.Size > 0) {
2052
+ ctx.strokeStyle = look.border ?? Theme.Widget.Border.Color;
2053
+ ctx.lineWidth = Theme.Widget.Border.Size * scale;
2054
+ ctx.stroke();
1942
2055
  }
1943
- DrawUnderline(ctx, box, scale, text) {
1944
- this.#box.DrawUnderline(ctx, box, scale);
1945
- ctx.textAlign = this.#textAlign;
1946
- ctx.textBaseline = this.#textBaseline;
1947
- this.#text.setupStyle(ctx, scale);
2056
+ if (Theme.Widget.Bevel !== null) {
2057
+ drawBevel(ctx, box, scale, Theme.Widget.Bevel, look.input === true || look.pressed === true);
2058
+ }
2059
+ }
2060
+ function drawField(ctx, box, scale, look) {
2061
+ drawFieldBox(ctx, box, scale, look);
2062
+ drawFieldText(ctx, box, scale, look);
2063
+ }
2064
+ function drawFieldText(ctx, box, scale, look) {
2065
+ const padding = Theme.Widget.Padding * scale;
2066
+ const middle = box.Position.y + box.Size.y / 2;
2067
+ const textColor = look.textColor ?? (look.input ? Theme.Widget.Input.FontColor : Theme.Widget.FontColor);
2068
+ ctx.font = widgetFont(scale);
2069
+ ctx.textBaseline = "middle";
2070
+ if (look.label === void 0 || look.label === "") {
2071
+ ctx.textAlign = "center";
2072
+ ctx.fillStyle = textColor;
1948
2073
  ctx.fillText(
1949
- text,
2074
+ fitString(ctx, look.value, box.Size.x - padding * 2),
1950
2075
  box.Position.x + box.Size.x / 2,
1951
- box.Position.y + box.Size.y / 2
2076
+ middle
1952
2077
  );
2078
+ return;
1953
2079
  }
1954
- setTextColor(color) {
1955
- this.#text.setColor(color);
1956
- }
1957
- setBoxColor(color) {
1958
- this.#box.setColor(color);
1959
- }
1960
- setBorderColor(color) {
1961
- this.#box.setBorderColor(color);
2080
+ const room = box.Size.x - padding * 2;
2081
+ const label = fitString(ctx, look.label, room / 2);
2082
+ const labelWidth = ctx.measureText(label).width;
2083
+ ctx.textAlign = "left";
2084
+ ctx.fillStyle = look.textColor ?? Theme.Widget.LabelColor;
2085
+ ctx.fillText(label, box.Position.x + padding, middle);
2086
+ ctx.textAlign = "right";
2087
+ ctx.fillStyle = textColor;
2088
+ ctx.fillText(
2089
+ fitString(ctx, look.value, room - labelWidth - padding),
2090
+ box.Position.x + box.Size.x - padding,
2091
+ middle
2092
+ );
2093
+ }
2094
+
2095
+ // src/widgets/inplace.ts
2096
+ var open = /* @__PURE__ */ new Set();
2097
+ function placeEditsDuring(canvas, draw) {
2098
+ open.forEach((edit) => {
2099
+ if (edit.canvas === canvas) {
2100
+ edit.placed = false;
2101
+ }
2102
+ });
2103
+ draw();
2104
+ open.forEach((edit) => {
2105
+ if (edit.canvas === canvas && !edit.placed) {
2106
+ edit.input.style.opacity = "0";
2107
+ edit.input.style.pointerEvents = "none";
2108
+ }
2109
+ });
2110
+ }
2111
+ function editInPlace(config) {
2112
+ const canvas = config.canvas;
2113
+ if (canvas === null) {
2114
+ return null;
1962
2115
  }
1963
- };
2116
+ const input = document.createElement("input");
2117
+ input.type = "text";
2118
+ input.value = config.value;
2119
+ const edit = { canvas, input, placed: true };
2120
+ const place = (box, scale) => {
2121
+ const rect = canvas.getBoundingClientRect();
2122
+ const sx = canvas.width > 0 ? rect.width / canvas.width : 1;
2123
+ const sy = canvas.height > 0 ? rect.height / canvas.height : 1;
2124
+ const left = rect.left + box.Position.x * sx;
2125
+ const top = rect.top + box.Position.y * sy;
2126
+ const width2 = box.Size.x * sx;
2127
+ const height2 = box.Size.y * sy;
2128
+ const clip = [
2129
+ Math.max(0, rect.top - top),
2130
+ Math.max(0, left + width2 - (rect.left + rect.width)),
2131
+ Math.max(0, top + height2 - (rect.top + rect.height)),
2132
+ Math.max(0, rect.left - left)
2133
+ ];
2134
+ edit.placed = true;
2135
+ input.style.cssText = [
2136
+ "position: fixed",
2137
+ `left: ${left}px`,
2138
+ `top: ${top}px`,
2139
+ `width: ${width2}px`,
2140
+ `height: ${height2}px`,
2141
+ `clip-path: inset(${clip.map((c) => `${c}px`).join(" ")})`,
2142
+ "box-sizing: border-box",
2143
+ "margin: 0",
2144
+ `padding: 0 ${Theme.Widget.Padding * scale}px`,
2145
+ `font: ${Theme.Widget.FontSize * scale * sy}px ${Theme.FontFamily}`,
2146
+ `color: ${Theme.Widget.FontColor}`,
2147
+ `background: ${Theme.Widget.Hover.BackgroundColor}`,
2148
+ `border: ${Theme.Widget.Border.Size * scale}px solid ${Theme.Widget.Border.Color}`,
2149
+ `border-radius: ${Theme.Widget.Border.Radius * scale}px`,
2150
+ "outline: none",
2151
+ "z-index: 1000"
2152
+ ].join(";");
2153
+ };
2154
+ place(config.box, config.scale);
2155
+ let done = false;
2156
+ const finish = (keep) => {
2157
+ if (done) {
2158
+ return;
2159
+ }
2160
+ done = true;
2161
+ open.delete(edit);
2162
+ input.remove();
2163
+ if (keep) {
2164
+ config.onCommit(input.value);
2165
+ }
2166
+ requestRender();
2167
+ };
2168
+ input.addEventListener("keydown", (event) => {
2169
+ event.stopPropagation();
2170
+ if (event.key === "Enter") {
2171
+ event.preventDefault();
2172
+ finish(true);
2173
+ } else if (event.key === "Escape") {
2174
+ event.preventDefault();
2175
+ finish(false);
2176
+ }
2177
+ });
2178
+ input.addEventListener("blur", () => finish(true));
2179
+ open.add(edit);
2180
+ document.body.appendChild(input);
2181
+ input.focus();
2182
+ input.select();
2183
+ return { input, place };
2184
+ }
1964
2185
 
1965
2186
  // src/widgets/widget.ts
1966
2187
  var width = 150;
1967
2188
  var height = 25;
1968
- function startingWidgetValue(node, property, configured, fallback) {
1969
- if (configured !== void 0) {
1970
- return configured;
2189
+ var WidgetBase = class {
2190
+ #hidden = false;
2191
+ #disabled = false;
2192
+ #hovered = false;
2193
+ #pressed = false;
2194
+ #box = { Position: { x: 0, y: 0 }, Size: { x: 0, y: 0 } };
2195
+ #canvas = null;
2196
+ #scale = 1;
2197
+ #editing = null;
2198
+ Draw(ctx, box, scale, mousePosition) {
2199
+ this.#box = { Position: { x: box.Position.x, y: box.Position.y }, Size: { x: box.Size.x, y: box.Size.y } };
2200
+ this.#canvas = ctx.canvas ?? null;
2201
+ this.#scale = scale;
2202
+ this.#hovered = !this.#disabled && mousePosition !== void 0 && InBox(box, mousePosition);
2203
+ if (this.#editing !== null) {
2204
+ if (this.#editing.input.isConnected) {
2205
+ this.#editing.place(this.#box, scale);
2206
+ } else {
2207
+ this.#editing = null;
2208
+ }
2209
+ }
2210
+ ctx.save();
2211
+ if (this.#disabled) {
2212
+ ctx.globalAlpha *= 0.4;
2213
+ }
2214
+ this.render(ctx, box, scale);
2215
+ ctx.restore();
1971
2216
  }
1972
- if (property !== void 0 && property !== null) {
2217
+ PointerDown(pointer) {
2218
+ if (this.#disabled) {
2219
+ return;
2220
+ }
2221
+ this.#pressed = true;
2222
+ this.onPointerDown(pointer);
2223
+ requestRender();
2224
+ }
2225
+ PointerMove(pointer) {
2226
+ if (!this.#pressed) {
2227
+ return;
2228
+ }
2229
+ this.onPointerMove(pointer);
2230
+ requestRender();
2231
+ }
2232
+ PointerUp(pointer) {
2233
+ if (!this.#pressed) {
2234
+ return;
2235
+ }
2236
+ this.#pressed = false;
2237
+ this.onPointerUp(pointer);
2238
+ if (pointer.inside) {
2239
+ this.onClick(pointer);
2240
+ }
2241
+ requestRender();
2242
+ }
2243
+ Cursor() {
2244
+ return "pointer" /* Pointer */;
2245
+ }
2246
+ Hidden() {
2247
+ return this.#hidden;
2248
+ }
2249
+ setHidden(hidden) {
2250
+ this.#hidden = hidden;
2251
+ requestRender();
2252
+ }
2253
+ Disabled() {
2254
+ return this.#disabled;
2255
+ }
2256
+ setDisabled(disabled) {
2257
+ this.#disabled = disabled;
2258
+ if (disabled) {
2259
+ this.#pressed = false;
2260
+ }
2261
+ requestRender();
2262
+ }
2263
+ hovered() {
2264
+ return this.#hovered;
2265
+ }
2266
+ pressed() {
2267
+ return this.#pressed;
2268
+ }
2269
+ /**
2270
+ * Where the widget was last drawn, in canvas pixels.
2271
+ */
2272
+ box() {
2273
+ return this.#box;
2274
+ }
2275
+ canvas() {
2276
+ return this.#canvas;
2277
+ }
2278
+ scale() {
2279
+ return this.#scale;
2280
+ }
2281
+ editText(value, onCommit) {
2282
+ this.#editing = editInPlace({
2283
+ canvas: this.#canvas,
2284
+ box: this.#box,
2285
+ scale: this.#scale,
2286
+ value,
2287
+ onCommit
2288
+ });
2289
+ }
2290
+ onPointerDown(pointer) {
2291
+ }
2292
+ onPointerMove(pointer) {
2293
+ }
2294
+ onPointerUp(pointer) {
2295
+ }
2296
+ /**
2297
+ * A press and release that both landed on the widget.
2298
+ */
2299
+ onClick(pointer) {
2300
+ }
2301
+ };
2302
+ function startingWidgetValue(node, property, configured, fallback) {
2303
+ if (configured !== void 0) {
2304
+ return configured;
2305
+ }
2306
+ if (property !== void 0 && property !== null) {
1973
2307
  const existing = node.getProperty(property);
1974
2308
  if (existing !== void 0 && existing !== null) {
1975
2309
  return existing;
@@ -1978,187 +2312,192 @@ function startingWidgetValue(node, property, configured, fallback) {
1978
2312
  return fallback;
1979
2313
  }
1980
2314
 
1981
- // src/widgets/number.ts
1982
- var NumberWidget = class {
2315
+ // src/widgets/value.ts
2316
+ var ValueWidget = class extends WidgetBase {
1983
2317
  #node;
1984
- #nodeProperty;
2318
+ #property;
1985
2319
  #value;
1986
- #idleBoxStyle;
1987
- #highlightBoxStyle;
1988
- #text;
2320
+ #label;
2321
+ #width;
1989
2322
  #callback;
1990
- constructor(node, config) {
1991
- this.#value = 0;
1992
- this.#text = "0";
2323
+ #onInput;
2324
+ constructor(node, config, fallback) {
2325
+ super();
1993
2326
  this.#node = node;
1994
- this.#nodeProperty = config?.property;
1995
- this.#idleBoxStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.idleBoxStyle, {
1996
- box: {
1997
- color: Theme.Widget.BackgroundColor,
1998
- border: {
1999
- size: Theme.Widget.Border.Size,
2000
- color: Theme.Widget.Border.Color
2001
- },
2002
- radius: Theme.Widget.Border.Radius
2003
- },
2004
- text: { color: Theme.Widget.FontColor }
2005
- }));
2006
- this.#highlightBoxStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.highlightBoxStyle, {
2007
- box: {
2008
- color: Theme.Widget.Hover.BackgroundColor,
2009
- border: {
2010
- size: Theme.Widget.Border.Size,
2011
- color: Theme.Widget.Border.Color
2012
- },
2013
- radius: Theme.Widget.Border.Radius
2014
- },
2015
- text: { color: Theme.Widget.FontColor }
2016
- }));
2017
- this.Set(startingWidgetValue(node, this.#nodeProperty, config?.value, 0));
2327
+ this.#property = config?.property;
2328
+ this.#label = config?.label ?? "";
2329
+ this.#width = config?.width ?? width;
2330
+ this.#value = startingWidgetValue(node, this.#property, config?.value, fallback);
2018
2331
  this.#callback = config?.callback;
2019
- if (this.#nodeProperty !== void 0) {
2020
- this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
2021
- this.Set(newVal);
2022
- });
2332
+ this.#onInput = config?.onInput;
2333
+ if (this.#property !== void 0) {
2334
+ node.setProperty(this.#property, this.#value);
2335
+ node.addPropertyChangeListener(this.#property, (_, next) => this.setValue(next));
2023
2336
  }
2024
2337
  }
2025
2338
  Size() {
2026
- return { "x": width, "y": height };
2339
+ return { x: this.#width, y: height };
2027
2340
  }
2028
- Set(newNumber) {
2029
- if (Object.is(this.#value, newNumber)) {
2341
+ value() {
2342
+ return this.#value;
2343
+ }
2344
+ /**
2345
+ * Shows a new value without treating it as a person's edit: no callback
2346
+ * and no edit event.
2347
+ */
2348
+ setValue(value) {
2349
+ if (this.equal(value, this.#value)) {
2030
2350
  return;
2031
2351
  }
2032
- this.#value = newNumber;
2033
- if (this.#nodeProperty !== void 0) {
2034
- this.#node.setProperty(this.#nodeProperty, this.#value);
2352
+ this.#value = value;
2353
+ this.valueChanged();
2354
+ if (this.#property !== void 0) {
2355
+ this.#node.setProperty(this.#property, value);
2035
2356
  }
2036
- if (this.#callback !== void 0) {
2037
- this.#callback(this.#value);
2357
+ requestRender();
2358
+ }
2359
+ label() {
2360
+ return this.#label;
2361
+ }
2362
+ setLabel(label) {
2363
+ this.#label = label;
2364
+ requestRender();
2365
+ }
2366
+ node() {
2367
+ return this.#node;
2368
+ }
2369
+ input(value) {
2370
+ this.setValue(value);
2371
+ this.#onInput?.(value);
2372
+ }
2373
+ /**
2374
+ * Ends a person's edit. Nothing is reported when they ended where they
2375
+ * started.
2376
+ */
2377
+ commit(value, startedAt) {
2378
+ this.setValue(value);
2379
+ if (this.equal(value, startedAt)) {
2380
+ return;
2038
2381
  }
2039
- this.#text = "" + parseFloat(this.#value.toPrecision(6));
2382
+ if (this.#property !== void 0) {
2383
+ this.#node.editProperty(this.#property, value);
2384
+ }
2385
+ this.#callback?.(value);
2040
2386
  }
2041
- ClickStart() {
2387
+ valueChanged() {
2042
2388
  }
2043
- ClickEnd() {
2044
- let input = null;
2045
- const setOption = "Set";
2046
- const cancelOption = "Cancel";
2047
- const popup = new Popup({
2048
- title: "Set Number",
2049
- options: [setOption, cancelOption],
2050
- content: () => {
2051
- const container = document.createElement("div");
2052
- input = document.createElement("input");
2053
- input.type = "number";
2054
- input.valueAsNumber = this.#value;
2055
- container.append(input);
2056
- return container;
2057
- },
2058
- onClose: (button) => {
2059
- if (button !== setOption || input === null) {
2060
- return;
2061
- }
2062
- this.Set(input.valueAsNumber);
2063
- }
2389
+ equal(a, b) {
2390
+ return Object.is(a, b);
2391
+ }
2392
+ };
2393
+
2394
+ // src/widgets/number.ts
2395
+ var DRAG_THRESHOLD = 3;
2396
+ var NumberWidget = class extends ValueWidget {
2397
+ #step;
2398
+ #integer;
2399
+ #min;
2400
+ #max;
2401
+ #startX = 0;
2402
+ #startValue = 0;
2403
+ #dragging = false;
2404
+ #dragged = false;
2405
+ constructor(node, config) {
2406
+ super(node, config, 0);
2407
+ this.#step = config?.step;
2408
+ this.#integer = config?.integer === true;
2409
+ this.#min = config?.min ?? -Infinity;
2410
+ this.#max = config?.max ?? Infinity;
2411
+ }
2412
+ Cursor() {
2413
+ return "ew-resize" /* EWResize */;
2414
+ }
2415
+ render(ctx, box, scale) {
2416
+ drawField(ctx, box, scale, {
2417
+ input: true,
2418
+ hovered: this.hovered() || this.pressed(),
2419
+ label: this.label(),
2420
+ value: formatNumber(this.value())
2064
2421
  });
2065
- popup.Show();
2066
2422
  }
2067
- Draw(ctx, position, scale, mousePosition) {
2068
- const box = {
2069
- Position: { x: 0, y: 0 },
2070
- Size: {
2071
- x: width * scale,
2072
- y: height * scale
2073
- }
2074
- };
2075
- CopyVector2(box.Position, position);
2076
- let style = this.#idleBoxStyle;
2077
- if (mousePosition !== void 0) {
2078
- if (InBox(box, mousePosition)) {
2079
- style = this.#highlightBoxStyle;
2423
+ onPointerDown(pointer) {
2424
+ this.#startX = pointer.position.x;
2425
+ this.#startValue = this.value();
2426
+ this.#dragging = false;
2427
+ this.#dragged = false;
2428
+ }
2429
+ onPointerMove(pointer) {
2430
+ const dx = pointer.position.x - this.#startX;
2431
+ if (!this.#dragging && Math.abs(dx) < DRAG_THRESHOLD) {
2432
+ return;
2433
+ }
2434
+ this.#dragging = true;
2435
+ this.input(this.#clean(this.#startValue + dx * this.#stepFor(this.#startValue)));
2436
+ }
2437
+ onPointerUp() {
2438
+ this.#dragged = this.#dragging;
2439
+ if (this.#dragging) {
2440
+ this.#dragging = false;
2441
+ this.commit(this.value(), this.#startValue);
2442
+ }
2443
+ }
2444
+ onClick() {
2445
+ if (this.#dragged) {
2446
+ return;
2447
+ }
2448
+ const before = this.value();
2449
+ this.editText(String(before), (text) => {
2450
+ const parsed = Number.parseFloat(text);
2451
+ if (Number.isFinite(parsed)) {
2452
+ this.commit(this.#clean(parsed), before);
2080
2453
  }
2454
+ });
2455
+ }
2456
+ #stepFor(value) {
2457
+ if (this.#step !== void 0) {
2458
+ return this.#step;
2081
2459
  }
2082
- style.DrawUnderline(ctx, box, scale, this.#text);
2083
- return box;
2460
+ return Math.max(Math.abs(value) * 0.01, this.#integer ? 0.1 : 0.01);
2461
+ }
2462
+ #clean(value) {
2463
+ const rounded = this.#integer ? Math.round(value) : value;
2464
+ return Math.min(this.#max, Math.max(this.#min, rounded));
2084
2465
  }
2085
2466
  };
2467
+ function formatNumber(value) {
2468
+ return "" + parseFloat(value.toPrecision(6));
2469
+ }
2086
2470
 
2087
2471
  // src/widgets/button.ts
2088
- var ButtonWidget = class {
2472
+ var ButtonWidget = class extends WidgetBase {
2089
2473
  #text;
2090
- #idleStyle;
2091
- #hoverStyle;
2092
- #clickStyle;
2093
2474
  #callback;
2094
- #gettingClicked;
2475
+ #width;
2095
2476
  constructor(config) {
2096
- this.#text = config?.text === void 0 ? "Button" : config?.text;
2477
+ super();
2478
+ this.#text = config?.text ?? "Button";
2097
2479
  this.#callback = config?.callback;
2098
- this.#gettingClicked = false;
2099
- this.#idleStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.idleStyle, {
2100
- box: {
2101
- color: Theme.Widget.BackgroundColor,
2102
- border: {
2103
- size: Theme.Widget.Border.Size,
2104
- color: Theme.Widget.Border.Color
2105
- }
2106
- },
2107
- text: { color: Theme.Widget.FontColor }
2108
- }));
2109
- this.#hoverStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.hoverStyle, {
2110
- box: {
2111
- color: Theme.Widget.Hover.BackgroundColor,
2112
- border: {
2113
- size: Theme.Widget.Border.Size,
2114
- color: Theme.Widget.Border.Color
2115
- }
2116
- },
2117
- text: { color: Theme.Widget.FontColor }
2118
- }));
2119
- this.#clickStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.clickStyle, {
2120
- box: {
2121
- color: Theme.Widget.Button.Click.BackgroundColor,
2122
- border: {
2123
- size: Theme.Widget.Border.Size,
2124
- color: Theme.Widget.Border.Color
2125
- }
2126
- },
2127
- text: { color: Theme.Widget.FontColor }
2128
- }));
2480
+ this.#width = config?.width ?? width;
2129
2481
  }
2130
2482
  Size() {
2131
- return { "x": width, "y": height };
2483
+ return { x: this.#width, y: height };
2132
2484
  }
2133
- Draw(ctx, position, scale, mousePosition) {
2134
- const box = {
2135
- Position: { x: 0, y: 0 },
2136
- Size: {
2137
- x: width * scale,
2138
- y: height * scale
2139
- }
2140
- };
2141
- CopyVector2(box.Position, position);
2142
- let style = this.#idleStyle;
2143
- if (mousePosition !== void 0 && !this.#gettingClicked) {
2144
- if (InBox(box, mousePosition)) {
2145
- style = this.#hoverStyle;
2146
- }
2147
- }
2148
- if (this.#gettingClicked) {
2149
- style = this.#clickStyle;
2150
- }
2151
- style.Draw(ctx, box, scale, this.#text);
2152
- return box;
2485
+ text() {
2486
+ return this.#text;
2153
2487
  }
2154
- ClickStart() {
2155
- this.#gettingClicked = true;
2488
+ setText(text) {
2489
+ this.#text = text;
2490
+ requestRender();
2156
2491
  }
2157
- ClickEnd() {
2158
- this.#gettingClicked = false;
2159
- if (this.#callback !== void 0) {
2160
- this.#callback();
2161
- }
2492
+ render(ctx, box, scale) {
2493
+ drawField(ctx, box, scale, {
2494
+ hovered: this.hovered(),
2495
+ pressed: this.pressed(),
2496
+ value: this.#text
2497
+ });
2498
+ }
2499
+ onClick() {
2500
+ this.#callback?.();
2162
2501
  }
2163
2502
  };
2164
2503
 
@@ -2173,500 +2512,208 @@ function contrastColor(color) {
2173
2512
  }
2174
2513
  return "white";
2175
2514
  }
2176
- var ColorWidget = class {
2177
- #node;
2178
- #nodeProperty;
2179
- #value;
2515
+ var picker = null;
2516
+ function sharedPicker() {
2517
+ if (picker === null || !picker.isConnected) {
2518
+ picker = document.createElement("input");
2519
+ picker.type = "color";
2520
+ document.body.appendChild(picker);
2521
+ }
2522
+ return picker;
2523
+ }
2524
+ var ColorWidget = class extends ValueWidget {
2180
2525
  #contrast;
2181
- #textBoxStyle;
2182
- #callback;
2183
2526
  constructor(node, config) {
2184
- this.#node = node;
2185
- this.#nodeProperty = config?.property;
2186
- this.#callback = config?.callback;
2187
- this.#value = startingWidgetValue(node, this.#nodeProperty, config?.value, "#000000");
2188
- this.#contrast = contrastColor(this.#value);
2189
- this.#textBoxStyle = new TextBoxStyle({
2190
- box: {
2191
- color: this.#value,
2192
- border: {
2193
- color: this.#contrast,
2194
- size: Theme.Widget.Border.Size
2195
- }
2196
- },
2197
- text: config?.textStyle
2527
+ super(node, config, "#000000");
2528
+ this.#contrast = contrastColor(this.value());
2529
+ }
2530
+ valueChanged() {
2531
+ this.#contrast = contrastColor(this.value());
2532
+ }
2533
+ render(ctx, box, scale) {
2534
+ drawField(ctx, box, scale, {
2535
+ input: true,
2536
+ hovered: this.hovered(),
2537
+ label: this.label(),
2538
+ value: this.value(),
2539
+ background: this.value(),
2540
+ border: this.#contrast,
2541
+ textColor: this.#contrast
2198
2542
  });
2199
- if (this.#nodeProperty !== void 0) {
2200
- this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
2201
- this.Set(newVal);
2202
- });
2203
- }
2204
2543
  }
2205
- Size() {
2206
- return { "x": width, "y": height };
2207
- }
2208
- Set(value) {
2209
- if (this.#value === value) {
2544
+ onClick() {
2545
+ const canvas = this.canvas();
2546
+ if (canvas === null) {
2210
2547
  return;
2211
2548
  }
2212
- this.#value = value;
2213
- if (this.#callback !== void 0) {
2214
- this.#callback(this.#value);
2215
- }
2216
- if (this.#nodeProperty !== void 0) {
2217
- this.#node.setProperty(this.#nodeProperty, this.#value);
2218
- }
2219
- this.#contrast = contrastColor(this.#value);
2220
- this.#textBoxStyle.setBoxColor(this.#value);
2221
- this.#textBoxStyle.setBorderColor(this.#contrast);
2222
- this.#textBoxStyle.setTextColor(this.#contrast);
2223
- }
2224
- ClickStart() {
2225
- }
2226
- ClickEnd() {
2227
- let input = null;
2228
- const popup = new Popup({
2229
- title: "Set Color",
2230
- options: ["Set", "Cancel"],
2231
- content: () => {
2232
- const container = document.createElement("div");
2233
- input = document.createElement("input");
2234
- input.type = "color";
2235
- input.value = this.#value;
2236
- input.name = "color";
2237
- container.append(input);
2238
- const label = document.createElement("label");
2239
- label.htmlFor = "color";
2240
- label.textContent = "Color";
2241
- container.append(label);
2242
- return container;
2243
- },
2244
- onClose: (button) => {
2245
- if (button !== "Set" || input === null) {
2246
- return;
2247
- }
2248
- this.Set(input.value);
2249
- }
2250
- });
2251
- popup.Show();
2252
- }
2253
- Draw(ctx, position, scale, mousePosition) {
2254
- const box = {
2255
- Position: { x: 0, y: 0 },
2256
- Size: {
2257
- x: width * scale,
2258
- y: height * scale
2259
- }
2260
- };
2261
- CopyVector2(box.Position, position);
2262
- this.#textBoxStyle.Draw(ctx, box, scale, this.#value);
2263
- return box;
2549
+ const before = this.value();
2550
+ const alpha = before.slice(7);
2551
+ const box = this.box();
2552
+ const rect = canvas.getBoundingClientRect();
2553
+ const sx = canvas.width > 0 ? rect.width / canvas.width : 1;
2554
+ const sy = canvas.height > 0 ? rect.height / canvas.height : 1;
2555
+ const input = sharedPicker();
2556
+ input.style.cssText = [
2557
+ "position: fixed",
2558
+ `left: ${rect.left + box.Position.x * sx}px`,
2559
+ `top: ${rect.top + box.Position.y * sy}px`,
2560
+ `width: ${box.Size.x * sx}px`,
2561
+ `height: ${box.Size.y * sy}px`,
2562
+ "opacity: 0",
2563
+ "pointer-events: none",
2564
+ "border: 0",
2565
+ "padding: 0"
2566
+ ].join(";");
2567
+ input.value = before.slice(0, 7);
2568
+ input.oninput = () => this.input(input.value + alpha);
2569
+ input.onchange = () => this.commit(input.value + alpha, before);
2570
+ input.getBoundingClientRect();
2571
+ input.click();
2264
2572
  }
2265
2573
  };
2266
2574
 
2267
2575
  // src/widgets/slider.ts
2268
- var SliderWidget = class {
2269
- // Data Variables =========================================================
2576
+ var SliderWidget = class extends ValueWidget {
2270
2577
  #min;
2271
2578
  #max;
2272
- #value;
2273
- #text;
2274
2579
  #snapIncrement;
2275
- #node;
2276
- #nodeProperty;
2277
- // Callbacks ==============================================================
2278
- #change;
2279
- #release;
2280
- // Styling Variables ======================================================
2281
- #textStyle;
2282
- #backgroundColor;
2283
- #borderColor;
2284
2580
  #fillColor;
2285
- // Runtime Variables ======================================================
2286
- #lastMousePosition;
2287
- #clickStartMousePosition;
2288
- #clicking;
2581
+ #startValue = 0;
2289
2582
  constructor(node, config) {
2290
- this.#node = node;
2291
- this.#value = 0;
2292
- this.#min = config?.min === void 0 ? 0 : config?.min;
2293
- this.#max = config?.max === void 0 ? 1 : config?.max;
2583
+ const min = config?.min ?? 0;
2584
+ const max = config?.max ?? 1;
2585
+ super(node, { ...config, value: config?.value === void 0 ? void 0 : Clamp(config.value, min, max) }, min);
2586
+ this.#min = min;
2587
+ this.#max = max;
2294
2588
  this.#snapIncrement = config?.snapIncrement;
2295
- this.#nodeProperty = config?.property;
2296
- this.#text = "";
2297
- this.#release = config?.release;
2298
- this.#backgroundColor = config?.backgroundColor === void 0 ? Theme.Widget.BackgroundColor : config?.backgroundColor;
2299
- this.#fillColor = config?.fillColor === void 0 ? Theme.Widget.Slider.FillColor : config?.fillColor;
2300
- this.#borderColor = config?.borderColor === void 0 ? Theme.Widget.Border.Color : config?.borderColor;
2301
- this.#textStyle = new TextStyle(TextStyleFallback(config?.textStyle, {
2302
- color: Theme.Widget.FontColor
2303
- }));
2304
- this.#lastMousePosition = Zero();
2305
- this.#clickStartMousePosition = Zero();
2306
- this.#clicking = false;
2307
- if (config?.property !== void 0 && config?.property !== null) {
2308
- node.addPropertyChangeListener(config.property, (oldVal, newVal) => {
2309
- this.SetValue(newVal);
2310
- });
2311
- }
2312
- this.SetValue(startingWidgetValue(node, this.#nodeProperty, config?.value, 0));
2313
- this.#change = config?.change;
2314
- }
2315
- SetValue(newValue) {
2316
- const cleanedValue = Clamp(newValue, this.#min, this.#max);
2317
- if (Object.is(this.#value, cleanedValue)) {
2318
- return;
2319
- }
2320
- this.#value = cleanedValue;
2321
- if (this.#nodeProperty) {
2322
- this.#node.setProperty(this.#nodeProperty, this.#value);
2323
- }
2324
- this.#text = this.#value.toFixed(3);
2325
- if (this.#change !== void 0) {
2326
- this.#change(this.#value);
2327
- }
2328
- }
2329
- Size() {
2330
- return { "x": width, "y": height };
2589
+ this.#fillColor = config?.fillColor;
2331
2590
  }
2332
- ClickStart() {
2333
- this.#clicking = true;
2334
- CopyVector2(this.#clickStartMousePosition, this.#lastMousePosition);
2591
+ Cursor() {
2592
+ return "ew-resize" /* EWResize */;
2335
2593
  }
2336
- ClickEnd() {
2337
- this.#clicking = false;
2338
- if (this.#release !== void 0) {
2339
- this.#release(this.#value);
2340
- }
2594
+ setValue(value) {
2595
+ super.setValue(Clamp(value, this.#min, this.#max));
2341
2596
  }
2342
- Draw(ctx, position, scale, mousePosition) {
2343
- const scaledWidth = width * scale;
2344
- const scaledHeight = height * scale;
2345
- const scaledBorderThickness = Theme.Widget.Border.Size * scale;
2346
- ctx.fillStyle = this.#backgroundColor;
2347
- ctx.strokeStyle = this.#borderColor;
2348
- ctx.lineWidth = scaledBorderThickness;
2597
+ render(ctx, box, scale) {
2598
+ drawFieldBox(ctx, box, scale, { input: true, hovered: this.hovered() || this.pressed(), value: "" });
2599
+ const border = Theme.Widget.Border.Size * scale;
2600
+ const fill = Clamp01((this.value() - this.#min) / (this.#max - this.#min));
2601
+ ctx.fillStyle = this.#fillColor ?? Theme.Widget.Slider.FillColor;
2349
2602
  ctx.beginPath();
2350
2603
  ctx.roundRect(
2351
- position.x,
2352
- position.y,
2353
- scaledWidth,
2354
- scaledHeight,
2604
+ box.Position.x + border / 2,
2605
+ box.Position.y + border / 2,
2606
+ Math.max(0, box.Size.x * fill - border),
2607
+ box.Size.y - border,
2355
2608
  Theme.Widget.Border.Radius * scale
2356
2609
  );
2357
2610
  ctx.fill();
2358
- ctx.stroke();
2359
- const fill = Clamp01((this.#value - this.#min) / (this.#max - this.#min));
2360
- ctx.fillStyle = this.#fillColor;
2361
- ctx.beginPath();
2362
- ctx.roundRect(
2363
- position.x + scaledBorderThickness / 2,
2364
- position.y + scaledBorderThickness / 2,
2365
- scaledWidth * fill - scaledBorderThickness,
2366
- scaledHeight - scaledBorderThickness,
2367
- Theme.Widget.Border.Radius * scale
2368
- );
2369
- ctx.fill();
2370
- ctx.textAlign = "center" /* Center */;
2371
- ctx.textBaseline = "middle" /* Middle */;
2372
- this.#textStyle.setupStyle(ctx, scale);
2373
- ctx.fillText(
2374
- this.#text,
2375
- position.x + scaledWidth / 2,
2376
- position.y + scaledHeight / 2
2377
- );
2378
- if (mousePosition !== void 0) {
2379
- CopyVector2(this.#lastMousePosition, mousePosition);
2380
- if (this.#clicking) {
2381
- const min = position.x + scaledBorderThickness / 2;
2382
- const max = min + scaledWidth - scaledBorderThickness;
2383
- const p = Clamp01((this.#lastMousePosition.x - min) / (max - min));
2384
- let value = p * (this.#max - this.#min) + this.#min;
2385
- if (this.#snapIncrement !== void 0) {
2386
- value = Math.round(value / this.#snapIncrement) * this.#snapIncrement;
2387
- }
2388
- this.SetValue(value);
2389
- }
2611
+ drawFieldText(ctx, box, scale, {
2612
+ input: true,
2613
+ hovered: false,
2614
+ label: this.label(),
2615
+ value: formatNumber(Number(this.value().toFixed(3)))
2616
+ });
2617
+ }
2618
+ onPointerDown(pointer) {
2619
+ this.#startValue = this.value();
2620
+ this.input(this.#valueAt(pointer.position.x));
2621
+ }
2622
+ onPointerMove(pointer) {
2623
+ this.input(this.#valueAt(pointer.position.x));
2624
+ }
2625
+ onPointerUp() {
2626
+ this.commit(this.value(), this.#startValue);
2627
+ }
2628
+ #valueAt(x) {
2629
+ const box = this.box();
2630
+ const border = Theme.Widget.Border.Size * this.scale();
2631
+ const min = box.Position.x + border / 2;
2632
+ const max = min + box.Size.x - border;
2633
+ const p = Clamp01((x - min) / (max - min));
2634
+ let value = p * (this.#max - this.#min) + this.#min;
2635
+ if (this.#snapIncrement !== void 0) {
2636
+ value = Math.round(value / this.#snapIncrement) * this.#snapIncrement;
2390
2637
  }
2391
- return {
2392
- Position: { x: position.x, y: position.y },
2393
- Size: {
2394
- x: scaledWidth,
2395
- y: scaledHeight
2396
- }
2397
- };
2638
+ return Clamp(value, this.#min, this.#max);
2398
2639
  }
2399
2640
  };
2400
2641
 
2401
- // src/popups/string.ts
2402
- function SetStringPopup(config) {
2403
- let input = null;
2404
- return new Popup({
2405
- title: config.title,
2406
- options: ["Set", "Cancel"],
2407
- content: () => {
2408
- const container = document.createElement("div");
2409
- input = document.createElement("input");
2410
- input.value = config.startingValue;
2411
- container.append(input);
2412
- return container;
2413
- },
2414
- onClose: (button) => {
2415
- if (button !== "Set" || input === null) {
2416
- if (config.onCancel) {
2417
- config.onCancel();
2418
- }
2419
- return;
2420
- }
2421
- config.onUpdate(input.value);
2422
- }
2423
- });
2424
- }
2425
-
2426
2642
  // src/widgets/string.ts
2427
- var StringWidget = class {
2428
- #value;
2429
- #idleStyle;
2430
- #hoverStyle;
2431
- #callback;
2432
- #node;
2433
- #nodeProperty;
2643
+ var StringWidget = class extends ValueWidget {
2434
2644
  constructor(node, config) {
2435
- this.#value = "";
2436
- this.#node = node;
2437
- this.#nodeProperty = config?.property;
2438
- this.#idleStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.textBoxStyle, {
2439
- box: {
2440
- color: Theme.Widget.BackgroundColor,
2441
- border: {
2442
- size: Theme.Widget.Border.Size,
2443
- color: Theme.Widget.Border.Color
2444
- },
2445
- radius: Theme.Widget.Border.Radius
2446
- },
2447
- text: { color: Theme.Widget.FontColor }
2448
- }));
2449
- this.#hoverStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.textBoxStyle, {
2450
- box: {
2451
- color: Theme.Widget.Hover.BackgroundColor,
2452
- border: {
2453
- size: Theme.Widget.Border.Size,
2454
- color: Theme.Widget.Border.Color
2455
- },
2456
- radius: Theme.Widget.Border.Radius
2457
- },
2458
- text: { color: Theme.Widget.FontColor }
2459
- }));
2460
- this.Set(startingWidgetValue(node, this.#nodeProperty, config?.value, ""));
2461
- this.#callback = config?.callback;
2462
- if (this.#nodeProperty !== void 0) {
2463
- this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
2464
- this.Set(newVal);
2465
- });
2466
- }
2645
+ super(node, config, "");
2467
2646
  }
2468
- Size() {
2469
- return { "x": width, "y": height };
2647
+ Cursor() {
2648
+ return "text" /* Text */;
2470
2649
  }
2471
- Set(value) {
2472
- if (this.#value === value) {
2473
- return;
2474
- }
2475
- this.#value = value;
2476
- if (this.#nodeProperty !== void 0) {
2477
- this.#node.setProperty(this.#nodeProperty, this.#value);
2478
- }
2479
- if (this.#callback !== void 0) {
2480
- this.#callback(this.#value);
2481
- }
2482
- }
2483
- ClickStart() {
2484
- }
2485
- ClickEnd() {
2486
- const popup = SetStringPopup({
2487
- title: "Set String",
2488
- startingValue: this.#value,
2489
- onUpdate: (value) => {
2490
- this.Set(value);
2491
- }
2650
+ render(ctx, box, scale) {
2651
+ drawField(ctx, box, scale, {
2652
+ input: true,
2653
+ hovered: this.hovered(),
2654
+ label: this.label(),
2655
+ value: this.value()
2492
2656
  });
2493
- popup.Show();
2494
2657
  }
2495
- Draw(ctx, position, scale, mousePosition) {
2496
- const box = {
2497
- Position: { x: 0, y: 0 },
2498
- Size: {
2499
- x: width * scale,
2500
- y: height * scale
2501
- }
2502
- };
2503
- CopyVector2(box.Position, position);
2504
- let style = this.#idleStyle;
2505
- if (mousePosition !== void 0) {
2506
- if (InBox(box, mousePosition)) {
2507
- style = this.#hoverStyle;
2508
- }
2509
- }
2510
- style.DrawUnderline(ctx, box, scale, fitString(ctx, this.#value, box.Size.x - 20 * scale));
2511
- return box;
2658
+ onClick() {
2659
+ const before = this.value();
2660
+ this.editText(before, (text) => this.commit(text, before));
2512
2661
  }
2513
2662
  };
2514
2663
 
2515
2664
  // src/widgets/toggle.ts
2516
- var ToggleStyle = class {
2517
- #idleStyle;
2518
- #hoverStyle;
2519
- #lightColor;
2520
- #lightBorderColor;
2521
- #lightBlur;
2522
- #box;
2523
- constructor(config) {
2524
- this.#idleStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.idle, {
2525
- box: {
2526
- color: Theme.Widget.BackgroundColor,
2527
- border: {
2528
- size: Theme.Widget.Border.Size,
2529
- color: Theme.Widget.Border.Color
2530
- }
2531
- },
2532
- text: { color: Theme.Widget.FontColor }
2533
- }));
2534
- this.#hoverStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.hover, {
2535
- box: {
2536
- color: Theme.Widget.Hover.BackgroundColor,
2537
- border: {
2538
- size: Theme.Widget.Border.Size,
2539
- color: Theme.Widget.Border.Color
2540
- }
2541
- },
2542
- text: { color: Theme.Widget.FontColor }
2543
- }));
2544
- this.#lightColor = config?.lightColor === void 0 ? "#222222" : config.lightColor;
2545
- this.#lightBorderColor = config?.lightBorderColor === void 0 ? "black" : config.lightBorderColor;
2546
- this.#lightBlur = config?.lightBlur;
2547
- this.#box = { Position: { x: 0, y: 0 }, Size: { x: 0, y: 0 } };
2548
- }
2549
- Draw(ctx, pos, scale, text, mousePosition) {
2550
- const scaledWidth = width * scale;
2551
- const scaledHeight = height * scale;
2552
- this.#box.Size.x = scaledWidth;
2553
- this.#box.Size.y = scaledHeight;
2554
- CopyVector2(this.#box.Position, pos);
2555
- let style = this.#idleStyle;
2556
- if (mousePosition !== void 0) {
2557
- if (InBox(this.#box, mousePosition)) {
2558
- style = this.#hoverStyle;
2559
- }
2560
- }
2561
- style.Draw(ctx, this.#box, scale, text);
2562
- const lightScale = Math.min(scaledWidth, scaledHeight);
2563
- ctx.strokeStyle = this.#lightBorderColor;
2564
- ctx.fillStyle = this.#lightColor;
2565
- ctx.beginPath();
2566
- ctx.roundRect(
2567
- pos.x + lightScale * 0.2,
2568
- pos.y + lightScale * 0.2,
2569
- lightScale * 0.6,
2570
- lightScale * 0.6,
2571
- Theme.Widget.Border.Radius * scale
2572
- );
2573
- if (this.#lightBlur) {
2574
- ctx.shadowBlur = this.#lightBlur * scale;
2575
- ctx.shadowColor = this.#lightColor;
2576
- }
2577
- ctx.fill();
2578
- if (this.#lightBlur) {
2579
- ctx.shadowBlur = 0;
2580
- }
2581
- return this.#box;
2582
- }
2583
- };
2584
- var ToggleWidget = class {
2585
- #node;
2586
- #nodeProperty;
2587
- #value;
2588
- #text;
2589
- #enabledStyle;
2590
- #disabledStyle;
2591
- #callback;
2665
+ var ToggleWidget = class extends ValueWidget {
2666
+ #onColor;
2667
+ #offColor;
2592
2668
  constructor(node, config) {
2593
- this.#value = false;
2594
- this.#node = node;
2595
- this.#nodeProperty = config?.property;
2596
- this.#text = config?.text === void 0 ? "Toggle" : config?.text;
2597
- this.#enabledStyle = new ToggleStyle({
2598
- idle: TextBoxStyleWithFallback(config?.enabledStyle?.idle, {
2599
- box: {
2600
- color: Theme.Widget.BackgroundColor,
2601
- border: {
2602
- size: Theme.Widget.Border.Size,
2603
- color: Theme.Widget.Border.Color
2604
- }
2605
- },
2606
- text: { color: Theme.Widget.FontColor }
2607
- }),
2608
- lightBorderColor: config?.enabledStyle?.lightBorderColor,
2609
- lightColor: config?.enabledStyle?.lightColor === void 0 ? "#00FF00" : config?.enabledStyle?.lightColor,
2610
- lightBlur: config?.enabledStyle?.lightBlur === void 0 ? 15 : config?.enabledStyle?.lightBlur
2611
- });
2612
- this.#disabledStyle = new ToggleStyle({
2613
- idle: TextBoxStyleWithFallback(config?.disabledStyle?.idle, {
2614
- box: {
2615
- color: Theme.Widget.BackgroundColor,
2616
- border: {
2617
- size: Theme.Widget.Border.Size,
2618
- color: Theme.Widget.Border.Color
2619
- }
2620
- },
2621
- text: { color: Theme.Widget.FontColor }
2622
- }),
2623
- lightBorderColor: config?.disabledStyle?.lightBorderColor,
2624
- lightColor: config?.disabledStyle?.lightColor === void 0 ? "#004400" : config?.enabledStyle?.lightColor
2625
- });
2626
- this.Set(startingWidgetValue(node, this.#nodeProperty, config?.value, false));
2627
- this.#callback = config?.callback;
2628
- if (this.#nodeProperty !== void 0) {
2629
- this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
2630
- this.Set(newVal);
2631
- });
2632
- }
2633
- }
2634
- Size() {
2635
- return { "x": width, "y": height };
2636
- }
2637
- Draw(ctx, position, scale, mousePosition) {
2638
- let style = this.#value ? this.#enabledStyle : this.#disabledStyle;
2639
- return style.Draw(ctx, position, scale, this.#text, mousePosition);
2669
+ super(node, { ...config, label: config?.label ?? "Toggle" }, false);
2670
+ this.#onColor = config?.onColor ?? "#00FF00";
2671
+ this.#offColor = config?.offColor ?? "#004400";
2640
2672
  }
2641
2673
  Toggle() {
2642
- this.Set(!this.#value);
2643
- }
2644
- Set(value) {
2645
- if (this.#value === value) {
2646
- return;
2647
- }
2648
- this.#value = value;
2649
- if (this.#nodeProperty !== void 0) {
2650
- this.#node.setProperty(this.#nodeProperty, this.#value);
2651
- }
2652
- if (this.#callback !== void 0) {
2653
- this.#callback(this.#value);
2674
+ const before = this.value();
2675
+ this.commit(!before, before);
2676
+ }
2677
+ render(ctx, box, scale) {
2678
+ drawFieldBox(ctx, box, scale, { hovered: this.hovered(), value: "" });
2679
+ const light = Math.min(box.Size.x, box.Size.y) * 0.6;
2680
+ const inset = (box.Size.y - light) / 2;
2681
+ const on = this.value();
2682
+ ctx.fillStyle = on ? this.#onColor : this.#offColor;
2683
+ ctx.beginPath();
2684
+ ctx.roundRect(box.Position.x + inset, box.Position.y + inset, light, light, Theme.Widget.Border.Radius * scale);
2685
+ if (on) {
2686
+ ctx.shadowBlur = 15 * scale;
2687
+ ctx.shadowColor = this.#onColor;
2654
2688
  }
2689
+ ctx.fill();
2690
+ ctx.shadowBlur = 0;
2691
+ const textLeft = box.Position.x + inset * 2 + light;
2692
+ ctx.font = widgetFont(scale);
2693
+ ctx.textAlign = "center";
2694
+ ctx.textBaseline = "middle";
2695
+ ctx.fillStyle = Theme.Widget.FontColor;
2696
+ const room = box.Position.x + box.Size.x - textLeft - inset;
2697
+ ctx.fillText(
2698
+ fitString(ctx, this.label(), room),
2699
+ textLeft + room / 2,
2700
+ box.Position.y + box.Size.y / 2
2701
+ );
2655
2702
  }
2656
- ClickStart() {
2703
+ onClick() {
2657
2704
  this.Toggle();
2658
2705
  }
2659
- ClickEnd() {
2660
- }
2661
2706
  };
2662
2707
 
2663
2708
  // src/widgets/image.ts
2664
- var ImageWidget = class {
2709
+ var ImageWidget = class extends WidgetBase {
2665
2710
  #url;
2711
+ #ownedUrl;
2666
2712
  #image;
2667
2713
  #maxWidth;
2668
2714
  #maxHeight;
2669
2715
  constructor(config) {
2716
+ super();
2670
2717
  this.#maxWidth = config?.maxWidth === void 0 ? 150 : config?.maxWidth;
2671
2718
  this.#maxHeight = config?.maxHeight === void 0 ? 150 : config?.maxHeight;
2672
2719
  if (config?.image) {
@@ -2676,16 +2723,23 @@ var ImageWidget = class {
2676
2723
  }
2677
2724
  }
2678
2725
  SetBlob(blob) {
2679
- const urlCreator = window.URL || window.webkitURL;
2680
- const imageUrl = urlCreator.createObjectURL(blob);
2681
- this.SetUrl(imageUrl);
2726
+ const url = URL.createObjectURL(blob);
2727
+ this.SetUrl(url);
2728
+ this.#ownedUrl = url;
2682
2729
  }
2683
2730
  SetUrl(url) {
2731
+ if (this.#ownedUrl !== void 0) {
2732
+ URL.revokeObjectURL(this.#ownedUrl);
2733
+ this.#ownedUrl = void 0;
2734
+ }
2684
2735
  this.#image = void 0;
2685
2736
  this.#url = url;
2686
2737
  const img = document.createElement("img");
2687
2738
  img.src = url;
2688
2739
  img.onload = () => {
2740
+ if (this.#url !== url) {
2741
+ return;
2742
+ }
2689
2743
  this.#image = img;
2690
2744
  requestRender();
2691
2745
  };
@@ -2693,6 +2747,9 @@ var ImageWidget = class {
2693
2747
  console.log("error loading image:", url, event);
2694
2748
  };
2695
2749
  }
2750
+ Cursor() {
2751
+ return "default" /* Default */;
2752
+ }
2696
2753
  Size() {
2697
2754
  if (this.#image === void 0) {
2698
2755
  return { "x": 0, "y": 0 };
@@ -2712,88 +2769,178 @@ var ImageWidget = class {
2712
2769
  "y": adjust * this.#image.height
2713
2770
  };
2714
2771
  }
2715
- ClickStart() {
2716
- }
2717
- ClickEnd() {
2718
- }
2719
- Draw(ctx, position, scale, mousePosition) {
2720
- const size = this.Size();
2721
- const box = {
2722
- Position: { x: 0, y: 0 },
2723
- Size: {
2724
- x: size.x * scale,
2725
- y: size.y * scale
2726
- }
2727
- };
2728
- CopyVector2(box.Position, position);
2772
+ render(ctx, box, scale) {
2729
2773
  if (!this.#image) {
2730
- return box;
2774
+ return;
2731
2775
  }
2776
+ const size = this.Size();
2777
+ const w = size.x * scale;
2778
+ const h = size.y * scale;
2732
2779
  ctx.drawImage(
2733
2780
  this.#image,
2734
- position.x,
2735
- position.y,
2736
- box.Size.x,
2737
- box.Size.y
2781
+ box.Position.x + (box.Size.x - w) / 2,
2782
+ box.Position.y,
2783
+ w,
2784
+ h
2738
2785
  );
2739
- return box;
2740
2786
  }
2741
2787
  };
2742
2788
 
2743
2789
  // src/widgets/text.ts
2744
- var TextWidget = class {
2790
+ var TextWidget = class extends WidgetBase {
2745
2791
  #value;
2746
- #callback;
2747
- #node;
2748
- #nodeProperty;
2749
2792
  #size;
2793
+ #styleConfig;
2794
+ #styledForTheme = -1;
2795
+ #followTheme() {
2796
+ if (this.#styledForTheme === themeVersion()) {
2797
+ return;
2798
+ }
2799
+ this.#styledForTheme = themeVersion();
2800
+ const style = this.#value.style();
2801
+ style.setColor(this.#styleConfig?.color ?? Theme.Widget.FontColor);
2802
+ style.setSize(this.#styleConfig?.size ?? Theme.Note.FontSize);
2803
+ }
2750
2804
  constructor(node, config) {
2805
+ super();
2751
2806
  this.#size = Zero();
2752
- this.#node = node;
2753
- this.#nodeProperty = config?.property;
2754
- this.#value = new Text("", TextStyleFallback(config?.textBoxStyle, {
2755
- font: Theme.FontFamily,
2756
- color: Theme.Widget.FontColor,
2757
- size: Theme.Note.FontSize
2758
- }));
2759
- this.Set(config?.value === void 0 ? "" : config?.value);
2760
- this.#callback = config?.callback;
2761
- if (this.#nodeProperty !== void 0) {
2762
- this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
2763
- this.Set(newVal);
2764
- });
2807
+ this.#styleConfig = config?.textBoxStyle;
2808
+ this.#value = new Text("", TextStyleFallback(config?.textBoxStyle, {}));
2809
+ this.#followTheme();
2810
+ this.setText(config?.value ?? "");
2811
+ const property = config?.property;
2812
+ if (property !== void 0) {
2813
+ const existing = node.getProperty(property);
2814
+ if (existing !== void 0 && existing !== null) {
2815
+ this.setText(String(existing));
2816
+ }
2817
+ node.addPropertyChangeListener(property, (_, next) => this.setText(String(next)));
2765
2818
  }
2766
2819
  }
2820
+ Cursor() {
2821
+ return "default" /* Default */;
2822
+ }
2767
2823
  Size() {
2768
2824
  return this.#size;
2769
2825
  }
2770
- Set(value) {
2826
+ text() {
2827
+ return this.#value.get();
2828
+ }
2829
+ setText(value) {
2771
2830
  if (this.#value.get() === value) {
2772
2831
  return;
2773
2832
  }
2774
2833
  this.#value.set(value);
2775
- if (this.#nodeProperty !== void 0) {
2776
- this.#node.setProperty(this.#nodeProperty, value);
2834
+ requestRender();
2835
+ }
2836
+ render(ctx, box, scale) {
2837
+ this.#followTheme();
2838
+ this.#value.size(ctx, 1, this.#size);
2839
+ ctx.textAlign = "left";
2840
+ this.#value.render(ctx, scale, box.Position);
2841
+ }
2842
+ };
2843
+
2844
+ // src/widgets/row.ts
2845
+ var RowWidget = class extends WidgetBase {
2846
+ #children;
2847
+ #grow;
2848
+ #gap;
2849
+ #boxes = [];
2850
+ #captured = null;
2851
+ #capturedBox = null;
2852
+ #mouse;
2853
+ constructor(children, config) {
2854
+ super();
2855
+ this.#children = children;
2856
+ this.#grow = config?.grow ?? children.map(() => 1);
2857
+ this.#gap = config?.gap ?? Theme.Widget.Gap;
2858
+ }
2859
+ children() {
2860
+ return this.#children;
2861
+ }
2862
+ Size() {
2863
+ const shown = this.#shown();
2864
+ let x = 0;
2865
+ let y = 0;
2866
+ for (const child of shown) {
2867
+ const size = child.Size();
2868
+ x += size.x;
2869
+ y = Math.max(y, size.y);
2870
+ }
2871
+ x += Math.max(0, shown.length - 1) * this.#gap;
2872
+ return { x, y };
2873
+ }
2874
+ Cursor() {
2875
+ const over = this.#captured ?? this.#childAt(this.#mouse);
2876
+ return over?.Cursor?.() ?? "default" /* Default */;
2877
+ }
2878
+ Draw(ctx, box, scale, mousePosition) {
2879
+ this.#mouse = mousePosition;
2880
+ super.Draw(ctx, box, scale, mousePosition);
2881
+ }
2882
+ render(ctx, box, scale) {
2883
+ const shown = this.#shown();
2884
+ const gap = this.#gap * scale;
2885
+ const natural = shown.reduce((sum, child) => sum + child.Size().x * scale, 0);
2886
+ const extra = Math.max(0, box.Size.x - natural - gap * Math.max(0, shown.length - 1));
2887
+ const totalGrow = shown.reduce((sum, child) => sum + this.#growOf(child), 0);
2888
+ this.#boxes = this.#children.map(() => null);
2889
+ let x = box.Position.x;
2890
+ for (const child of shown) {
2891
+ const size = child.Size();
2892
+ const share = totalGrow > 0 ? extra * (this.#growOf(child) / totalGrow) : 0;
2893
+ const childBox = {
2894
+ Position: { x, y: box.Position.y + (box.Size.y - size.y * scale) / 2 },
2895
+ Size: { x: size.x * scale + share, y: size.y * scale }
2896
+ };
2897
+ this.#boxes[this.#children.indexOf(child)] = childBox;
2898
+ child.Draw(ctx, childBox, scale, this.#captured === null || this.#captured === child ? this.#mouse : void 0);
2899
+ x += childBox.Size.x + gap;
2777
2900
  }
2778
- if (this.#callback !== void 0) {
2779
- this.#callback(value);
2901
+ }
2902
+ onPointerDown(pointer) {
2903
+ const child = this.#childAt(pointer.position);
2904
+ if (child === null || child.Disabled?.()) {
2905
+ return;
2780
2906
  }
2907
+ this.#captured = child;
2908
+ this.#capturedBox = this.#boxes[this.#children.indexOf(child)];
2909
+ child.PointerDown?.(this.#forChild(pointer));
2781
2910
  }
2782
- ClickStart() {
2911
+ onPointerMove(pointer) {
2912
+ this.#captured?.PointerMove?.(this.#forChild(pointer));
2783
2913
  }
2784
- ClickEnd() {
2914
+ onPointerUp(pointer) {
2915
+ const child = this.#captured;
2916
+ const forChild = this.#forChild(pointer);
2917
+ this.#captured = null;
2918
+ this.#capturedBox = null;
2919
+ child?.PointerUp?.(forChild);
2785
2920
  }
2786
- Draw(ctx, position, scale, mousePosition) {
2787
- this.#value.size(ctx, scale, this.#size);
2788
- const box = {
2789
- Position: { x: 0, y: 0 },
2790
- Size: this.#size
2921
+ #forChild(pointer) {
2922
+ return {
2923
+ position: pointer.position,
2924
+ inside: this.#capturedBox !== null && InBox(this.#capturedBox, pointer.position)
2791
2925
  };
2792
- CopyVector2(box.Position, position);
2793
- this.#value.size(ctx, 1, this.#size);
2794
- ctx.textAlign = "left";
2795
- this.#value.render(ctx, scale, box.Position);
2796
- return box;
2926
+ }
2927
+ #childAt(position) {
2928
+ if (position === void 0) {
2929
+ return null;
2930
+ }
2931
+ for (let i = 0; i < this.#children.length; i++) {
2932
+ const box = this.#boxes[i];
2933
+ if (box && InBox(box, position)) {
2934
+ return this.#children[i];
2935
+ }
2936
+ }
2937
+ return null;
2938
+ }
2939
+ #shown() {
2940
+ return this.#children.filter((child) => !child.Hidden?.());
2941
+ }
2942
+ #growOf(child) {
2943
+ return this.#grow[this.#children.indexOf(child)] ?? 1;
2797
2944
  }
2798
2945
  };
2799
2946
 
@@ -2813,6 +2960,9 @@ var WidgetFactory = class {
2813
2960
  }
2814
2961
  return builder(node, config);
2815
2962
  }
2963
+ createFromConfig(node, config) {
2964
+ return this.create(node, config.type, config.config);
2965
+ }
2816
2966
  };
2817
2967
  var GlobalWidgetFactory = new WidgetFactory();
2818
2968
  GlobalWidgetFactory.register("button", (node, config) => new ButtonWidget(config));
@@ -2823,6 +2973,10 @@ GlobalWidgetFactory.register("string", (node, config) => new StringWidget(node,
2823
2973
  GlobalWidgetFactory.register("toggle", (node, config) => new ToggleWidget(node, config));
2824
2974
  GlobalWidgetFactory.register("image", (node, config) => new ImageWidget(config));
2825
2975
  GlobalWidgetFactory.register("text", (node, config) => new TextWidget(node, config));
2976
+ GlobalWidgetFactory.register("row", (node, config) => new RowWidget(
2977
+ (config?.widgets ?? []).map((child) => GlobalWidgetFactory.createFromConfig(node, child)),
2978
+ config
2979
+ ));
2826
2980
 
2827
2981
  // src/connection.ts
2828
2982
  var BACKWARD_PORT_STUB = 30;
@@ -2907,7 +3061,7 @@ var Connection = class {
2907
3061
  return;
2908
3062
  }
2909
3063
  if (this.#inNode !== null) {
2910
- const inPortBox = this.#inNode.inputPortPosition(this.#inNodePortIndex);
3064
+ const inPortBox = this.#inSlotBox();
2911
3065
  if (inPortBox === void 0) {
2912
3066
  return;
2913
3067
  }
@@ -2966,14 +3120,14 @@ var Connection = class {
2966
3120
  this.clearInput();
2967
3121
  this.clearOutput();
2968
3122
  }
2969
- setInput(node, portIndex, replace) {
3123
+ setInput(node, portIndex, replace, insertAt) {
2970
3124
  this.#inNode = node;
2971
3125
  this.#inNodePortIndex = portIndex;
2972
3126
  const port = this.#inNode.inputPort(portIndex);
2973
3127
  if (!!replace && port.connections().length > 0) {
2974
3128
  port.replaceConnection(this, 0);
2975
3129
  } else {
2976
- port.addConnection(this);
3130
+ port.addConnection(this, insertAt);
2977
3131
  }
2978
3132
  }
2979
3133
  setOutput(node, portIndex) {
@@ -2991,9 +3145,33 @@ var Connection = class {
2991
3145
  this.#outNode = null;
2992
3146
  this.#outNodePortIndex = -1;
2993
3147
  }
3148
+ /**
3149
+ * The part of the input port this wire lands on: the whole port, or its
3150
+ * own slot on a port holding several wires.
3151
+ */
3152
+ #inSlotBox() {
3153
+ if (this.#inNode === null) {
3154
+ return void 0;
3155
+ }
3156
+ const box = this.#inNode.inputPortPosition(this.#inNodePortIndex);
3157
+ const port = this.#inNode.inputPort(this.#inNodePortIndex);
3158
+ const slots = port.slots();
3159
+ if (box === void 0 || slots === 1) {
3160
+ return box;
3161
+ }
3162
+ const index = port.connections().indexOf(this);
3163
+ if (index === -1) {
3164
+ return box;
3165
+ }
3166
+ const height2 = box.Size.y / slots;
3167
+ return {
3168
+ Position: { x: box.Position.x, y: box.Position.y + index * height2 },
3169
+ Size: { x: box.Size.x, y: height2 }
3170
+ };
3171
+ }
2994
3172
  mouseOverPort(mousePosition) {
2995
3173
  if (this.#inNode !== null) {
2996
- const inPortBox = this.#inNode.inputPortPosition(this.#inNodePortIndex);
3174
+ const inPortBox = this.#inSlotBox();
2997
3175
  if (inPortBox !== void 0 && InBox(inPortBox, mousePosition)) {
2998
3176
  return this.#inNode.inputPort(this.#inNodePortIndex);
2999
3177
  }
@@ -3070,7 +3248,10 @@ var Camera = class {
3070
3248
  // src/organize.ts
3071
3249
  var widthSpacing = 100;
3072
3250
  var heightSpacing = 50;
3251
+ var laneSpacing = 25;
3252
+ var pieceSpacing = 150;
3073
3253
  var orderingSweeps = 8;
3254
+ var placementSweeps = 8;
3074
3255
  function topologicalOrder(sorting, upstream) {
3075
3256
  const visited = /* @__PURE__ */ new Set();
3076
3257
  const order = new Array();
@@ -3092,11 +3273,7 @@ function topologicalOrder(sorting, upstream) {
3092
3273
  continue;
3093
3274
  }
3094
3275
  visited.add(top.node);
3095
- const feeders = upstream.get(top.node);
3096
- if (feeders === void 0) {
3097
- continue;
3098
- }
3099
- for (const feeder of feeders) {
3276
+ for (const feeder of upstream.get(top.node) ?? []) {
3100
3277
  if (!visited.has(feeder)) {
3101
3278
  stack.push({ node: feeder, expanded: false });
3102
3279
  }
@@ -3105,43 +3282,103 @@ function topologicalOrder(sorting, upstream) {
3105
3282
  }
3106
3283
  return order;
3107
3284
  }
3285
+ function consumersOf(sorting, upstream) {
3286
+ const downstream = /* @__PURE__ */ new Map();
3287
+ for (const node of sorting) {
3288
+ downstream.set(node, []);
3289
+ }
3290
+ for (const node of sorting) {
3291
+ for (const feeder of upstream.get(node) ?? []) {
3292
+ const consumers = downstream.get(feeder);
3293
+ if (consumers !== void 0 && !consumers.includes(node)) {
3294
+ consumers.push(node);
3295
+ }
3296
+ }
3297
+ }
3298
+ return downstream;
3299
+ }
3108
3300
  function assignLayers(sorting, upstream) {
3109
- const order = topologicalOrder(sorting, upstream);
3301
+ const downstream = consumersOf(sorting, upstream);
3302
+ const order = topologicalOrder(sorting, downstream);
3110
3303
  const placed = /* @__PURE__ */ new Set();
3111
- const layerOf = /* @__PURE__ */ new Map();
3304
+ const rank = /* @__PURE__ */ new Map();
3112
3305
  for (const node of order) {
3113
- let layer = 0;
3114
- for (const feeder of upstream.get(node) ?? []) {
3115
- if (placed.has(feeder)) {
3116
- layer = Math.max(layer, layerOf.get(feeder) + 1);
3306
+ let r = 0;
3307
+ for (const consumer of downstream.get(node) ?? []) {
3308
+ if (placed.has(consumer)) {
3309
+ r = Math.max(r, rank.get(consumer) + 1);
3117
3310
  }
3118
3311
  }
3119
- layerOf.set(node, layer);
3312
+ rank.set(node, r);
3120
3313
  placed.add(node);
3121
3314
  }
3122
3315
  let deepest = 0;
3123
- for (const layer of layerOf.values()) {
3124
- deepest = Math.max(deepest, layer);
3316
+ for (const r of rank.values()) {
3317
+ deepest = Math.max(deepest, r);
3125
3318
  }
3126
- const layers = new Array(deepest + 1);
3127
- for (let i = 0; i < layers.length; i++) {
3128
- layers[i] = new Array();
3319
+ const layerOf = /* @__PURE__ */ new Map();
3320
+ for (const node of sorting) {
3321
+ layerOf.set(node, deepest - rank.get(node));
3129
3322
  }
3130
- for (const node of order) {
3131
- layers[layerOf.get(node)].push(node);
3323
+ for (const node of sorting) {
3324
+ const layer = layerOf.get(node);
3325
+ if ((downstream.get(node) ?? []).some((c) => layerOf.get(c) > layer)) {
3326
+ continue;
3327
+ }
3328
+ let pulled = -1;
3329
+ for (const feeder of upstream.get(node) ?? []) {
3330
+ const feederLayer = layerOf.get(feeder);
3331
+ if (feederLayer !== void 0 && feederLayer < layer) {
3332
+ pulled = Math.max(pulled, feederLayer + 1);
3333
+ }
3334
+ }
3335
+ if (pulled >= 0) {
3336
+ layerOf.set(node, pulled);
3337
+ }
3338
+ }
3339
+ const used = [...new Set(layerOf.values())].sort((a, b) => a - b);
3340
+ const compact = /* @__PURE__ */ new Map();
3341
+ used.forEach((layer, i) => compact.set(layer, i));
3342
+ const layers = used.map(() => new Array());
3343
+ for (const node of sorting) {
3344
+ const layer = compact.get(layerOf.get(node));
3345
+ layerOf.set(node, layer);
3346
+ layers[layer].push(node);
3132
3347
  }
3133
3348
  return { layers, layerOf };
3134
3349
  }
3135
- function orderWithinLayers(layout, upstream, downstream) {
3350
+ function crossingsOf(layout, downstream) {
3136
3351
  const slotOf = /* @__PURE__ */ new Map();
3137
- const recordSlots = () => {
3138
- for (const layer of layout.layers) {
3139
- for (let i = 0; i < layer.length; i++) {
3140
- slotOf.set(layer[i], i);
3352
+ for (const layer of layout.layers) {
3353
+ layer.forEach((node, slot) => slotOf.set(node, slot));
3354
+ }
3355
+ let total = 0;
3356
+ for (let i = 0; i + 1 < layout.layers.length; i++) {
3357
+ const spans = new Array();
3358
+ for (const node of layout.layers[i]) {
3359
+ for (const consumer of downstream.get(node) ?? []) {
3360
+ if (layout.layerOf.get(consumer) === i + 1) {
3361
+ spans.push([slotOf.get(node), slotOf.get(consumer)]);
3362
+ }
3141
3363
  }
3142
3364
  }
3143
- };
3144
- recordSlots();
3365
+ for (let a = 0; a < spans.length; a++) {
3366
+ for (let b = a + 1; b < spans.length; b++) {
3367
+ if ((spans[a][0] - spans[b][0]) * (spans[a][1] - spans[b][1]) < 0) {
3368
+ total++;
3369
+ }
3370
+ }
3371
+ }
3372
+ }
3373
+ return total;
3374
+ }
3375
+ function orderWithinLayers(layout, upstream, downstream) {
3376
+ const slotOf = /* @__PURE__ */ new Map();
3377
+ for (const layer of layout.layers) {
3378
+ for (let i = 0; i < layer.length; i++) {
3379
+ slotOf.set(layer[i], i);
3380
+ }
3381
+ }
3145
3382
  const meanSlot = (node, neighbours, layer) => {
3146
3383
  let total = 0;
3147
3384
  let count = 0;
@@ -3161,22 +3398,377 @@ function orderWithinLayers(layout, upstream, downstream) {
3161
3398
  if (adjacent < 0 || adjacent >= layout.layers.length) {
3162
3399
  continue;
3163
3400
  }
3164
- const layer = layout.layers[index];
3165
- const keys = /* @__PURE__ */ new Map();
3166
- for (const node of layer) {
3167
- const mean = meanSlot(node, neighbours, adjacent);
3168
- keys.set(node, mean === -1 ? slotOf.get(node) : mean);
3401
+ const layer = layout.layers[index];
3402
+ const keys = /* @__PURE__ */ new Map();
3403
+ for (const node of layer) {
3404
+ const mean = meanSlot(node, neighbours, adjacent);
3405
+ keys.set(node, mean === -1 ? slotOf.get(node) : mean);
3406
+ }
3407
+ layer.sort((a, b) => keys.get(a) - keys.get(b));
3408
+ for (let s = 0; s < layer.length; s++) {
3409
+ slotOf.set(layer[s], s);
3410
+ }
3411
+ }
3412
+ };
3413
+ let best = layout.layers.map((layer) => layer.slice());
3414
+ let fewest = crossingsOf(layout, downstream);
3415
+ for (let i = 0; i < orderingSweeps && fewest > 0; i++) {
3416
+ sweep(upstream, -1);
3417
+ sweep(downstream, 1);
3418
+ const crossings = crossingsOf(layout, downstream);
3419
+ if (crossings < fewest) {
3420
+ fewest = crossings;
3421
+ best = layout.layers.map((layer) => layer.slice());
3422
+ }
3423
+ }
3424
+ for (let i = 0; i < best.length; i++) {
3425
+ layout.layers[i] = best[i];
3426
+ }
3427
+ }
3428
+ function findPieces(sorting, upstream, downstream) {
3429
+ const rankOf = /* @__PURE__ */ new Map();
3430
+ sorting.forEach((node, i) => rankOf.set(node, i));
3431
+ const seen = /* @__PURE__ */ new Set();
3432
+ const pieces = new Array();
3433
+ for (const start of sorting) {
3434
+ if (seen.has(start)) {
3435
+ continue;
3436
+ }
3437
+ seen.add(start);
3438
+ const piece = new Array();
3439
+ const stack = [start];
3440
+ while (stack.length > 0) {
3441
+ const node = stack.pop();
3442
+ piece.push(node);
3443
+ for (const neighbour of [...upstream.get(node) ?? [], ...downstream.get(node) ?? []]) {
3444
+ if (rankOf.has(neighbour) && !seen.has(neighbour)) {
3445
+ seen.add(neighbour);
3446
+ stack.push(neighbour);
3447
+ }
3448
+ }
3449
+ }
3450
+ piece.sort((a, b) => rankOf.get(a) - rankOf.get(b));
3451
+ pieces.push(piece);
3452
+ }
3453
+ return pieces;
3454
+ }
3455
+ function treeRoot(piece, downstream) {
3456
+ let root;
3457
+ for (const node of piece) {
3458
+ const consumers = downstream.get(node) ?? [];
3459
+ if (consumers.length > 1) {
3460
+ return void 0;
3461
+ }
3462
+ if (consumers.length === 0) {
3463
+ if (root !== void 0) {
3464
+ return void 0;
3465
+ }
3466
+ root = node;
3467
+ }
3468
+ }
3469
+ return root;
3470
+ }
3471
+ function columnXs(widths) {
3472
+ const xs = new Array();
3473
+ let x = 0;
3474
+ for (const width2 of widths) {
3475
+ xs.push(x);
3476
+ x += width2 + widthSpacing;
3477
+ }
3478
+ return xs;
3479
+ }
3480
+ function layoutTree(root, upstream, sizes) {
3481
+ const children = (node) => [...new Set(upstream.get(node) ?? [])];
3482
+ const depthOf = /* @__PURE__ */ new Map([[root, 0]]);
3483
+ const preorder = new Array();
3484
+ const stack = [root];
3485
+ let deepest = 0;
3486
+ while (stack.length > 0) {
3487
+ const node = stack.pop();
3488
+ preorder.push(node);
3489
+ const kids = children(node);
3490
+ for (let i = kids.length - 1; i >= 0; i--) {
3491
+ const depth = depthOf.get(node) + 1;
3492
+ depthOf.set(kids[i], depth);
3493
+ deepest = Math.max(deepest, depth);
3494
+ stack.push(kids[i]);
3495
+ }
3496
+ }
3497
+ const widths = new Array(deepest + 1).fill(0);
3498
+ for (const [node, depth] of depthOf) {
3499
+ widths[deepest - depth] = Math.max(widths[deepest - depth], sizes.get(node).x);
3500
+ }
3501
+ const xs = columnXs(widths);
3502
+ const band = /* @__PURE__ */ new Map();
3503
+ const topInBand = /* @__PURE__ */ new Map();
3504
+ const childOffset = /* @__PURE__ */ new Map();
3505
+ for (let i = preorder.length - 1; i >= 0; i--) {
3506
+ const node = preorder[i];
3507
+ const height2 = sizes.get(node).y;
3508
+ const kids = children(node);
3509
+ if (kids.length === 0) {
3510
+ band.set(node, height2);
3511
+ topInBand.set(node, 0);
3512
+ continue;
3513
+ }
3514
+ let offset = 0;
3515
+ for (const kid of kids) {
3516
+ childOffset.set(kid, offset);
3517
+ offset += band.get(kid) + heightSpacing;
3518
+ }
3519
+ const centre = (kid) => childOffset.get(kid) + topInBand.get(kid) + sizes.get(kid).y / 2;
3520
+ let top = (centre(kids[0]) + centre(kids[kids.length - 1])) / 2 - height2 / 2;
3521
+ let shift = 0;
3522
+ if (top < 0) {
3523
+ shift = -top;
3524
+ top = 0;
3525
+ for (const kid of kids) {
3526
+ childOffset.set(kid, childOffset.get(kid) + shift);
3527
+ }
3528
+ }
3529
+ band.set(node, Math.max(offset - heightSpacing + shift, top + height2));
3530
+ topInBand.set(node, top);
3531
+ }
3532
+ const positions = /* @__PURE__ */ new Map();
3533
+ const bandTop = /* @__PURE__ */ new Map([[root, 0]]);
3534
+ for (const node of preorder) {
3535
+ const column = deepest - depthOf.get(node);
3536
+ const size = sizes.get(node);
3537
+ positions.set(node, {
3538
+ x: xs[column] + (widths[column] - size.x) / 2,
3539
+ y: bandTop.get(node) + topInBand.get(node)
3540
+ });
3541
+ for (const kid of children(node)) {
3542
+ bandTop.set(kid, bandTop.get(node) + childOffset.get(kid));
3543
+ }
3544
+ }
3545
+ return positions;
3546
+ }
3547
+ function isotonic(targets) {
3548
+ const blocks = new Array();
3549
+ for (const target of targets) {
3550
+ blocks.push({ sum: target, count: 1 });
3551
+ while (blocks.length > 1) {
3552
+ const last = blocks[blocks.length - 1];
3553
+ const before = blocks[blocks.length - 2];
3554
+ if (before.sum / before.count <= last.sum / last.count) {
3555
+ break;
3556
+ }
3557
+ before.sum += last.sum;
3558
+ before.count += last.count;
3559
+ blocks.pop();
3560
+ }
3561
+ }
3562
+ const result = new Array();
3563
+ for (const block of blocks) {
3564
+ for (let i = 0; i < block.count; i++) {
3565
+ result.push(block.sum / block.count);
3566
+ }
3567
+ }
3568
+ return result;
3569
+ }
3570
+ function median(values) {
3571
+ const sorted = values.slice().sort((a, b) => a - b);
3572
+ const middle = Math.floor(sorted.length / 2);
3573
+ return sorted.length % 2 === 1 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
3574
+ }
3575
+ function layoutLayered(piece, upstream, downstream, sizes) {
3576
+ const layout = assignLayers(piece, upstream);
3577
+ const up = /* @__PURE__ */ new Map();
3578
+ const down = /* @__PURE__ */ new Map();
3579
+ const heightOf = /* @__PURE__ */ new Map();
3580
+ for (const node of piece) {
3581
+ up.set(node, [...new Set(upstream.get(node) ?? [])]);
3582
+ down.set(node, [...new Set(downstream.get(node) ?? [])]);
3583
+ heightOf.set(node, sizes.get(node).y);
3584
+ }
3585
+ let nextLane = -1;
3586
+ for (const from of piece) {
3587
+ const fromLayer = layout.layerOf.get(from);
3588
+ const consumers = down.get(from);
3589
+ for (let c = 0; c < consumers.length; c++) {
3590
+ const to = consumers[c];
3591
+ const toLayer = layout.layerOf.get(to);
3592
+ if (toLayer - fromLayer < 2) {
3593
+ continue;
3594
+ }
3595
+ let previous = from;
3596
+ for (let layer = fromLayer + 1; layer < toLayer; layer++) {
3597
+ const lane = nextLane--;
3598
+ layout.layerOf.set(lane, layer);
3599
+ layout.layers[layer].push(lane);
3600
+ heightOf.set(lane, 0);
3601
+ up.set(lane, [previous]);
3602
+ down.set(lane, []);
3603
+ if (previous === from) {
3604
+ consumers[c] = lane;
3605
+ } else {
3606
+ down.get(previous).push(lane);
3607
+ }
3608
+ previous = lane;
3609
+ }
3610
+ down.get(previous).push(to);
3611
+ const feeders = up.get(to);
3612
+ feeders[feeders.indexOf(from)] = previous;
3613
+ }
3614
+ }
3615
+ const sequence = /* @__PURE__ */ new Map();
3616
+ for (const sink of piece) {
3617
+ if (down.get(sink).length > 0) {
3618
+ continue;
3619
+ }
3620
+ const stack = [sink];
3621
+ while (stack.length > 0) {
3622
+ const node = stack.pop();
3623
+ if (sequence.has(node)) {
3624
+ continue;
3625
+ }
3626
+ sequence.set(node, sequence.size);
3627
+ const feeders = up.get(node) ?? [];
3628
+ for (let i = feeders.length - 1; i >= 0; i--) {
3629
+ stack.push(feeders[i]);
3169
3630
  }
3170
- layer.sort((a, b) => keys.get(a) - keys.get(b));
3171
- for (let s = 0; s < layer.length; s++) {
3172
- slotOf.set(layer[s], s);
3631
+ }
3632
+ }
3633
+ for (const layer of layout.layers) {
3634
+ layer.sort((a, b) => (sequence.get(a) ?? Infinity) - (sequence.get(b) ?? Infinity));
3635
+ }
3636
+ orderWithinLayers(layout, up, down);
3637
+ const gapAfter = (above, below) => above < 0 || below < 0 ? laneSpacing : heightSpacing;
3638
+ const topOf = /* @__PURE__ */ new Map();
3639
+ for (const layer of layout.layers) {
3640
+ let y = 0;
3641
+ for (let i = 0; i < layer.length; i++) {
3642
+ topOf.set(layer[i], y);
3643
+ y += heightOf.get(layer[i]) + (i + 1 < layer.length ? gapAfter(layer[i], layer[i + 1]) : 0);
3644
+ }
3645
+ for (const node of layer) {
3646
+ topOf.set(node, topOf.get(node) - y / 2);
3647
+ }
3648
+ }
3649
+ const centreOf = (node) => topOf.get(node) + heightOf.get(node) / 2;
3650
+ const place = (layer, neighbours, adjacent) => {
3651
+ const offsets = new Array();
3652
+ const targets = new Array();
3653
+ let offset = 0;
3654
+ for (let i = 0; i < layer.length; i++) {
3655
+ const node = layer[i];
3656
+ const centres = (neighbours.get(node) ?? []).filter((n) => layout.layerOf.get(n) === adjacent).map(centreOf);
3657
+ const desired = centres.length > 0 ? median(centres) - heightOf.get(node) / 2 : topOf.get(node);
3658
+ offsets.push(offset);
3659
+ targets.push(desired - offset);
3660
+ if (i + 1 < layer.length) {
3661
+ offset += heightOf.get(node) + gapAfter(node, layer[i + 1]);
3173
3662
  }
3174
3663
  }
3664
+ const solved = isotonic(targets);
3665
+ for (let i = 0; i < layer.length; i++) {
3666
+ topOf.set(layer[i], solved[i] + offsets[i]);
3667
+ }
3175
3668
  };
3176
- for (let i = 0; i < orderingSweeps; i++) {
3177
- sweep(upstream, -1);
3178
- sweep(downstream, 1);
3669
+ for (let s = 0; s < placementSweeps; s++) {
3670
+ if (s % 2 === 0) {
3671
+ for (let i = 1; i < layout.layers.length; i++) {
3672
+ place(layout.layers[i], up, i - 1);
3673
+ }
3674
+ } else {
3675
+ for (let i = layout.layers.length - 2; i >= 0; i--) {
3676
+ place(layout.layers[i], down, i + 1);
3677
+ }
3678
+ }
3679
+ }
3680
+ const widths = layout.layers.map((layer) => layer.reduce((widest, node) => node < 0 ? widest : Math.max(widest, sizes.get(node).x), 0));
3681
+ const xs = columnXs(widths);
3682
+ const positions = /* @__PURE__ */ new Map();
3683
+ for (const node of piece) {
3684
+ const layer = layout.layerOf.get(node);
3685
+ positions.set(node, {
3686
+ x: xs[layer] + (widths[layer] - sizes.get(node).x) / 2,
3687
+ y: topOf.get(node)
3688
+ });
3179
3689
  }
3690
+ return positions;
3691
+ }
3692
+ function layoutLoose(loose, sizes) {
3693
+ const columns = Math.ceil(Math.sqrt(loose.length));
3694
+ const widths = new Array(columns).fill(0);
3695
+ const heights = new Array(Math.ceil(loose.length / columns)).fill(0);
3696
+ loose.forEach((node, i) => {
3697
+ const size = sizes.get(node);
3698
+ widths[i % columns] = Math.max(widths[i % columns], size.x);
3699
+ heights[Math.floor(i / columns)] = Math.max(heights[Math.floor(i / columns)], size.y);
3700
+ });
3701
+ const xs = new Array();
3702
+ let x = 0;
3703
+ for (const width2 of widths) {
3704
+ xs.push(x);
3705
+ x += width2 + heightSpacing;
3706
+ }
3707
+ const ys = new Array();
3708
+ let y = 0;
3709
+ for (const height2 of heights) {
3710
+ ys.push(y);
3711
+ y += height2 + heightSpacing;
3712
+ }
3713
+ const positions = /* @__PURE__ */ new Map();
3714
+ loose.forEach((node, i) => positions.set(node, { x: xs[i % columns], y: ys[Math.floor(i / columns)] }));
3715
+ return positions;
3716
+ }
3717
+ function toBlock(positions, sizes) {
3718
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
3719
+ for (const [node, position] of positions) {
3720
+ const size = sizes.get(node);
3721
+ minX = Math.min(minX, position.x);
3722
+ minY = Math.min(minY, position.y);
3723
+ maxX = Math.max(maxX, position.x + size.x);
3724
+ maxY = Math.max(maxY, position.y + size.y);
3725
+ }
3726
+ const shifted = /* @__PURE__ */ new Map();
3727
+ for (const [node, position] of positions) {
3728
+ shifted.set(node, { x: position.x - minX, y: position.y - minY });
3729
+ }
3730
+ return { positions: shifted, size: { x: maxX - minX, y: maxY - minY } };
3731
+ }
3732
+ function layoutGraph(sorting, upstream, downstream, sizes) {
3733
+ const blocks = new Array();
3734
+ const loose = new Array();
3735
+ for (const piece of findPieces(sorting, upstream, downstream)) {
3736
+ if (piece.length === 1) {
3737
+ loose.push(piece[0]);
3738
+ continue;
3739
+ }
3740
+ const root = treeRoot(piece, downstream);
3741
+ const positions2 = root !== void 0 ? layoutTree(root, upstream, sizes) : layoutLayered(piece, upstream, downstream, sizes);
3742
+ blocks.push(toBlock(positions2, sizes));
3743
+ }
3744
+ blocks.sort((a, b) => b.size.y - a.size.y || b.size.x - a.size.x);
3745
+ if (loose.length > 0) {
3746
+ blocks.push(toBlock(layoutLoose(loose, sizes), sizes));
3747
+ }
3748
+ let area = 0;
3749
+ let widest = 0;
3750
+ for (const block of blocks) {
3751
+ area += (block.size.x + pieceSpacing) * (block.size.y + pieceSpacing);
3752
+ widest = Math.max(widest, block.size.x);
3753
+ }
3754
+ const rowWidth = Math.max(widest, Math.sqrt(area));
3755
+ const positions = /* @__PURE__ */ new Map();
3756
+ let x = 0;
3757
+ let y = 0;
3758
+ let rowHeight = 0;
3759
+ for (const block of blocks) {
3760
+ if (x > 0 && x + block.size.x > rowWidth) {
3761
+ x = 0;
3762
+ y += rowHeight + pieceSpacing;
3763
+ rowHeight = 0;
3764
+ }
3765
+ for (const [node, position] of block.positions) {
3766
+ positions.set(node, { x: position.x + x, y: position.y + y });
3767
+ }
3768
+ x += block.size.x + pieceSpacing;
3769
+ rowHeight = Math.max(rowHeight, block.size.y);
3770
+ }
3771
+ return positions;
3180
3772
  }
3181
3773
  function Organize(ctx, graph, nodesToSort) {
3182
3774
  const nodes = graph.getNodes();
@@ -3185,9 +3777,6 @@ function Organize(ctx, graph, nodesToSort) {
3185
3777
  }
3186
3778
  const sorting = new Array();
3187
3779
  if (nodesToSort) {
3188
- if (nodesToSort.length < 2) {
3189
- return;
3190
- }
3191
3780
  for (const index of nodesToSort) {
3192
3781
  if (index >= 0 && index < nodes.length) {
3193
3782
  sorting.push(index);
@@ -3207,33 +3796,28 @@ function Organize(ctx, graph, nodesToSort) {
3207
3796
  nodeLUT.set(nodes[i], i);
3208
3797
  }
3209
3798
  const camera = new Camera();
3210
- const bounds = /* @__PURE__ */ new Map();
3799
+ const sizes = /* @__PURE__ */ new Map();
3211
3800
  for (const index of sorting) {
3212
- bounds.set(index, nodes[index].calculateBounds(ctx, camera));
3801
+ sizes.set(index, nodes[index].calculateBounds(ctx, camera).Size);
3213
3802
  }
3214
3803
  const upstream = /* @__PURE__ */ new Map();
3215
- const downstream = /* @__PURE__ */ new Map();
3216
3804
  for (const index of sorting) {
3805
+ const node = nodes[index];
3217
3806
  const feeders = new Array();
3218
- for (const feeder of graph.connectedInputsNodeReferencesByIndex(index)) {
3219
- const feederIndex = nodeLUT.get(feeder);
3220
- if (feederIndex !== void 0 && inLayout.has(feederIndex) && feederIndex !== index) {
3221
- feeders.push(feederIndex);
3807
+ for (let i = 0; i < node.inputs(); i++) {
3808
+ for (const connection of node.inputPort(i).connections()) {
3809
+ const feeder = connection.outNode();
3810
+ const feederIndex = feeder === null ? void 0 : nodeLUT.get(feeder);
3811
+ if (feederIndex !== void 0 && inLayout.has(feederIndex) && feederIndex !== index && !feeders.includes(feederIndex)) {
3812
+ feeders.push(feederIndex);
3813
+ }
3222
3814
  }
3223
3815
  }
3224
3816
  upstream.set(index, feeders);
3225
- const consumers = new Array();
3226
- for (const consumer of graph.connectedOutputsNodeReferences(index)) {
3227
- const consumerIndex = nodeLUT.get(consumer);
3228
- if (consumerIndex !== void 0 && inLayout.has(consumerIndex) && consumerIndex !== index) {
3229
- consumers.push(consumerIndex);
3230
- }
3231
- }
3232
- downstream.set(index, consumers);
3233
3817
  }
3234
- const layout = assignLayers(sorting, upstream);
3235
- orderWithinLayers(layout, upstream, downstream);
3236
- let anchor = { x: 0, y: 0 };
3818
+ const downstream = consumersOf(sorting, upstream);
3819
+ const positions = layoutGraph(sorting, upstream, downstream, sizes);
3820
+ const anchor = { x: 0, y: 0 };
3237
3821
  for (const index of sorting) {
3238
3822
  const position = nodes[index].getPosition();
3239
3823
  anchor.x += position.x;
@@ -3241,19 +3825,14 @@ function Organize(ctx, graph, nodesToSort) {
3241
3825
  }
3242
3826
  anchor.x /= sorting.length;
3243
3827
  anchor.y /= sorting.length;
3244
- const layerWidths = layout.layers.map((layer) => layer.reduce((widest, node) => Math.max(widest, bounds.get(node).Size.x), 0));
3245
- const layerHeights = layout.layers.map((layer) => layer.reduce((total, node) => total + bounds.get(node).Size.y + heightSpacing, -heightSpacing));
3246
- const totalWidth = layerWidths.reduce((total, width2) => total + width2 + widthSpacing, -widthSpacing);
3247
- const tallest = layerHeights.reduce((tallest2, height2) => Math.max(tallest2, height2), 0);
3248
- let x = anchor.x - totalWidth / 2;
3249
- for (let i = 0; i < layout.layers.length; i++) {
3250
- let y = anchor.y - tallest / 2 + (tallest - layerHeights[i]) / 2;
3251
- for (const node of layout.layers[i]) {
3252
- const size = bounds.get(node).Size;
3253
- nodes[node].setPosition({ x: x + (layerWidths[i] - size.x) / 2, y });
3254
- y += size.y + heightSpacing;
3255
- }
3256
- x += layerWidths[i] + widthSpacing;
3828
+ let right = 0;
3829
+ let bottom = 0;
3830
+ for (const [index, position] of positions) {
3831
+ right = Math.max(right, position.x + sizes.get(index).x);
3832
+ bottom = Math.max(bottom, position.y + sizes.get(index).y);
3833
+ }
3834
+ for (const [index, position] of positions) {
3835
+ nodes[index].setPosition({ x: anchor.x - right / 2 + position.x, y: anchor.y - bottom / 2 + position.y });
3257
3836
  }
3258
3837
  }
3259
3838
 
@@ -3445,14 +4024,17 @@ var NodeSubsystem2 = class {
3445
4024
  #portHovering;
3446
4025
  #widgetHovering;
3447
4026
  #widgetCurrentlyClicking;
4027
+ #widgetHoveringBox = null;
4028
+ #capturedWidgetBox = null;
4029
+ #capturedPointer = Zero();
3448
4030
  #cursor;
3449
4031
  #nodeFactory;
3450
4032
  #boxSelect;
3451
4033
  #boxSelectStart_graphSpace;
3452
4034
  #boxSelectEnd_graphSpace;
3453
4035
  #boxSelectionNodes;
3454
- #boxSelectStyle;
3455
4036
  #lastCtx;
4037
+ #lastMousePosition;
3456
4038
  // LISTENERS ==============================================================
3457
4039
  #nodeRemovedCallbacks;
3458
4040
  #registeredNodeAddedCallbacks;
@@ -3477,14 +4059,6 @@ var NodeSubsystem2 = class {
3477
4059
  this.#boxSelectEnd_graphSpace = Zero();
3478
4060
  this.#boxSelectionNodes = new List();
3479
4061
  this.#lastCtx = null;
3480
- this.#boxSelectStyle = new BoxStyle({
3481
- border: {
3482
- color: Theme.BoxSelect.Color,
3483
- size: Theme.BoxSelect.Size
3484
- },
3485
- color: "rgba(0,0,0,0)",
3486
- radius: Theme.BoxSelect.Radius
3487
- });
3488
4062
  }
3489
4063
  addPublisher(identifier, publisher) {
3490
4064
  this.#nodeFactory.addPublisher(identifier, publisher);
@@ -3521,6 +4095,7 @@ var NodeSubsystem2 = class {
3521
4095
  }
3522
4096
  if (intersection.Widget !== void 0) {
3523
4097
  this.#widgetHovering = intersection.Widget;
4098
+ this.#widgetHoveringBox = intersection.WidgetBox ?? null;
3524
4099
  }
3525
4100
  if (intersection.Port !== void 0 && intersection.Node !== void 0 && intersection.PortIndex !== void 0 && intersection.PortIsInput !== void 0) {
3526
4101
  this.#portHovering = {
@@ -3543,8 +4118,15 @@ var NodeSubsystem2 = class {
3543
4118
  hoveringSomething = true;
3544
4119
  }
3545
4120
  if (this.#widgetHovering !== null) {
3546
- this.#widgetHovering.ClickStart();
3547
- this.#widgetCurrentlyClicking = this.#widgetHovering;
4121
+ if (!this.#widgetHovering.Disabled?.()) {
4122
+ this.#widgetCurrentlyClicking = this.#widgetHovering;
4123
+ this.#capturedWidgetBox = this.#widgetHoveringBox === null ? null : {
4124
+ Position: { x: this.#widgetHoveringBox.Position.x, y: this.#widgetHoveringBox.Position.y },
4125
+ Size: { x: this.#widgetHoveringBox.Size.x, y: this.#widgetHoveringBox.Size.y }
4126
+ };
4127
+ CopyVector2(this.#capturedPointer, mousePosition);
4128
+ this.#widgetCurrentlyClicking.PointerDown?.(this.#widgetPointer());
4129
+ }
3548
4130
  hoveringSomething = true;
3549
4131
  }
3550
4132
  if (this.#portHovering === null) {
@@ -3559,12 +4141,12 @@ var NodeSubsystem2 = class {
3559
4141
  return hoveringSomething || ctrlKey;
3560
4142
  }
3561
4143
  if (this.#portHovering.InputPort) {
3562
- for (let i = 0; i < this.#connections.length; i++) {
3563
- if (this.#connections[i].inPort() === this.#portHovering.Port) {
3564
- this.#connections[i].clearInput();
3565
- this.#connectionSelected = this.#connections[i];
3566
- break;
3567
- }
4144
+ const port = this.#portHovering.Port;
4145
+ const wires = port.connections();
4146
+ const picked = wires.find((wire) => wire.mouseOverPort(mousePosition) === port) ?? wires[0];
4147
+ if (picked !== void 0) {
4148
+ picked.clearInput();
4149
+ this.#connectionSelected = picked;
3568
4150
  }
3569
4151
  } else {
3570
4152
  let inNode = this.#portHovering.Node;
@@ -3584,7 +4166,19 @@ var NodeSubsystem2 = class {
3584
4166
  }
3585
4167
  return true;
3586
4168
  }
4169
+ #widgetPointer() {
4170
+ return {
4171
+ position: { x: this.#capturedPointer.x, y: this.#capturedPointer.y },
4172
+ inside: this.#capturedWidgetBox !== null && InBox(this.#capturedWidgetBox, this.#capturedPointer)
4173
+ };
4174
+ }
3587
4175
  mouseDragEvent(delta, scale) {
4176
+ if (this.#widgetCurrentlyClicking !== null) {
4177
+ this.#capturedPointer.x += delta.x;
4178
+ this.#capturedPointer.y += delta.y;
4179
+ this.#widgetCurrentlyClicking.PointerMove?.(this.#widgetPointer());
4180
+ return true;
4181
+ }
3588
4182
  let nodeMoved = false;
3589
4183
  VectorPool.run(() => {
3590
4184
  const scaledDelta = VectorPool.get();
@@ -3703,13 +4297,20 @@ var NodeSubsystem2 = class {
3703
4297
  }
3704
4298
  #firstCompatiblePort(node, side, against) {
3705
4299
  const count = side === "INPUT" /* Input */ ? node.inputs() : node.outputs();
4300
+ let firstHidden = -1;
3706
4301
  for (let i = 0; i < count; i++) {
3707
4302
  const port = side === "INPUT" /* Input */ ? node.inputPort(i) : node.outputPort(i);
3708
- if (portsCompatible(port, against)) {
4303
+ if (!portsCompatible(port, against)) {
4304
+ continue;
4305
+ }
4306
+ if (!port.hidden()) {
3709
4307
  return i;
3710
4308
  }
4309
+ if (firstHidden === -1) {
4310
+ firstHidden = i;
4311
+ }
3711
4312
  }
3712
- return -1;
4313
+ return firstHidden;
3713
4314
  }
3714
4315
  clickEnd() {
3715
4316
  for (let i = 0; i < this.#nodesGrabbed.Count(); i++) {
@@ -3725,10 +4326,11 @@ var NodeSubsystem2 = class {
3725
4326
  this.#boxSelect = false;
3726
4327
  }
3727
4328
  if (this.#widgetCurrentlyClicking !== null) {
3728
- if (this.#widgetHovering === this.#widgetCurrentlyClicking) {
3729
- this.#widgetCurrentlyClicking.ClickEnd();
3730
- }
4329
+ const widget = this.#widgetCurrentlyClicking;
4330
+ const pointer = this.#widgetPointer();
3731
4331
  this.#widgetCurrentlyClicking = null;
4332
+ this.#capturedWidgetBox = null;
4333
+ widget.PointerUp?.(pointer);
3732
4334
  }
3733
4335
  if (this.#connectionSelected === null) {
3734
4336
  return;
@@ -3773,7 +4375,7 @@ var NodeSubsystem2 = class {
3773
4375
  inputPort.connections()[0].clearOutput();
3774
4376
  this.#removeConnection(inputPort.connections()[0], false);
3775
4377
  }
3776
- conn.setInput(this.#portHovering.Node, this.#portHovering.Index, replace);
4378
+ conn.setInput(this.#portHovering.Node, this.#portHovering.Index, replace, this.#insertIndex());
3777
4379
  } else {
3778
4380
  conn.setOutput(this.#portHovering.Node, this.#portHovering.Index);
3779
4381
  }
@@ -4145,7 +4747,8 @@ var NodeSubsystem2 = class {
4145
4747
  }
4146
4748
  if (intersection.Widget !== void 0) {
4147
4749
  this.#widgetHovering = intersection.Widget;
4148
- this.#cursor = "pointer" /* Pointer */;
4750
+ this.#widgetHoveringBox = intersection.WidgetBox ?? null;
4751
+ this.#cursor = intersection.Widget.Disabled?.() ? "default" /* Default */ : intersection.Widget.Cursor?.() ?? "pointer" /* Pointer */;
4149
4752
  }
4150
4753
  if (intersection.Port !== void 0 && intersection.Node !== void 0 && intersection.PortIndex !== void 0 && intersection.PortIsInput !== void 0) {
4151
4754
  this.#portHovering = {
@@ -4168,6 +4771,9 @@ var NodeSubsystem2 = class {
4168
4771
  }
4169
4772
  this.#nodes[i].render(ctx, camera, state, mousePosition, this.#postProcessPass);
4170
4773
  }
4774
+ if (this.#widgetCurrentlyClicking !== null) {
4775
+ this.#cursor = this.#widgetCurrentlyClicking.Cursor?.() ?? "pointer" /* Pointer */;
4776
+ }
4171
4777
  }
4172
4778
  #boxSelectionScreenspaceBox(camera) {
4173
4779
  const box = { Position: Zero(), Size: Zero() };
@@ -4176,29 +4782,174 @@ var NodeSubsystem2 = class {
4176
4782
  SubVector2(box.Size, box.Size, box.Position);
4177
4783
  return box;
4178
4784
  }
4785
+ /**
4786
+ * Where a wire being dropped on the hovered array input would go, or
4787
+ * undefined when it is not over one.
4788
+ */
4789
+ #insertIndex() {
4790
+ const hovering = this.#portHovering;
4791
+ if (hovering === null || !hovering.InputPort || this.#lastMousePosition === void 0 || hovering.Port.getPortType() !== "INPUTARRAY" /* InputArray */) {
4792
+ return void 0;
4793
+ }
4794
+ const box = hovering.Node.inputPortPosition(hovering.Index);
4795
+ return arrayInsertIndex(box, hovering.Port.connections().length, this.#lastMousePosition.y);
4796
+ }
4797
+ #renderInsertMarker(ctx, camera) {
4798
+ const hovering = this.#portHovering;
4799
+ if (this.#connectionSelected === null || this.#connectionSelected.inPort() !== null || hovering === null) {
4800
+ return;
4801
+ }
4802
+ const index = this.#insertIndex();
4803
+ const count = hovering.Port.connections().length;
4804
+ if (index === void 0 || count === 0) {
4805
+ return;
4806
+ }
4807
+ const box = hovering.Node.inputPortPosition(hovering.Index);
4808
+ const y = box.Position.y + index * box.Size.y / count;
4809
+ const overhang = 6 * camera.zoom;
4810
+ ctx.save();
4811
+ ctx.strokeStyle = "#ffffff";
4812
+ ctx.lineWidth = Math.max(1, 2 * camera.zoom);
4813
+ ctx.beginPath();
4814
+ ctx.moveTo(box.Position.x - overhang, y);
4815
+ ctx.lineTo(box.Position.x + box.Size.x + overhang, y);
4816
+ ctx.stroke();
4817
+ ctx.restore();
4818
+ }
4179
4819
  render(ctx, camera, mousePosition) {
4180
4820
  this.#lastCtx = ctx;
4821
+ this.#lastMousePosition = mousePosition;
4181
4822
  this.#cursor = "default" /* Default */;
4182
4823
  const awaitingLayout = this.#nodes.some((node) => !node.hasBeenLaidOut());
4824
+ for (const node of this.#nodes) {
4825
+ if (node.hasBeenLaidOut()) {
4826
+ node.placePorts(node.calculateBounds(ctx, camera), camera.zoom);
4827
+ }
4828
+ }
4183
4829
  TimeExecution("Render_Connections", () => {
4184
4830
  this.#renderConnections(ctx, camera, mousePosition);
4185
4831
  });
4186
4832
  TimeExecution("Render_Nodes", () => {
4187
4833
  this.#renderNodes(ctx, camera, mousePosition);
4188
4834
  });
4835
+ this.#renderInsertMarker(ctx, camera);
4189
4836
  if (awaitingLayout) {
4190
4837
  requestRender();
4191
4838
  }
4192
4839
  if (this.#boxSelect) {
4193
4840
  const box = this.#boxSelectionScreenspaceBox(camera);
4194
4841
  ctx.setLineDash([Theme.BoxSelect.LineDashLength]);
4195
- this.#boxSelectStyle.Draw(ctx, box, 1);
4842
+ new BoxStyle({
4843
+ border: { color: Theme.BoxSelect.Color, size: Theme.BoxSelect.Size },
4844
+ color: "rgba(0,0,0,0)",
4845
+ radius: Theme.BoxSelect.Radius
4846
+ }).Draw(ctx, box, 1);
4196
4847
  ctx.setLineDash([]);
4197
4848
  }
4198
4849
  return { cursorStyle: this.#cursor };
4199
4850
  }
4200
4851
  };
4201
4852
 
4853
+ // src/popup.ts
4854
+ var Popup = class {
4855
+ #title;
4856
+ #options;
4857
+ #content;
4858
+ #onClose;
4859
+ // Styling ================================================================
4860
+ #buttonCSS;
4861
+ // Runtime ================================================================
4862
+ #popup;
4863
+ #keyHandler;
4864
+ constructor(config) {
4865
+ this.#title = config.title;
4866
+ this.#options = config.options;
4867
+ this.#content = config.content;
4868
+ this.#onClose = config.onClose;
4869
+ this.#buttonCSS = config.buttonCSS === void 0 ? "flex-grow: 1; margin-right: 8px;" : config.buttonCSS;
4870
+ this.#popup = null;
4871
+ this.#keyHandler = null;
4872
+ }
4873
+ Hide() {
4874
+ if (this.#popup === null) {
4875
+ return;
4876
+ }
4877
+ if (this.#keyHandler !== null) {
4878
+ document.removeEventListener("keydown", this.#keyHandler, true);
4879
+ this.#keyHandler = null;
4880
+ }
4881
+ document.body.removeChild(this.#popup);
4882
+ this.#popup = null;
4883
+ }
4884
+ #close(option) {
4885
+ this.Hide();
4886
+ if (this.#onClose !== void 0) {
4887
+ this.#onClose(option);
4888
+ }
4889
+ }
4890
+ Show() {
4891
+ this.#popup = document.createElement("div");
4892
+ 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};`;
4893
+ document.body.appendChild(this.#popup);
4894
+ const container = document.createElement("div");
4895
+ 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;`;
4896
+ this.#popup.appendChild(container);
4897
+ const title = document.createElement("h2");
4898
+ title.textContent = this.#title;
4899
+ title.style.cssText = "margin-top:0;margin-bottom:16px;";
4900
+ container.appendChild(title);
4901
+ if (this.#content !== void 0) {
4902
+ container.appendChild(this.#content());
4903
+ }
4904
+ const buttonContainer = document.createElement("div");
4905
+ buttonContainer.style.cssText = "margin-top:16px;justify-content:space-around;align-items:center;flex-direction:row;display:flex;";
4906
+ container.appendChild(buttonContainer);
4907
+ for (let i = 0; i < this.#options.length; i++) {
4908
+ const button = document.createElement("button");
4909
+ button.style.cssText = this.#buttonCSS;
4910
+ button.textContent = this.#options[i];
4911
+ button.onclick = () => this.#close(this.#options[i]);
4912
+ buttonContainer.appendChild(button);
4913
+ }
4914
+ this.#keyHandler = (e) => {
4915
+ if (e.key === "Enter") {
4916
+ e.preventDefault();
4917
+ this.#close(this.#options.length > 0 ? this.#options[0] : null);
4918
+ } else if (e.key === "Escape") {
4919
+ e.preventDefault();
4920
+ this.#close(null);
4921
+ }
4922
+ e.stopPropagation();
4923
+ };
4924
+ document.addEventListener("keydown", this.#keyHandler, true);
4925
+ }
4926
+ };
4927
+
4928
+ // src/popups/string.ts
4929
+ function SetStringPopup(config) {
4930
+ let input = null;
4931
+ return new Popup({
4932
+ title: config.title,
4933
+ options: ["Set", "Cancel"],
4934
+ content: () => {
4935
+ const container = document.createElement("div");
4936
+ input = document.createElement("input");
4937
+ input.value = config.startingValue;
4938
+ container.append(input);
4939
+ return container;
4940
+ },
4941
+ onClose: (button) => {
4942
+ if (button !== "Set" || input === null) {
4943
+ if (config.onCancel) {
4944
+ config.onCancel();
4945
+ }
4946
+ return;
4947
+ }
4948
+ config.onUpdate(input.value);
4949
+ }
4950
+ });
4951
+ }
4952
+
4202
4953
  // src/popups/form.ts
4203
4954
  function FormPopup(config) {
4204
4955
  let input = new Array();
@@ -4315,9 +5066,11 @@ var FlowNode = class {
4315
5066
  #titleChangeCallback;
4316
5067
  #infoChangeCallback;
4317
5068
  // Styling ================================================================
5069
+ #styleConfig;
5070
+ #styledForTheme = -1;
4318
5071
  #titleColor;
4319
5072
  #selectedStyle;
4320
- #stateStyles;
5073
+ #stateStyles = /* @__PURE__ */ new Map();
4321
5074
  #padding;
4322
5075
  #portTextStyle;
4323
5076
  #elementSpacing;
@@ -4329,6 +5082,7 @@ var FlowNode = class {
4329
5082
  #data;
4330
5083
  #registeredAnyPropertyChangeCallbacks;
4331
5084
  #registeredPropertyChangeCallbacks;
5085
+ #registeredPropertyEditCallbacks = /* @__PURE__ */ new Map();
4332
5086
  constructor(config) {
4333
5087
  this.#input = new Array();
4334
5088
  this.#output = new Array();
@@ -4381,58 +5135,17 @@ var FlowNode = class {
4381
5135
  this.#position = config?.position === void 0 ? { x: 0, y: 0 } : config.position;
4382
5136
  this.#title = new Text(
4383
5137
  config?.title === void 0 ? "" : config.title,
4384
- TextStyleFallback(config?.style?.title?.textStyle, {
4385
- size: 16,
4386
- weight: "bold" /* Bold */,
4387
- color: Theme.Node.FontColor
4388
- })
5138
+ TextStyleFallback(config?.style?.title?.textStyle, { size: 16, weight: "bold" /* Bold */ })
4389
5139
  );
4390
5140
  this.#subTitle = new Text(
4391
5141
  config?.subTitle === void 0 ? "" : config.subTitle,
4392
- TextStyleFallback(config?.style?.title?.textStyle, {
4393
- size: 10,
4394
- weight: "bold" /* Bold */,
4395
- color: Theme.Node.FontColor
4396
- })
4397
- );
4398
- this.#infoSymbol = new Text(
4399
- "i",
4400
- {
4401
- size: 13,
4402
- weight: "bold" /* Bold */,
4403
- color: Theme.Node.FontColor
4404
- }
5142
+ TextStyleFallback(config?.style?.title?.textStyle, { size: 10, weight: "bold" /* Bold */ })
4405
5143
  );
5144
+ this.#infoSymbol = new Text("i", { size: 13, weight: "bold" /* Bold */ });
4406
5145
  this.#infoText = config?.info ? config?.info : "";
4407
- this.#titleColor = config?.style?.title?.color === void 0 ? "#154050" : config?.style?.title?.color;
4408
5146
  this.#padding = config?.style?.title?.padding === void 0 ? 15 : config?.style?.title?.padding;
4409
- this.#stateStyles = /* @__PURE__ */ new Map();
4410
- this.#stateStyles.set(0 /* Idle */, new BoxStyle(BoxStyleWithFallback(config?.style?.idle, {
4411
- border: { color: Theme.Node.Border.Idle, size: 1 },
4412
- radius: Theme.Node.BorderRadius,
4413
- color: Theme.Node.BackgroundColor
4414
- })));
4415
- this.#stateStyles.set(1 /* MouseOver */, new BoxStyle(BoxStyleWithFallback(config?.style?.mouseOver, {
4416
- border: { color: Theme.Node.Border.MouseOver, size: 1.1 },
4417
- radius: Theme.Node.BorderRadius,
4418
- color: Theme.Node.BackgroundColor
4419
- })));
4420
- this.#stateStyles.set(2 /* Grabbed */, new BoxStyle(BoxStyleWithFallback(config?.style?.grabbed, {
4421
- border: { color: Theme.Node.Border.Grabbed, size: 2 },
4422
- radius: Theme.Node.BorderRadius,
4423
- color: Theme.Node.BackgroundColor
4424
- })));
4425
- this.#selectedStyle = new BoxStyle(BoxStyleWithFallback(config?.style?.selected, {
4426
- border: { color: Theme.Node.Border.Selected, size: 1 },
4427
- radius: Theme.Node.BorderRadius,
4428
- color: Theme.Node.BackgroundColor
4429
- }));
4430
- this.#portTextStyle = new TextStyle({
4431
- size: config?.style?.portText?.size === void 0 ? 14 : config?.style?.portText?.size,
4432
- color: config?.style?.portText?.color === void 0 ? Theme.Node.Port.FontColor : config?.style?.portText?.color,
4433
- font: config?.style?.portText?.font,
4434
- weight: config?.style?.portText?.weight
4435
- });
5147
+ this.#styleConfig = config?.style;
5148
+ this.#applyStyle();
4436
5149
  if (config?.inputs !== void 0) {
4437
5150
  for (let i = 0; i < config.inputs.length; i++) {
4438
5151
  this.addInput(config.inputs[i]);
@@ -4444,12 +5157,8 @@ var FlowNode = class {
4444
5157
  }
4445
5158
  }
4446
5159
  if (config?.widgets !== void 0) {
4447
- for (let i = 0; i < config.widgets.length; i++) {
4448
- const widget = config.widgets[i];
4449
- if (widget.type === void 0) {
4450
- continue;
4451
- }
4452
- this.addWidget(GlobalWidgetFactory.create(this, widget.type, widget.config));
5160
+ for (const widget of config.widgets) {
5161
+ this.addWidget(GlobalWidgetFactory.createFromConfig(this, widget));
4453
5162
  }
4454
5163
  }
4455
5164
  }
@@ -4522,6 +5231,32 @@ var FlowNode = class {
4522
5231
  callbacks[i](oldValue, value);
4523
5232
  }
4524
5233
  }
5234
+ /**
5235
+ * Listens for a person changing the property through a widget, but not
5236
+ * for the property being set from code.
5237
+ */
5238
+ addPropertyEditListener(name, callback) {
5239
+ let callbacks = this.#registeredPropertyEditCallbacks.get(name);
5240
+ if (callbacks === void 0) {
5241
+ callbacks = [];
5242
+ this.#registeredPropertyEditCallbacks.set(name, callbacks);
5243
+ }
5244
+ callbacks.push(callback);
5245
+ }
5246
+ /**
5247
+ * Sets the property as a person's finished edit, so edit listeners hear
5248
+ * it even when the value was already showing.
5249
+ */
5250
+ editProperty(name, value) {
5251
+ this.setProperty(name, value);
5252
+ const callbacks = this.#registeredPropertyEditCallbacks.get(name);
5253
+ if (callbacks === void 0) {
5254
+ return;
5255
+ }
5256
+ for (const callback of [...callbacks]) {
5257
+ callback(value);
5258
+ }
5259
+ }
4525
5260
  getProperty(name) {
4526
5261
  return this.#data[name];
4527
5262
  }
@@ -4810,6 +5545,7 @@ var FlowNode = class {
4810
5545
  // return this.#titleTextStyle.measure(ctx, scale, this.#title);
4811
5546
  // }
4812
5547
  calculateBounds(ctx, camera) {
5548
+ this.#followTheme();
4813
5549
  const tempMeasurement = Zero();
4814
5550
  const doublePadding = this.#padding * 2;
4815
5551
  const screenSpacePosition = Zero();
@@ -4823,25 +5559,21 @@ var FlowNode = class {
4823
5559
  size.x += size.y * 4;
4824
5560
  }
4825
5561
  size.x += doublePadding;
4826
- size.y += doublePadding + this.#elementSpacing * this.#input.length;
4827
- for (let i = 0; i < this.#input.length; i++) {
4828
- const port = this.#input[i];
4829
- this.#portTextStyle.measure(ctx, 1, port.getDisplayName(), tempMeasurement);
4830
- size.y += tempMeasurement.y;
4831
- size.x = Math.max(size.x, tempMeasurement.x + doublePadding);
4832
- }
4833
- size.y += this.#elementSpacing * this.#output.length;
4834
- for (let i = 0; i < this.#output.length; i++) {
4835
- const port = this.#output[i];
5562
+ size.y += doublePadding;
5563
+ for (const port of [...this.#input, ...this.#output]) {
5564
+ if (port.hidden()) {
5565
+ continue;
5566
+ }
4836
5567
  this.#portTextStyle.measure(ctx, 1, port.getDisplayName(), tempMeasurement);
4837
- size.y += tempMeasurement.y;
5568
+ size.y += this.#portRowHeight(port, tempMeasurement.y) + this.#elementSpacing;
4838
5569
  size.x = Math.max(size.x, tempMeasurement.x + doublePadding);
4839
5570
  }
4840
- size.y += this.#elementSpacing * this.#widgets.length;
4841
- for (let i = 0; i < this.#widgets.length; i++) {
4842
- const element = this.#widgets[i];
4843
- const eleSize = element.Size();
4844
- size.y += eleSize.y;
5571
+ for (const widget of this.#widgets) {
5572
+ if (widget.Hidden?.()) {
5573
+ continue;
5574
+ }
5575
+ const eleSize = widget.Size();
5576
+ size.y += eleSize.y + this.#elementSpacing;
4845
5577
  size.x = Math.max(size.x, eleSize.x + doublePadding);
4846
5578
  }
4847
5579
  size.y += this.#elementSpacing;
@@ -4925,7 +5657,7 @@ var FlowNode = class {
4925
5657
  intersection.Node = this;
4926
5658
  }
4927
5659
  for (let i = 0; i < this.#inputPortPositions.Count(); i++) {
4928
- if (InBox(this.#inputPortPositions.At(i), position)) {
5660
+ if (!this.#input[i].hidden() && InBox(this.#inputPortPositions.At(i), position)) {
4929
5661
  intersection.Node = this;
4930
5662
  intersection.PortIndex = i;
4931
5663
  intersection.PortIsInput = true;
@@ -4933,7 +5665,7 @@ var FlowNode = class {
4933
5665
  }
4934
5666
  }
4935
5667
  for (let i = 0; i < this.#outputPortPositions.Count(); i++) {
4936
- if (InBox(this.#outputPortPositions.At(i), position)) {
5668
+ if (!this.#output[i].hidden() && InBox(this.#outputPortPositions.At(i), position)) {
4937
5669
  intersection.Node = this;
4938
5670
  intersection.PortIndex = i;
4939
5671
  intersection.PortIsInput = false;
@@ -4941,10 +5673,12 @@ var FlowNode = class {
4941
5673
  }
4942
5674
  }
4943
5675
  for (let i = 0; i < this.#widgetPositions.Count(); i++) {
4944
- if (InBox(this.#widgetPositions.At(i), position)) {
5676
+ const widget = this.#widgets[i];
5677
+ if (widget !== void 0 && !widget.Hidden?.() && InBox(this.#widgetPositions.At(i), position)) {
4945
5678
  intersection.Node = this;
4946
5679
  intersection.WidgetIndex = i;
4947
- intersection.Widget = this.#widgets[i];
5680
+ intersection.Widget = widget;
5681
+ intersection.WidgetBox = this.#widgetPositions.At(i);
4948
5682
  }
4949
5683
  }
4950
5684
  return intersection;
@@ -4993,6 +5727,12 @@ var FlowNode = class {
4993
5727
  inputPortPosition(index) {
4994
5728
  return this.#inputPortPositions.At(index);
4995
5729
  }
5730
+ widgetPosition(index) {
5731
+ if (index < 0 || index >= this.#widgetPositions.Count()) {
5732
+ return void 0;
5733
+ }
5734
+ return this.#widgetPositions.At(index);
5735
+ }
4996
5736
  inputPort(index) {
4997
5737
  return this.#input[index];
4998
5738
  }
@@ -5008,6 +5748,35 @@ var FlowNode = class {
5008
5748
  outputs() {
5009
5749
  return this.#output.length;
5010
5750
  }
5751
+ #applyStyle() {
5752
+ const style = this.#styleConfig;
5753
+ const titleText = style?.title?.textStyle?.color ?? Theme.Node.Title.FontColor;
5754
+ this.#title.style().setColor(titleText);
5755
+ this.#subTitle.style().setColor(titleText);
5756
+ this.#infoSymbol.style().setColor(titleText);
5757
+ this.#titleColor = style?.title?.color ?? Theme.Node.Title.Color;
5758
+ const box = (config, border, size) => new BoxStyle(BoxStyleWithFallback(config, {
5759
+ border: { color: border, size },
5760
+ radius: Theme.Node.BorderRadius,
5761
+ color: Theme.Node.BackgroundColor
5762
+ }));
5763
+ this.#stateStyles.set(0 /* Idle */, box(style?.idle, Theme.Node.Border.Idle, 1));
5764
+ this.#stateStyles.set(1 /* MouseOver */, box(style?.mouseOver, Theme.Node.Border.MouseOver, 1.1));
5765
+ this.#stateStyles.set(2 /* Grabbed */, box(style?.grabbed, Theme.Node.Border.Grabbed, 2));
5766
+ this.#selectedStyle = box(style?.selected, Theme.Node.Border.Selected, 1);
5767
+ this.#portTextStyle = new TextStyle({
5768
+ size: style?.portText?.size ?? 14,
5769
+ color: style?.portText?.color ?? Theme.Node.Port.FontColor,
5770
+ font: style?.portText?.font,
5771
+ weight: style?.portText?.weight
5772
+ });
5773
+ this.#styledForTheme = themeVersion();
5774
+ }
5775
+ #followTheme() {
5776
+ if (this.#styledForTheme !== themeVersion()) {
5777
+ this.#applyStyle();
5778
+ }
5779
+ }
5011
5780
  #calculateStyle(state) {
5012
5781
  if (this.#selected && state === 0 /* Idle */) {
5013
5782
  return this.#selectedStyle;
@@ -5024,9 +5793,22 @@ var FlowNode = class {
5024
5793
  clearMessages() {
5025
5794
  this.#messages = [];
5026
5795
  }
5796
+ #portRowHeight(port, textHeight) {
5797
+ const slots = port.slots();
5798
+ return slots > 1 ? Math.max(textHeight, slots * ARRAY_SLOT_PITCH) : textHeight;
5799
+ }
5027
5800
  hasBeenLaidOut() {
5028
5801
  return this.#laidOut;
5029
5802
  }
5803
+ /**
5804
+ * Throws away where the ports were last placed, so wires wait for the
5805
+ * next draw instead of pointing at the old spots.
5806
+ */
5807
+ invalidateLayout() {
5808
+ this.#laidOut = false;
5809
+ this.#portOffsets = { input: [], output: [] };
5810
+ requestRender();
5811
+ }
5030
5812
  #laidOut = false;
5031
5813
  render(ctx, camera, state, mousePosition, postProcess) {
5032
5814
  this.#laidOut = true;
@@ -5064,6 +5846,9 @@ var FlowNode = class {
5064
5846
  [nodeStyle.radius() * camera.zoom, nodeStyle.radius() * camera.zoom, 0, 0]
5065
5847
  );
5066
5848
  ctx.fill();
5849
+ if (Theme.Node.Bevel !== null) {
5850
+ drawBevel(ctx, nodeBounds, camera.zoom, Theme.Node.Bevel, false);
5851
+ }
5067
5852
  const titlePosition = VectorPool.get();
5068
5853
  titlePosition.x = nodeBounds.Position.x + nodeBounds.Size.x / 2;
5069
5854
  titlePosition.y = nodeBounds.Position.y + scaledPadding + titleSize.y / 2;
@@ -5124,8 +5909,12 @@ var FlowNode = class {
5124
5909
  for (let i = 0; i < this.#input.length; i++) {
5125
5910
  ctx.textAlign = "left" /* Left */;
5126
5911
  const port = this.#input[i];
5912
+ if (port.hidden()) {
5913
+ this.#inputPortPositions.Push({ Position: { x: nodeBounds.Position.x, y: startY }, Size: { x: 0, y: 0 } });
5914
+ continue;
5915
+ }
5127
5916
  this.#portTextStyle.measure(ctx, 1, port.getDisplayName(), tempMeasurement);
5128
- const portHeight = tempMeasurement.y * camera.zoom;
5917
+ const portHeight = this.#portRowHeight(port, tempMeasurement.y) * camera.zoom;
5129
5918
  const position = VectorPool.get();
5130
5919
  position.x = nodeBounds.Position.x;
5131
5920
  position.y = startY + portHeight / 2;
@@ -5138,6 +5927,10 @@ var FlowNode = class {
5138
5927
  ctx.textAlign = "right" /* Right */;
5139
5928
  for (let i = 0; i < this.#output.length; i++) {
5140
5929
  const port = this.#output[i];
5930
+ if (port.hidden()) {
5931
+ this.#outputPortPositions.Push({ Position: { x: rightSide, y: startY }, Size: { x: 0, y: 0 } });
5932
+ continue;
5933
+ }
5141
5934
  this.#portTextStyle.measure(ctx, 1, port.getDisplayName(), tempMeasurement);
5142
5935
  const portHeight = tempMeasurement.y * camera.zoom;
5143
5936
  const position = VectorPool.get();
@@ -5150,13 +5943,18 @@ var FlowNode = class {
5150
5943
  }
5151
5944
  for (let i = 0; i < this.#widgets.length; i++) {
5152
5945
  const widget = this.#widgets[i];
5946
+ if (widget.Hidden?.()) {
5947
+ this.#widgetPositions.Push({ Position: { x: nodeBounds.Position.x, y: startY }, Size: { x: 0, y: 0 } });
5948
+ continue;
5949
+ }
5153
5950
  const widgetSize = widget.Size();
5154
- const scaledWidgetWidth = widgetSize.x * camera.zoom;
5155
- const position = VectorPool.get();
5156
- position.x = nodeBounds.Position.x + (nodeBounds.Size.x - scaledWidgetWidth) / 2;
5157
- position.y = startY;
5158
- this.#widgetPositions.Push(widget.Draw(ctx, position, camera.zoom, mousePosition));
5159
- startY += widgetSize.y * camera.zoom + scaledElementSpacing;
5951
+ const box = {
5952
+ Position: { x: nodeBounds.Position.x + scaledPadding, y: startY },
5953
+ Size: { x: nodeBounds.Size.x - scaledPadding * 2, y: widgetSize.y * camera.zoom }
5954
+ };
5955
+ widget.Draw(ctx, box, camera.zoom, mousePosition);
5956
+ this.#widgetPositions.Push(box);
5957
+ startY += box.Size.y + scaledElementSpacing;
5160
5958
  }
5161
5959
  const messageStart = VectorPool.get();
5162
5960
  messageStart.x = nodeBounds.Position.x + nodeBounds.Size.x / 2;
@@ -5193,6 +5991,13 @@ var FlowNode = class {
5193
5991
  record(this.#outputPortPositions, this.#portOffsets.output);
5194
5992
  }
5195
5993
  layoutPortsWithoutDrawing(bounds, zoom) {
5994
+ if (!this.placePorts(bounds, zoom)) {
5995
+ return false;
5996
+ }
5997
+ this.#widgetPositions.Clear();
5998
+ return true;
5999
+ }
6000
+ placePorts(bounds, zoom) {
5196
6001
  if (this.#portOffsets.input.length === 0 && this.#portOffsets.output.length === 0) {
5197
6002
  return false;
5198
6003
  }
@@ -5214,7 +6019,6 @@ var FlowNode = class {
5214
6019
  };
5215
6020
  project(this.#portOffsets.input, this.#inputPortPositions);
5216
6021
  project(this.#portOffsets.output, this.#outputPortPositions);
5217
- this.#widgetPositions.Clear();
5218
6022
  return true;
5219
6023
  }
5220
6024
  };
@@ -5406,33 +6210,33 @@ var CodeBlockEntry = class {
5406
6210
  }
5407
6211
  render(ctx, position, scale, maxWidth) {
5408
6212
  this.#calculateLayout(ctx, maxWidth);
5409
- let padding = Theme.Note.CodeBlock.Padding * scale;
5410
- let max = 0;
5411
- for (let i = 0; i < this.#calculatedEntries.Count(); i++) {
5412
- const pos = this.#calculatedPositions.At(i);
5413
- max = Math.max(max, pos.y * scale);
5414
- }
5415
- const y = position.y + max + scale * 5;
6213
+ const padding = Theme.Note.CodeBlock.Padding * scale;
6214
+ const lineCount = this.#calculatedEntries.Count();
6215
+ const lastTop = lineCount === 0 ? 0 : this.#calculatedPositions.At(lineCount - 1).y;
6216
+ const contentHeight = lineCount === 0 ? 0 : (lastTop + this.#text.style().getSize()) * scale;
6217
+ const height2 = contentHeight + padding * 2;
5416
6218
  ctx.fillStyle = Theme.Note.CodeBlock.BackgroundColor;
5417
6219
  ctx.beginPath();
5418
6220
  ctx.roundRect(
5419
6221
  position.x,
5420
6222
  position.y,
5421
6223
  maxWidth * scale,
5422
- max + padding * 2,
6224
+ height2,
5423
6225
  Theme.Note.CodeBlock.BorderRadius * scale
5424
6226
  );
5425
6227
  ctx.fill();
5426
- for (let i = 0; i < this.#calculatedEntries.Count(); i++) {
6228
+ const baseline = ctx.textBaseline;
6229
+ ctx.textBaseline = "top";
6230
+ for (let i = 0; i < lineCount; i++) {
5427
6231
  const entry = this.#calculatedEntries.At(i);
5428
6232
  const pos = this.#calculatedPositions.At(i);
5429
6233
  entry.render(ctx, scale, {
5430
6234
  x: pos.x * scale + position.x + padding,
5431
6235
  y: pos.y * scale + position.y + padding
5432
6236
  });
5433
- max = Math.max(max, pos.y * scale);
5434
6237
  }
5435
- return max + padding * 2;
6238
+ ctx.textBaseline = baseline;
6239
+ return height2;
5436
6240
  }
5437
6241
  };
5438
6242
  var UnorderedListMarkdownEntry = class {
@@ -5810,9 +6614,10 @@ var MarkdownSyntaxParser = class {
5810
6614
  this.#inc();
5811
6615
  this.#inc();
5812
6616
  this.#inc();
5813
- const codeBlockText = this.#originalText.substring(start, end);
6617
+ const codeBlockText = this.#originalText.substring(start, end).replace(/\r?\n$/, "");
5814
6618
  const entryText = new Text(codeBlockText);
5815
6619
  this.#assignStandardStyling([entryText]);
6620
+ entryText.setColor(Theme.Note.CodeBlock.FontColor);
5816
6621
  return new CodeBlockEntry(entryText);
5817
6622
  }
5818
6623
  parse() {
@@ -5867,6 +6672,7 @@ var BOX_SIZE = 10;
5867
6672
  var FlowNote = class {
5868
6673
  #originalText;
5869
6674
  #document;
6675
+ #parsedForTheme = -1;
5870
6676
  #width;
5871
6677
  #edittingStyle;
5872
6678
  // Runtime ================================================================
@@ -5909,6 +6715,7 @@ var FlowNote = class {
5909
6715
  setText(text) {
5910
6716
  this.#originalText = text;
5911
6717
  this.#document = BuildMarkdown(this.#originalText);
6718
+ this.#parsedForTheme = themeVersion();
5912
6719
  for (let i = 0; i < this.#contentChangeCallbacks.length; i++) {
5913
6720
  this.#contentChangeCallbacks[i](this, text);
5914
6721
  }
@@ -5996,6 +6803,10 @@ var FlowNote = class {
5996
6803
  }
5997
6804
  camera.graphSpaceToScreenSpace(this.#position, this.#tempPosition);
5998
6805
  CopyVector2(this.#lastRenderedBox.Position, this.#tempPosition);
6806
+ if (this.#parsedForTheme !== themeVersion()) {
6807
+ this.#document = BuildMarkdown(this.#originalText);
6808
+ this.#parsedForTheme = themeVersion();
6809
+ }
5999
6810
  const startY = this.#tempPosition.y;
6000
6811
  const lineSpacing = Theme.Note.EntrySpacing * camera.zoom;
6001
6812
  ctx.textAlign = "left" /* Left */;
@@ -6691,13 +7502,15 @@ var Minimap = class {
6691
7502
  // src/graph.ts
6692
7503
  function BuildBackgroundRenderer(backgroundColor) {
6693
7504
  return (canvas, context, position, scale) => {
6694
- context.fillStyle = backgroundColor;
7505
+ context.fillStyle = backgroundColor ?? Theme.Graph.BackgroundColor;
6695
7506
  context.fillRect(0, 0, canvas.width, canvas.height);
6696
- const alpha = Math.round(Clamp01(scale - 0.3) * 255);
7507
+ const alpha = Clamp01(scale - 0.3);
6697
7508
  if (alpha <= 0) {
6698
7509
  return;
6699
7510
  }
6700
- context.fillStyle = `rgba(41, 54, 57, ${alpha})`;
7511
+ context.save();
7512
+ context.globalAlpha = alpha;
7513
+ context.fillStyle = Theme.Graph.DotColor;
6701
7514
  const spacing = 100;
6702
7515
  const pi2 = 2 * Math.PI;
6703
7516
  const dotScale = 2 * scale;
@@ -6710,6 +7523,7 @@ function BuildBackgroundRenderer(backgroundColor) {
6710
7523
  context.fill();
6711
7524
  }
6712
7525
  }
7526
+ context.restore();
6713
7527
  };
6714
7528
  }
6715
7529
  var contextMenuGroup = "graph-context-menu";
@@ -6760,9 +7574,9 @@ var GraphView = class {
6760
7574
  const results = {};
6761
7575
  for (let i = 0; i < this.#subsystems.length; i++) {
6762
7576
  TimeExecution("Render_Subsystem_" + i, () => {
6763
- let results2 = this.#subsystems[i].render(ctx, camera, mousePosition);
6764
- if (results2?.cursorStyle) {
6765
- results2.cursorStyle = results2?.cursorStyle;
7577
+ const layer = this.#subsystems[i].render(ctx, camera, mousePosition);
7578
+ if (layer?.cursorStyle) {
7579
+ results.cursorStyle = layer.cursorStyle;
6766
7580
  }
6767
7581
  });
6768
7582
  }
@@ -6822,8 +7636,7 @@ var NodeFlowGraph = class {
6822
7636
  if (config?.backgroundRenderer !== void 0) {
6823
7637
  this.#backgroundRenderer = config?.backgroundRenderer;
6824
7638
  } else {
6825
- const backgroundColor = config?.backgroundColor === void 0 ? Theme.Graph.BackgroundColor : config.backgroundColor;
6826
- this.#backgroundRenderer = BuildBackgroundRenderer(backgroundColor);
7639
+ this.#backgroundRenderer = BuildBackgroundRenderer(config?.backgroundColor);
6827
7640
  }
6828
7641
  this.#minimap = new Minimap(config?.minimap);
6829
7642
  this.#mainNodeSubsystem.setConnectionReleasedHandler((dangling) => {
@@ -7241,10 +8054,12 @@ var NodeFlowGraph = class {
7241
8054
  this.#cursor = "default" /* Default */;
7242
8055
  TimeExecution("Render_Background", this.#renderBackground.bind(this));
7243
8056
  TimeExecution("Render_View_" + this.#currentView, () => {
7244
- let results = this.currentView().render(this.#ctx, this.#camera, this.#mousePosition);
7245
- if (results?.cursorStyle) {
7246
- this.#cursor = results?.cursorStyle;
7247
- }
8057
+ placeEditsDuring(this.#canvas, () => {
8058
+ let results = this.currentView().render(this.#ctx, this.#camera, this.#mousePosition);
8059
+ if (results?.cursorStyle) {
8060
+ this.#cursor = results?.cursorStyle;
8061
+ }
8062
+ });
7248
8063
  });
7249
8064
  TimeExecution("Render_Minimap", () => {
7250
8065
  this.#minimap.render(this.#ctx, this.#canvas, this.#camera, this.#mainNodeSubsystem.getNodes());
@@ -7289,11 +8104,15 @@ export {
7289
8104
  ColorWidget,
7290
8105
  Connection,
7291
8106
  ContextMenuItemState,
8107
+ CursorStyle,
7292
8108
  DefaultConnectionRenderer,
8109
+ DefaultTheme,
7293
8110
  FlowNode,
7294
8111
  FlowNote,
7295
8112
  GlobalWidgetFactory,
8113
+ GraphView,
7296
8114
  ImageWidget,
8115
+ InBox,
7297
8116
  MessageType,
7298
8117
  Minimap,
7299
8118
  NodeFactory,
@@ -7303,16 +8122,28 @@ export {
7303
8122
  Port,
7304
8123
  PortType,
7305
8124
  Publisher,
8125
+ RowWidget,
7306
8126
  SliderWidget,
7307
8127
  StringWidget,
7308
8128
  TextWidget,
7309
8129
  Theme,
7310
8130
  ToggleWidget,
8131
+ ValueWidget,
8132
+ WidgetBase,
7311
8133
  WidgetFactory,
8134
+ drawField,
8135
+ drawFieldBox,
8136
+ drawFieldText,
8137
+ editInPlace,
8138
+ formatNumber,
8139
+ onThemeChange,
7312
8140
  portConfigTypes,
7313
8141
  portsCompatible,
7314
8142
  renderContinuously,
7315
8143
  requestRender,
7316
- typeSetsCompatible
8144
+ setTheme,
8145
+ themeVersion,
8146
+ typeSetsCompatible,
8147
+ widgetFont
7317
8148
  };
7318
8149
  //# sourceMappingURL=index.mjs.map