@elicdavis/node-flow 0.1.20 → 0.2.0

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