@elicdavis/node-flow 0.1.16 → 0.1.18

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.
package/dist/index.js CHANGED
@@ -30,6 +30,7 @@ __export(index_exports, {
30
30
  GlobalWidgetFactory: () => GlobalWidgetFactory,
31
31
  ImageWidget: () => ImageWidget,
32
32
  MessageType: () => MessageType,
33
+ Minimap: () => Minimap,
33
34
  NodeFactory: () => NodeFactory,
34
35
  NodeFlowGraph: () => NodeFlowGraph,
35
36
  NodeState: () => NodeState,
@@ -43,7 +44,11 @@ __export(index_exports, {
43
44
  Theme: () => Theme,
44
45
  ToggleWidget: () => ToggleWidget,
45
46
  WidgetFactory: () => WidgetFactory,
46
- portsCompatible: () => portsCompatible
47
+ portConfigTypes: () => portConfigTypes,
48
+ portsCompatible: () => portsCompatible,
49
+ renderContinuously: () => renderContinuously,
50
+ requestRender: () => requestRender,
51
+ typeSetsCompatible: () => typeSetsCompatible
47
52
  });
48
53
  module.exports = __toCommonJS(index_exports);
49
54
 
@@ -109,6 +114,14 @@ var Theme = {
109
114
  FontColor: "#afb9bb",
110
115
  DisabledFontColor: "#4a585c"
111
116
  },
117
+ Minimap: {
118
+ BackgroundColor: "rgba(10, 16, 18, 0.85)",
119
+ BorderColor: "#1c1c1c",
120
+ BorderRadius: 4,
121
+ Padding: 6,
122
+ NodeColor: "#6b7f85",
123
+ ViewportColor: "rgba(175, 185, 187, 0.25)"
124
+ },
112
125
  Note: {
113
126
  FontColor: "#afb9bb",
114
127
  FontSize: 16,
@@ -650,6 +663,43 @@ var ContextMenu = class _ContextMenu {
650
663
  }
651
664
  };
652
665
 
666
+ // src/render_scheduler.ts
667
+ var subscribers = /* @__PURE__ */ new Set();
668
+ var frameQueued = false;
669
+ var continuous = 0;
670
+ var canSchedule = () => typeof requestAnimationFrame === "function";
671
+ function onRenderRequested(redraw) {
672
+ subscribers.add(redraw);
673
+ return () => {
674
+ subscribers.delete(redraw);
675
+ };
676
+ }
677
+ function requestRender() {
678
+ if (frameQueued || !canSchedule()) {
679
+ return;
680
+ }
681
+ frameQueued = true;
682
+ requestAnimationFrame(() => {
683
+ frameQueued = false;
684
+ subscribers.forEach((redraw) => redraw());
685
+ if (continuous > 0) {
686
+ requestRender();
687
+ }
688
+ });
689
+ }
690
+ function renderContinuously() {
691
+ continuous++;
692
+ requestRender();
693
+ let released = false;
694
+ return () => {
695
+ if (released) {
696
+ return;
697
+ }
698
+ released = true;
699
+ continuous--;
700
+ };
701
+ }
702
+
653
703
  // src/input.ts
654
704
  var LONG_PRESS_MS = 500;
655
705
  var LONG_PRESS_SLOP = 12;
@@ -683,15 +733,19 @@ var MouseObserver = class {
683
733
  this.#lastTouch = Zero();
684
734
  this.#lastMousePosition = Zero();
685
735
  ele.style.touchAction = "none";
686
- ele.addEventListener("mousedown", this.#down.bind(this), false);
687
- ele.addEventListener("touchstart", this.#touchStart.bind(this), { passive: false });
688
- document.addEventListener("mouseup", this.#up.bind(this), false);
689
- document.addEventListener("touchend", this.#touchEnd.bind(this), false);
690
- document.addEventListener("touchcancel", this.#touchEnd.bind(this), false);
691
- ele.addEventListener("mousemove", this.#move.bind(this), false);
692
- ele.addEventListener("touchmove", this.#touchMove.bind(this), { passive: false });
693
- ele.addEventListener("wheel", this.#wheel.bind(this), { passive: false });
694
- ele.addEventListener("drop", (ev) => {
736
+ const redraws = (handler) => (event) => {
737
+ handler(event);
738
+ requestRender();
739
+ };
740
+ ele.addEventListener("mousedown", redraws(this.#down.bind(this)), false);
741
+ ele.addEventListener("touchstart", redraws(this.#touchStart.bind(this)), { passive: false });
742
+ document.addEventListener("mouseup", redraws(this.#up.bind(this)), false);
743
+ document.addEventListener("touchend", redraws(this.#touchEnd.bind(this)), false);
744
+ document.addEventListener("touchcancel", redraws(this.#touchEnd.bind(this)), false);
745
+ ele.addEventListener("mousemove", redraws(this.#move.bind(this)), false);
746
+ ele.addEventListener("touchmove", redraws(this.#touchMove.bind(this)), { passive: false });
747
+ ele.addEventListener("wheel", redraws(this.#wheel.bind(this)), { passive: false });
748
+ ele.addEventListener("drop", redraws((ev) => {
695
749
  ev.preventDefault();
696
750
  if (ev.dataTransfer?.items) {
697
751
  [...ev.dataTransfer.items].forEach((item, i) => {
@@ -703,18 +757,18 @@ var MouseObserver = class {
703
757
  }
704
758
  });
705
759
  }
706
- });
707
- ele.addEventListener("dragover", (ev) => {
760
+ }));
761
+ ele.addEventListener("dragover", redraws((ev) => {
708
762
  ev.preventDefault();
709
763
  this.#moveCallback(this.#mousePos(ev));
710
- });
711
- ele.addEventListener("contextmenu", (evt) => {
764
+ }));
765
+ ele.addEventListener("contextmenu", redraws((evt) => {
712
766
  evt.preventDefault();
713
767
  if (Date.now() - this.#lastTouchAt < SUPPRESS_MOUSE_MS) {
714
768
  return;
715
769
  }
716
770
  this.#contextMenu(this.#mousePos(evt));
717
- }, false);
771
+ }), false);
718
772
  }
719
773
  #mousePos(event) {
720
774
  const rect = this.#ele.getBoundingClientRect();
@@ -811,6 +865,7 @@ var MouseObserver = class {
811
865
  this.#clicked = false;
812
866
  this.#clickStop();
813
867
  this.#contextMenu({ x, y });
868
+ requestRender();
814
869
  }, LONG_PRESS_MS);
815
870
  this.#clicked = true;
816
871
  this.#moveCallback(pos);
@@ -1247,6 +1302,7 @@ var Text = class _Text {
1247
1302
  if (!document.fonts.check(`16px "${this.#style.getFont()}"`)) {
1248
1303
  document.fonts.addEventListener("loadingdone", () => {
1249
1304
  this.#measured = false;
1305
+ requestRender();
1250
1306
  });
1251
1307
  }
1252
1308
  }
@@ -1458,12 +1514,22 @@ function portsCompatible(a, b) {
1458
1514
  if (!a || !b) {
1459
1515
  return false;
1460
1516
  }
1461
- const left = a.acceptedTypes();
1462
- const right = b.acceptedTypes();
1463
- if (left.length === 0 || right.length === 0) {
1517
+ return typeSetsCompatible(a.acceptedTypes(), b.acceptedTypes());
1518
+ }
1519
+ function typeSetsCompatible(a, b) {
1520
+ if (a.length === 0 || b.length === 0) {
1464
1521
  return true;
1465
1522
  }
1466
- return left.some((type) => right.includes(type));
1523
+ return a.some((type) => b.includes(type));
1524
+ }
1525
+ function portConfigTypes(config) {
1526
+ if (config.anyType === true) {
1527
+ return [];
1528
+ }
1529
+ if (config.acceptedTypes !== void 0 && config.acceptedTypes.length > 0) {
1530
+ return config.acceptedTypes;
1531
+ }
1532
+ return [config.type === void 0 ? "" : config.type];
1467
1533
  }
1468
1534
  function fallbackColor(type, s) {
1469
1535
  let value = 0;
@@ -1578,6 +1644,7 @@ var Port = class {
1578
1644
  this.#filledStyle.fillColor = fallbackColor(this.#dataType, 1);
1579
1645
  }
1580
1646
  this.#dataTypePopupElement = this.#buildTooltip();
1647
+ requestRender();
1581
1648
  }
1582
1649
  setDataType(type) {
1583
1650
  if (this.#dataType === type) {
@@ -1831,6 +1898,7 @@ var Popup = class {
1831
1898
  #buttonCSS;
1832
1899
  // Runtime ================================================================
1833
1900
  #popup;
1901
+ #keyHandler;
1834
1902
  constructor(config) {
1835
1903
  this.#title = config.title;
1836
1904
  this.#options = config.options;
@@ -1838,14 +1906,25 @@ var Popup = class {
1838
1906
  this.#onClose = config.onClose;
1839
1907
  this.#buttonCSS = config.buttonCSS === void 0 ? "flex-grow: 1; margin-right: 8px;" : config.buttonCSS;
1840
1908
  this.#popup = null;
1909
+ this.#keyHandler = null;
1841
1910
  }
1842
1911
  Hide() {
1843
1912
  if (this.#popup === null) {
1844
1913
  return;
1845
1914
  }
1915
+ if (this.#keyHandler !== null) {
1916
+ document.removeEventListener("keydown", this.#keyHandler, true);
1917
+ this.#keyHandler = null;
1918
+ }
1846
1919
  document.body.removeChild(this.#popup);
1847
1920
  this.#popup = null;
1848
1921
  }
1922
+ #close(option) {
1923
+ this.Hide();
1924
+ if (this.#onClose !== void 0) {
1925
+ this.#onClose(option);
1926
+ }
1927
+ }
1849
1928
  Show() {
1850
1929
  this.#popup = document.createElement("div");
1851
1930
  this.#popup.style.cssText = "position:absolute;top:0;width:100%;display:flex;height:100%;justify-content:center;align-items:center;background-color:#00000070;";
@@ -1867,14 +1946,20 @@ var Popup = class {
1867
1946
  const button = document.createElement("button");
1868
1947
  button.style.cssText = this.#buttonCSS;
1869
1948
  button.textContent = this.#options[i];
1870
- button.onclick = () => {
1871
- this.Hide();
1872
- if (this.#onClose !== void 0) {
1873
- this.#onClose(this.#options[i]);
1874
- }
1875
- };
1949
+ button.onclick = () => this.#close(this.#options[i]);
1876
1950
  buttonContainer.appendChild(button);
1877
1951
  }
1952
+ this.#keyHandler = (e) => {
1953
+ if (e.key === "Enter") {
1954
+ e.preventDefault();
1955
+ this.#close(this.#options.length > 0 ? this.#options[0] : null);
1956
+ } else if (e.key === "Escape") {
1957
+ e.preventDefault();
1958
+ this.#close(null);
1959
+ }
1960
+ e.stopPropagation();
1961
+ };
1962
+ document.addEventListener("keydown", this.#keyHandler, true);
1878
1963
  }
1879
1964
  };
1880
1965
 
@@ -1983,8 +2068,8 @@ var NumberWidget = class {
1983
2068
  },
1984
2069
  text: { color: Theme.Widget.FontColor }
1985
2070
  }));
1986
- this.#callback = config?.callback;
1987
2071
  this.Set(startingWidgetValue(node, this.#nodeProperty, config?.value, 0));
2072
+ this.#callback = config?.callback;
1988
2073
  if (this.#nodeProperty !== void 0) {
1989
2074
  this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
1990
2075
  this.Set(newVal);
@@ -2426,8 +2511,8 @@ var StringWidget = class {
2426
2511
  },
2427
2512
  text: { color: Theme.Widget.FontColor }
2428
2513
  }));
2429
- this.#callback = config?.callback;
2430
2514
  this.Set(startingWidgetValue(node, this.#nodeProperty, config?.value, ""));
2515
+ this.#callback = config?.callback;
2431
2516
  if (this.#nodeProperty !== void 0) {
2432
2517
  this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
2433
2518
  this.Set(newVal);
@@ -2592,8 +2677,8 @@ var ToggleWidget = class {
2592
2677
  lightBorderColor: config?.disabledStyle?.lightBorderColor,
2593
2678
  lightColor: config?.disabledStyle?.lightColor === void 0 ? "#004400" : config?.enabledStyle?.lightColor
2594
2679
  });
2595
- this.#callback = config?.callback;
2596
2680
  this.Set(startingWidgetValue(node, this.#nodeProperty, config?.value, false));
2681
+ this.#callback = config?.callback;
2597
2682
  if (this.#nodeProperty !== void 0) {
2598
2683
  this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
2599
2684
  this.Set(newVal);
@@ -2656,6 +2741,7 @@ var ImageWidget = class {
2656
2741
  img.src = url;
2657
2742
  img.onload = () => {
2658
2743
  this.#image = img;
2744
+ requestRender();
2659
2745
  };
2660
2746
  img.onerror = (event) => {
2661
2747
  console.log("error loading image:", url, event);
@@ -3236,6 +3322,13 @@ function TimeExecution(name, callback) {
3236
3322
  }
3237
3323
 
3238
3324
  // src/nodes/publisher.ts
3325
+ function offersMatchingPort(config, filter) {
3326
+ const ports = filter.needs === "INPUT" /* Input */ ? config.inputs : config.outputs;
3327
+ if (ports === void 0) {
3328
+ return false;
3329
+ }
3330
+ return ports.some((port) => typeSetsCompatible(portConfigTypes(port), filter.types));
3331
+ }
3239
3332
  var Publisher = class {
3240
3333
  #name;
3241
3334
  #description;
@@ -3261,19 +3354,23 @@ var Publisher = class {
3261
3354
  unregister(nodeType) {
3262
3355
  return this.#registeredNodes.delete(nodeType);
3263
3356
  }
3264
- #recurseBuildMenu(graph, name, subMenu, position) {
3357
+ #recurseBuildMenu(graph, name, subMenu, position, filter) {
3265
3358
  const items = [];
3266
3359
  const subMenus = /* @__PURE__ */ new Map();
3267
3360
  for (let [key, nodeConfig] of subMenu) {
3268
3361
  const slashIndex = key.indexOf("/");
3269
3362
  const bracketIndex = key.indexOf("[");
3270
3363
  if (slashIndex === -1 || bracketIndex !== -1 && bracketIndex < slashIndex) {
3364
+ if (filter !== void 0 && !offersMatchingPort(nodeConfig, filter)) {
3365
+ continue;
3366
+ }
3271
3367
  items.push({
3272
3368
  name: key,
3273
3369
  callback: () => {
3274
3370
  const node = new FlowNode(nodeConfig);
3275
3371
  node.setPosition(position);
3276
3372
  graph.addNode(node);
3373
+ filter?.onCreated?.(node);
3277
3374
  }
3278
3375
  });
3279
3376
  } else {
@@ -3288,7 +3385,11 @@ var Publisher = class {
3288
3385
  }
3289
3386
  const menus = [];
3290
3387
  for (let [key, nodes] of subMenus) {
3291
- menus.push(this.#recurseBuildMenu(graph, key, nodes, position));
3388
+ const built = this.#recurseBuildMenu(graph, key, nodes, position, filter);
3389
+ if (filter !== void 0 && built.items?.length === 0 && built.subMenus?.length === 0) {
3390
+ continue;
3391
+ }
3392
+ menus.push(built);
3292
3393
  }
3293
3394
  return {
3294
3395
  name,
@@ -3296,8 +3397,8 @@ var Publisher = class {
3296
3397
  subMenus: menus
3297
3398
  };
3298
3399
  }
3299
- contextMenu(graph, position) {
3300
- return this.#recurseBuildMenu(graph, this.#name, this.#registeredNodes, position);
3400
+ contextMenu(graph, position, filter) {
3401
+ return this.#recurseBuildMenu(graph, this.#name, this.#registeredNodes, position, filter);
3301
3402
  }
3302
3403
  create(nodeType) {
3303
3404
  const config = this.#registeredNodes.get(nodeType);
@@ -3352,18 +3453,22 @@ var NodeFactory = class {
3352
3453
  }
3353
3454
  return node;
3354
3455
  }
3355
- newNodeSubmenus(graph, position) {
3456
+ newNodeSubmenus(graph, position, filter) {
3356
3457
  const menus = [];
3357
3458
  for (let [_, publisher] of this.#registeredPublishers) {
3358
- menus.push(publisher.contextMenu(graph, position));
3459
+ const built = publisher.contextMenu(graph, position, filter);
3460
+ if (filter !== void 0 && built.items?.length === 0 && built.subMenus?.length === 0) {
3461
+ continue;
3462
+ }
3463
+ menus.push(built);
3359
3464
  }
3360
3465
  return menus;
3361
3466
  }
3362
- openMenu(graph, position) {
3467
+ openMenu(graph, position, filter) {
3363
3468
  return {
3364
3469
  name: "New Node",
3365
3470
  group: nodeFlowGroup,
3366
- subMenus: this.newNodeSubmenus(graph, position)
3471
+ subMenus: this.newNodeSubmenus(graph, position, filter)
3367
3472
  };
3368
3473
  }
3369
3474
  };
@@ -3502,6 +3607,9 @@ var NodeSubsystem2 = class {
3502
3607
  CopyVector2(this.#boxSelectEnd_graphSpace, this.#boxSelectStart_graphSpace);
3503
3608
  this.#boxSelect = true;
3504
3609
  }
3610
+ if (!hoveringSomething && !ctrlKey) {
3611
+ this.unselectAllNodes();
3612
+ }
3505
3613
  return hoveringSomething || ctrlKey;
3506
3614
  }
3507
3615
  if (this.#portHovering.InputPort) {
@@ -3561,6 +3669,102 @@ var NodeSubsystem2 = class {
3561
3669
  }
3562
3670
  }
3563
3671
  }
3672
+ connectionsWithin(nodes) {
3673
+ const within = [];
3674
+ for (const connection of this.#connections) {
3675
+ const outNode = connection.outNode();
3676
+ const inNode = connection.inNode();
3677
+ if (outNode === null || inNode === null) {
3678
+ continue;
3679
+ }
3680
+ const from = nodes.indexOf(outNode);
3681
+ const to = nodes.indexOf(inNode);
3682
+ if (from === -1 || to === -1) {
3683
+ continue;
3684
+ }
3685
+ const outPort = connection.outPort();
3686
+ const inPort = connection.inPort();
3687
+ if (outPort === null || inPort === null) {
3688
+ continue;
3689
+ }
3690
+ within.push({
3691
+ fromNode: from,
3692
+ fromPort: this.#outputPortIndex(outNode, outPort),
3693
+ toNode: to,
3694
+ toPort: this.#inputPortIndex(inNode, inPort)
3695
+ });
3696
+ }
3697
+ return within;
3698
+ }
3699
+ #onConnectionReleased = null;
3700
+ setConnectionReleasedHandler(handler) {
3701
+ this.#onConnectionReleased = handler;
3702
+ }
3703
+ #describeDanglingConnection(conn) {
3704
+ const outNode = conn.outNode();
3705
+ const inNode = conn.inNode();
3706
+ if (outNode !== null && inNode === null) {
3707
+ const from = conn.outPort();
3708
+ if (from === null) {
3709
+ return null;
3710
+ }
3711
+ const fromIndex = this.#outputPortIndex(outNode, from);
3712
+ return {
3713
+ needs: "INPUT" /* Input */,
3714
+ types: from.acceptedTypes(),
3715
+ connectTo: (node) => {
3716
+ const to = this.#firstCompatiblePort(node, "INPUT" /* Input */, from);
3717
+ if (to !== -1 && fromIndex !== -1) {
3718
+ this.connectNodes(outNode, fromIndex, node, to);
3719
+ }
3720
+ }
3721
+ };
3722
+ }
3723
+ if (inNode !== null && outNode === null) {
3724
+ const to = conn.inPort();
3725
+ if (to === null) {
3726
+ return null;
3727
+ }
3728
+ const toIndex = this.#inputPortIndex(inNode, to);
3729
+ return {
3730
+ needs: "OUTPUT" /* Output */,
3731
+ types: to.acceptedTypes(),
3732
+ connectTo: (node) => {
3733
+ const from = this.#firstCompatiblePort(node, "OUTPUT" /* Output */, to);
3734
+ if (from !== -1 && toIndex !== -1) {
3735
+ this.connectNodes(node, from, inNode, toIndex);
3736
+ }
3737
+ }
3738
+ };
3739
+ }
3740
+ return null;
3741
+ }
3742
+ #outputPortIndex(node, port) {
3743
+ for (let i = 0; i < node.outputs(); i++) {
3744
+ if (node.outputPort(i) === port) {
3745
+ return i;
3746
+ }
3747
+ }
3748
+ return -1;
3749
+ }
3750
+ #inputPortIndex(node, port) {
3751
+ for (let i = 0; i < node.inputs(); i++) {
3752
+ if (node.inputPort(i) === port) {
3753
+ return i;
3754
+ }
3755
+ }
3756
+ return -1;
3757
+ }
3758
+ #firstCompatiblePort(node, side, against) {
3759
+ const count = side === "INPUT" /* Input */ ? node.inputs() : node.outputs();
3760
+ for (let i = 0; i < count; i++) {
3761
+ const port = side === "INPUT" /* Input */ ? node.inputPort(i) : node.outputPort(i);
3762
+ if (portsCompatible(port, against)) {
3763
+ return i;
3764
+ }
3765
+ }
3766
+ return -1;
3767
+ }
3564
3768
  clickEnd() {
3565
3769
  for (let i = 0; i < this.#nodesGrabbed.Count(); i++) {
3566
3770
  const node = this.#nodes[this.#nodesGrabbed.At(i)];
@@ -3575,14 +3779,20 @@ var NodeSubsystem2 = class {
3575
3779
  this.#boxSelect = false;
3576
3780
  }
3577
3781
  if (this.#widgetCurrentlyClicking !== null) {
3578
- this.#widgetCurrentlyClicking.ClickEnd();
3782
+ if (this.#widgetHovering === this.#widgetCurrentlyClicking) {
3783
+ this.#widgetCurrentlyClicking.ClickEnd();
3784
+ }
3579
3785
  this.#widgetCurrentlyClicking = null;
3580
3786
  }
3581
3787
  if (this.#connectionSelected === null) {
3582
3788
  return;
3583
3789
  }
3584
3790
  if (this.#portHovering === null) {
3791
+ const dangling = this.#describeDanglingConnection(this.#connectionSelected);
3585
3792
  this.#clearCurrentlySelectedConnection();
3793
+ if (dangling !== null) {
3794
+ this.#onConnectionReleased?.(dangling);
3795
+ }
3586
3796
  return;
3587
3797
  }
3588
3798
  const port = this.#portHovering.Port;
@@ -3649,6 +3859,7 @@ var NodeSubsystem2 = class {
3649
3859
  this.#idleConnectionRenderer
3650
3860
  );
3651
3861
  this.#connections.push(connection);
3862
+ requestRender();
3652
3863
  return connection;
3653
3864
  }
3654
3865
  getNodes() {
@@ -3709,6 +3920,7 @@ var NodeSubsystem2 = class {
3709
3920
  }
3710
3921
  addNode(node) {
3711
3922
  this.#nodes.push(node);
3923
+ requestRender();
3712
3924
  for (let i = 0; i < this.#registeredNodeAddedCallbacks.length; i++) {
3713
3925
  const callback = this.#registeredNodeAddedCallbacks[i];
3714
3926
  if (!callback) {
@@ -3755,6 +3967,7 @@ var NodeSubsystem2 = class {
3755
3967
  this.#removeNodeConnections(nodeIndex);
3756
3968
  const node = this.#nodes[nodeIndex];
3757
3969
  this.#nodes.splice(nodeIndex, 1);
3970
+ requestRender();
3758
3971
  for (let i = 0; i < this.#nodeRemovedCallbacks.length; i++) {
3759
3972
  this.#nodeRemovedCallbacks[i](node);
3760
3973
  }
@@ -3764,6 +3977,7 @@ var NodeSubsystem2 = class {
3764
3977
  this.#connections[index].clearPorts();
3765
3978
  }
3766
3979
  this.#connections.splice(index, 1);
3980
+ requestRender();
3767
3981
  }
3768
3982
  #removeConnection(connection, clearPorts) {
3769
3983
  const index = this.#connections.indexOf(connection);
@@ -3809,7 +4023,8 @@ var NodeSubsystem2 = class {
3809
4023
  this.#nodeFactory.openMenu(this, position)
3810
4024
  ]
3811
4025
  };
3812
- if (this.#nodesSelected().length > 0) {
4026
+ const selectedCount = this.#nodesSelected().length;
4027
+ if (selectedCount > 0) {
3813
4028
  organizeNodesSubMenu.items?.push({
3814
4029
  name: "Selected Nodes",
3815
4030
  group: nodeFlowGroup,
@@ -3817,6 +4032,13 @@ var NodeSubsystem2 = class {
3817
4032
  this.#organizeSelected(ctx);
3818
4033
  }
3819
4034
  });
4035
+ config.items?.push({
4036
+ name: selectedCount === 1 ? "Delete Selected Node" : `Delete ${selectedCount} Selected Nodes`,
4037
+ group: nodeFlowGroup,
4038
+ callback: () => {
4039
+ this.removeSelectedNodes();
4040
+ }
4041
+ });
3820
4042
  }
3821
4043
  if (this.#nodeHovering > -1) {
3822
4044
  const nodeToReview = this.#nodeHovering;
@@ -3890,6 +4112,29 @@ var NodeSubsystem2 = class {
3890
4112
  this.#selectNode(inputs[i], false);
3891
4113
  }
3892
4114
  }
4115
+ /** Drops the selection entirely. */
4116
+ unselectAllNodes() {
4117
+ let changed = false;
4118
+ for (let i = 0; i < this.#nodes.length; i++) {
4119
+ if (this.#nodes[i].selected()) {
4120
+ this.#nodes[i].unselect();
4121
+ changed = true;
4122
+ }
4123
+ }
4124
+ if (changed) {
4125
+ requestRender();
4126
+ }
4127
+ }
4128
+ /**
4129
+ * Removes every selected node, and the connections that touched them.
4130
+ */
4131
+ removeSelectedNodes() {
4132
+ const removed = this.getSelectedNodes();
4133
+ for (const node of removed) {
4134
+ this.removeNode(node);
4135
+ }
4136
+ return removed;
4137
+ }
3893
4138
  #selectNodeByIndex(nodeIndex, unselectOthers) {
3894
4139
  this.#selectNode(this.#nodes[nodeIndex], unselectOthers);
3895
4140
  }
@@ -3927,8 +4172,16 @@ var NodeSubsystem2 = class {
3927
4172
  this.#nodeHovering = -1;
3928
4173
  this.#boxSelectionNodes.Clear();
3929
4174
  const selectedBox_Screenspace = this.#boxSelectionScreenspaceBox(camera);
4175
+ const canvas = {
4176
+ Position: { x: 0, y: 0 },
4177
+ Size: { x: ctx.canvas.width, y: ctx.canvas.height }
4178
+ };
3930
4179
  for (let i = 0; i < this.#nodes.length; i++) {
3931
4180
  let state = 0 /* Idle */;
4181
+ const nodeBounds = this.#nodes[i].calculateBounds(ctx, camera);
4182
+ if (!BoxIntersection(nodeBounds, canvas) && this.#nodes[i].layoutPortsWithoutDrawing(nodeBounds, camera.zoom)) {
4183
+ continue;
4184
+ }
3932
4185
  if (mousePosition !== void 0 && !this.#boxSelect) {
3933
4186
  const intersection = this.#nodes[i].inBounds(ctx, camera, mousePosition);
3934
4187
  if (intersection.Node !== void 0 && intersection.PortIndex === void 0 && intersection.Widget === void 0) {
@@ -3949,8 +4202,8 @@ var NodeSubsystem2 = class {
3949
4202
  };
3950
4203
  }
3951
4204
  } else if (this.#boxSelect) {
3952
- const nodeBounds = this.#nodes[i].calculateBounds(ctx, camera);
3953
- if (BoxIntersection(selectedBox_Screenspace, nodeBounds)) {
4205
+ const nodeBounds2 = this.#nodes[i].calculateBounds(ctx, camera);
4206
+ if (BoxIntersection(selectedBox_Screenspace, nodeBounds2)) {
3954
4207
  state = 1 /* MouseOver */;
3955
4208
  this.#boxSelectionNodes.Push(i);
3956
4209
  }
@@ -3972,12 +4225,16 @@ var NodeSubsystem2 = class {
3972
4225
  render(ctx, camera, mousePosition) {
3973
4226
  this.#lastCtx = ctx;
3974
4227
  this.#cursor = "default" /* Default */;
4228
+ const awaitingLayout = this.#nodes.some((node) => !node.hasBeenLaidOut());
3975
4229
  TimeExecution("Render_Connections", () => {
3976
4230
  this.#renderConnections(ctx, camera, mousePosition);
3977
4231
  });
3978
4232
  TimeExecution("Render_Nodes", () => {
3979
4233
  this.#renderNodes(ctx, camera, mousePosition);
3980
4234
  });
4235
+ if (awaitingLayout) {
4236
+ requestRender();
4237
+ }
3981
4238
  if (this.#boxSelect) {
3982
4239
  const box = this.#boxSelectionScreenspaceBox(camera);
3983
4240
  ctx.setLineDash([Theme.BoxSelect.LineDashLength]);
@@ -4253,6 +4510,7 @@ var FlowNode = class {
4253
4510
  return;
4254
4511
  }
4255
4512
  this.#selected = true;
4513
+ requestRender();
4256
4514
  for (let i = 0; i < this.#onSelect.length; i++) {
4257
4515
  this.#onSelect[i]();
4258
4516
  }
@@ -4298,6 +4556,7 @@ var FlowNode = class {
4298
4556
  return;
4299
4557
  }
4300
4558
  this.#data[name] = value;
4559
+ requestRender();
4301
4560
  for (let i = 0; i < this.#registeredAnyPropertyChangeCallbacks.length; i++) {
4302
4561
  this.#registeredAnyPropertyChangeCallbacks[i](name, oldValue, value);
4303
4562
  }
@@ -4558,6 +4817,7 @@ var FlowNode = class {
4558
4817
  return;
4559
4818
  }
4560
4819
  this.#selected = false;
4820
+ requestRender();
4561
4821
  for (let i = 0; i < this.#onUnselect.length; i++) {
4562
4822
  this.#onUnselect[i]();
4563
4823
  }
@@ -4587,6 +4847,7 @@ var FlowNode = class {
4587
4847
  }
4588
4848
  setPosition(position) {
4589
4849
  CopyVector2(this.#position, position);
4850
+ requestRender();
4590
4851
  }
4591
4852
  getPosition() {
4592
4853
  return this.#position;
@@ -4649,10 +4910,12 @@ var FlowNode = class {
4649
4910
  }
4650
4911
  addWidget(widget) {
4651
4912
  this.#widgets.push(widget);
4913
+ requestRender();
4652
4914
  }
4653
4915
  insertWidget(index, widget) {
4654
4916
  const at = Math.max(0, Math.min(index, this.#widgets.length));
4655
4917
  this.#widgets.splice(at, 0, widget);
4918
+ requestRender();
4656
4919
  }
4657
4920
  removeWidget(widget) {
4658
4921
  const index = this.#widgets.indexOf(widget, 0);
@@ -4660,12 +4923,15 @@ var FlowNode = class {
4660
4923
  throw new Error("node does not contain widget");
4661
4924
  }
4662
4925
  this.#widgets.splice(index, 1);
4926
+ requestRender();
4663
4927
  }
4664
4928
  removeWidgetAt(index) {
4665
4929
  if (index < 0 || index >= this.#widgets.length) {
4666
4930
  return void 0;
4667
4931
  }
4668
- return this.#widgets.splice(index, 1)[0];
4932
+ const removed = this.#widgets.splice(index, 1)[0];
4933
+ requestRender();
4934
+ return removed;
4669
4935
  }
4670
4936
  moveWidget(from, to) {
4671
4937
  if (from < 0 || from >= this.#widgets.length) {
@@ -4676,9 +4942,11 @@ var FlowNode = class {
4676
4942
  }
4677
4943
  const [widget] = this.#widgets.splice(from, 1);
4678
4944
  this.#widgets.splice(to, 0, widget);
4945
+ requestRender();
4679
4946
  }
4680
4947
  clearWidgets() {
4681
4948
  this.#widgets.length = 0;
4949
+ requestRender();
4682
4950
  }
4683
4951
  getWidget(index) {
4684
4952
  return this.#widgets[index];
@@ -4750,6 +5018,7 @@ var FlowNode = class {
4750
5018
  return;
4751
5019
  }
4752
5020
  this.#title.set(cleaned);
5021
+ requestRender();
4753
5022
  for (let i = 0; i < this.#titleChangeCallback.length; i++) {
4754
5023
  const callback = this.#titleChangeCallback[i];
4755
5024
  callback(this, old, cleaned);
@@ -4768,6 +5037,7 @@ var FlowNode = class {
4768
5037
  }
4769
5038
  const old = this.#infoText;
4770
5039
  this.#infoText = cleaned;
5040
+ requestRender();
4771
5041
  for (let i = 0; i < this.#infoChangeCallback.length; i++) {
4772
5042
  const callback = this.#infoChangeCallback[i];
4773
5043
  callback(this, old, cleaned);
@@ -4807,7 +5077,12 @@ var FlowNode = class {
4807
5077
  clearMessages() {
4808
5078
  this.#messages = [];
4809
5079
  }
5080
+ hasBeenLaidOut() {
5081
+ return this.#laidOut;
5082
+ }
5083
+ #laidOut = false;
4810
5084
  render(ctx, camera, state, mousePosition, postProcess) {
5085
+ this.#laidOut = true;
4811
5086
  VectorPool.run(() => {
4812
5087
  const tempMeasurement = VectorPool.get();
4813
5088
  this.#inputPortPositions.Clear();
@@ -4943,8 +5218,58 @@ var FlowNode = class {
4943
5218
  const message = this.#messages[i];
4944
5219
  messageStart.y += message.render(ctx, camera.zoom, messageStart, !(state === 0 /* Idle */)) + 10 * camera.zoom;
4945
5220
  }
5221
+ this.#rememberPortOffsets(nodeBounds, camera.zoom);
4946
5222
  });
4947
5223
  }
5224
+ #portOffsets = { input: [], output: [] };
5225
+ #rememberPortOffsets(bounds, zoom) {
5226
+ if (zoom <= 0) {
5227
+ return;
5228
+ }
5229
+ const record = (positions, into) => {
5230
+ into.length = 0;
5231
+ for (let i = 0; i < positions.Count(); i++) {
5232
+ const box = positions.At(i);
5233
+ into.push({
5234
+ Position: {
5235
+ x: (box.Position.x - bounds.Position.x) / zoom,
5236
+ y: (box.Position.y - bounds.Position.y) / zoom
5237
+ },
5238
+ Size: {
5239
+ x: box.Size.x / zoom,
5240
+ y: box.Size.y / zoom
5241
+ }
5242
+ });
5243
+ }
5244
+ };
5245
+ record(this.#inputPortPositions, this.#portOffsets.input);
5246
+ record(this.#outputPortPositions, this.#portOffsets.output);
5247
+ }
5248
+ layoutPortsWithoutDrawing(bounds, zoom) {
5249
+ if (this.#portOffsets.input.length === 0 && this.#portOffsets.output.length === 0) {
5250
+ return false;
5251
+ }
5252
+ const project = (offsets, into) => {
5253
+ into.Clear();
5254
+ for (let i = 0; i < offsets.length; i++) {
5255
+ const offset = offsets[i];
5256
+ into.Push({
5257
+ Position: {
5258
+ x: bounds.Position.x + offset.Position.x * zoom,
5259
+ y: bounds.Position.y + offset.Position.y * zoom
5260
+ },
5261
+ Size: {
5262
+ x: offset.Size.x * zoom,
5263
+ y: offset.Size.y * zoom
5264
+ }
5265
+ });
5266
+ }
5267
+ };
5268
+ project(this.#portOffsets.input, this.#inputPortPositions);
5269
+ project(this.#portOffsets.output, this.#outputPortPositions);
5270
+ this.#widgetPositions.Clear();
5271
+ return true;
5272
+ }
4948
5273
  };
4949
5274
 
4950
5275
  // src/markdown/token.ts
@@ -5085,7 +5410,10 @@ function onFontsLoaded(callback) {
5085
5410
  if (typeof document === "undefined" || document.fonts === void 0) {
5086
5411
  return;
5087
5412
  }
5088
- document.fonts.addEventListener("loadingdone", () => callback());
5413
+ document.fonts.addEventListener("loadingdone", () => {
5414
+ callback();
5415
+ requestRender();
5416
+ });
5089
5417
  }
5090
5418
  var CodeBlockEntry = class {
5091
5419
  #text;
@@ -5835,6 +6163,7 @@ var NoteSubsystem = class {
5835
6163
  return;
5836
6164
  }
5837
6165
  this.#notes.push(note);
6166
+ requestRender();
5838
6167
  for (let i = 0; i < this.#onNoteAddedCallbacks.length; i++) {
5839
6168
  this.#onNoteAddedCallbacks[i](note);
5840
6169
  }
@@ -5957,6 +6286,7 @@ var NoteSubsystem = class {
5957
6286
  if (index > -1) {
5958
6287
  const noteRemoved = this.#notes[index];
5959
6288
  this.#notes.splice(index, 1);
6289
+ requestRender();
5960
6290
  for (let i = 0; i < this.#onNoteRemovedCallbacks.length; i++) {
5961
6291
  this.#onNoteRemovedCallbacks[i](noteRemoved);
5962
6292
  }
@@ -6293,6 +6623,124 @@ var QuickMenu = class {
6293
6623
  }
6294
6624
  };
6295
6625
 
6626
+ // src/minimap.ts
6627
+ var Minimap = class {
6628
+ #enabled;
6629
+ #width;
6630
+ #height;
6631
+ #margin;
6632
+ #backgroundColor;
6633
+ #borderColor;
6634
+ #nodeColor;
6635
+ #viewportColor;
6636
+ #box = { Position: { x: 0, y: 0 }, Size: { x: 0, y: 0 } };
6637
+ #scale = 1;
6638
+ #originX = 0;
6639
+ #originY = 0;
6640
+ // The padding the last frame was laid out with, so a click maps back
6641
+ // through the same geometry it was drawn with.
6642
+ #padding = Theme.Minimap.Padding;
6643
+ #drawn = false;
6644
+ constructor(config) {
6645
+ this.#enabled = config?.enabled === true;
6646
+ this.#width = config?.width === void 0 ? 200 : config.width;
6647
+ this.#height = config?.height === void 0 ? 150 : config.height;
6648
+ this.#margin = config?.margin === void 0 ? 16 : config.margin;
6649
+ this.#backgroundColor = config?.backgroundColor;
6650
+ this.#borderColor = config?.borderColor;
6651
+ this.#nodeColor = config?.nodeColor;
6652
+ this.#viewportColor = config?.viewportColor;
6653
+ }
6654
+ enabled() {
6655
+ return this.#enabled;
6656
+ }
6657
+ setEnabled(enabled) {
6658
+ this.#enabled = enabled;
6659
+ this.#drawn = false;
6660
+ }
6661
+ contains(position) {
6662
+ return this.#enabled && this.#drawn && InBox(this.#box, position);
6663
+ }
6664
+ graphPositionOf(position, out) {
6665
+ out.x = (position.x - this.#box.Position.x - this.#padding) / this.#scale + this.#originX;
6666
+ out.y = (position.y - this.#box.Position.y - this.#padding) / this.#scale + this.#originY;
6667
+ }
6668
+ render(ctx, canvas, camera, nodes) {
6669
+ if (!this.#enabled) {
6670
+ this.#drawn = false;
6671
+ return;
6672
+ }
6673
+ const flat = new Camera();
6674
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
6675
+ const boxes = [];
6676
+ for (let i = 0; i < nodes.length; i++) {
6677
+ const bounds = nodes[i].calculateBounds(ctx, flat);
6678
+ boxes.push(bounds);
6679
+ minX = Math.min(minX, bounds.Position.x);
6680
+ minY = Math.min(minY, bounds.Position.y);
6681
+ maxX = Math.max(maxX, bounds.Position.x + bounds.Size.x);
6682
+ maxY = Math.max(maxY, bounds.Position.y + bounds.Size.y);
6683
+ }
6684
+ const topLeft = { x: 0, y: 0 };
6685
+ const bottomRight = { x: 0, y: 0 };
6686
+ camera.screenSpaceToGraphSpace({ x: 0, y: 0 }, topLeft);
6687
+ camera.screenSpaceToGraphSpace({ x: canvas.width, y: canvas.height }, bottomRight);
6688
+ minX = Math.min(minX, topLeft.x);
6689
+ minY = Math.min(minY, topLeft.y);
6690
+ maxX = Math.max(maxX, bottomRight.x);
6691
+ maxY = Math.max(maxY, bottomRight.y);
6692
+ if (!isFinite(minX) || maxX <= minX || maxY <= minY) {
6693
+ this.#drawn = false;
6694
+ return;
6695
+ }
6696
+ this.#box.Size.x = this.#width;
6697
+ this.#box.Size.y = this.#height;
6698
+ this.#box.Position.x = canvas.width - this.#width - this.#margin;
6699
+ this.#box.Position.y = canvas.height - this.#height - this.#margin;
6700
+ this.#padding = Theme.Minimap.Padding;
6701
+ const usableWidth = this.#width - this.#padding * 2;
6702
+ const usableHeight = this.#height - this.#padding * 2;
6703
+ this.#scale = Math.min(usableWidth / (maxX - minX), usableHeight / (maxY - minY));
6704
+ this.#originX = minX;
6705
+ this.#originY = minY;
6706
+ const left = this.#box.Position.x + this.#padding;
6707
+ const top = this.#box.Position.y + this.#padding;
6708
+ const toMapX = (x) => left + (x - minX) * this.#scale;
6709
+ const toMapY = (y) => top + (y - minY) * this.#scale;
6710
+ ctx.fillStyle = this.#backgroundColor ?? Theme.Minimap.BackgroundColor;
6711
+ ctx.strokeStyle = this.#borderColor ?? Theme.Minimap.BorderColor;
6712
+ ctx.lineWidth = 1;
6713
+ ctx.beginPath();
6714
+ ctx.roundRect(
6715
+ this.#box.Position.x,
6716
+ this.#box.Position.y,
6717
+ this.#width,
6718
+ this.#height,
6719
+ Theme.Minimap.BorderRadius
6720
+ );
6721
+ ctx.fill();
6722
+ ctx.stroke();
6723
+ ctx.fillStyle = this.#nodeColor ?? Theme.Minimap.NodeColor;
6724
+ for (let i = 0; i < boxes.length; i++) {
6725
+ const b = boxes[i];
6726
+ ctx.fillRect(
6727
+ toMapX(b.Position.x),
6728
+ toMapY(b.Position.y),
6729
+ Math.max(1, b.Size.x * this.#scale),
6730
+ Math.max(1, b.Size.y * this.#scale)
6731
+ );
6732
+ }
6733
+ ctx.fillStyle = this.#viewportColor ?? Theme.Minimap.ViewportColor;
6734
+ ctx.fillRect(
6735
+ toMapX(topLeft.x),
6736
+ toMapY(topLeft.y),
6737
+ Math.max(1, (bottomRight.x - topLeft.x) * this.#scale),
6738
+ Math.max(1, (bottomRight.y - topLeft.y) * this.#scale)
6739
+ );
6740
+ this.#drawn = true;
6741
+ }
6742
+ };
6743
+
6296
6744
  // src/graph.ts
6297
6745
  function BuildBackgroundRenderer(backgroundColor) {
6298
6746
  return (canvas, context, position, scale) => {
@@ -6430,7 +6878,13 @@ var NodeFlowGraph = class {
6430
6878
  const backgroundColor = config?.backgroundColor === void 0 ? Theme.Graph.BackgroundColor : config.backgroundColor;
6431
6879
  this.#backgroundRenderer = BuildBackgroundRenderer(backgroundColor);
6432
6880
  }
6433
- window.requestAnimationFrame(this.#render.bind(this));
6881
+ this.#minimap = new Minimap(config?.minimap);
6882
+ this.#mainNodeSubsystem.setConnectionReleasedHandler((dangling) => {
6883
+ this.#openScopedMenu(dangling);
6884
+ });
6885
+ this.#watchCanvasSize();
6886
+ this.#stopRendering = onRenderRequested(this.#render.bind(this));
6887
+ requestRender();
6434
6888
  new MouseObserver(
6435
6889
  this.#canvas,
6436
6890
  this.#mouseDragEvent.bind(this),
@@ -6448,39 +6902,90 @@ var NodeFlowGraph = class {
6448
6902
  document.addEventListener(
6449
6903
  "keydown",
6450
6904
  (e) => {
6451
- if (document.activeElement) {
6452
- if (["INPUT", "TEXTAREA"].includes(document.activeElement.tagName)) return;
6453
- }
6454
- if (this.#openQuickMenu) {
6455
- if (e.code === "Escape") {
6456
- this.#openQuickMenu = null;
6457
- return;
6458
- }
6459
- if (e.code === "Enter") {
6460
- this.#openQuickMenu.Menu.execute();
6461
- this.#openQuickMenu = null;
6462
- return;
6463
- }
6464
- this.#openQuickMenu.Menu.keyboardEvent(e);
6905
+ this.#keyDown(e);
6906
+ requestRender();
6907
+ }
6908
+ );
6909
+ }
6910
+ #copyCallbacks = [];
6911
+ #pasteCallbacks = [];
6912
+ addCopyListener(callback) {
6913
+ this.#copyCallbacks.push(callback);
6914
+ }
6915
+ addPasteListener(callback) {
6916
+ this.#pasteCallbacks.push(callback);
6917
+ }
6918
+ #raiseCopy() {
6919
+ const nodes = this.#mainNodeSubsystem.getSelectedNodes();
6920
+ if (nodes.length === 0) {
6921
+ return;
6922
+ }
6923
+ const connections = this.#mainNodeSubsystem.connectionsWithin(nodes);
6924
+ this.#copyCallbacks.forEach((callback) => callback(nodes, connections));
6925
+ }
6926
+ #raisePaste() {
6927
+ const position = this.#mousePosition === void 0 ? { x: 0, y: 0 } : this.#sceenPositionToGraphPosition(this.#mousePosition);
6928
+ this.#pasteCallbacks.forEach((callback) => callback(position));
6929
+ }
6930
+ #deleteKey(e) {
6931
+ return e.code === "Delete" || e.code === "Backspace" || e.key === "Delete" || e.key === "Backspace";
6932
+ }
6933
+ #keyDown(e) {
6934
+ if (document.activeElement) {
6935
+ if (["INPUT", "TEXTAREA"].includes(document.activeElement.tagName)) return;
6936
+ }
6937
+ if (this.#deleteKey(e)) {
6938
+ if (this.#mainNodeSubsystem.getSelectedNodes().length > 0) {
6939
+ e.preventDefault();
6940
+ this.#mainNodeSubsystem.removeSelectedNodes();
6941
+ return;
6942
+ }
6943
+ }
6944
+ if (e.ctrlKey || e.metaKey) {
6945
+ const letter = e.code.startsWith("Key") ? e.code.substring(3).toLowerCase() : (e.key ?? "").toLowerCase();
6946
+ switch (letter) {
6947
+ case "c":
6948
+ this.#raiseCopy();
6465
6949
  return;
6466
- }
6467
- const spacePressed = e.key == " " || e.code == "Space";
6468
- if (!spacePressed) {
6950
+ case "v":
6951
+ this.#raisePaste();
6469
6952
  return;
6470
- }
6471
- if (!this.#mousePosition) {
6953
+ // duplicate
6954
+ case "d":
6955
+ e.preventDefault();
6956
+ this.#raiseCopy();
6957
+ this.#raisePaste();
6472
6958
  return;
6473
- }
6474
- const contextMenuPosition = this.#sceenPositionToGraphPosition(this.#mousePosition);
6475
- let items = this.#mainNodeSubsystem.nodeFactory().newNodeSubmenus(this.#mainNodeSubsystem, contextMenuPosition);
6476
- this.#openQuickMenu = {
6477
- Menu: new QuickMenu({
6478
- subMenus: items
6479
- }),
6480
- Position: contextMenuPosition
6481
- };
6482
6959
  }
6483
- );
6960
+ }
6961
+ if (this.#openQuickMenu) {
6962
+ if (e.code === "Escape") {
6963
+ this.#openQuickMenu = null;
6964
+ return;
6965
+ }
6966
+ if (e.code === "Enter") {
6967
+ this.#openQuickMenu.Menu.execute();
6968
+ this.#openQuickMenu = null;
6969
+ return;
6970
+ }
6971
+ this.#openQuickMenu.Menu.keyboardEvent(e);
6972
+ return;
6973
+ }
6974
+ const spacePressed = e.key == " " || e.code == "Space";
6975
+ if (!spacePressed) {
6976
+ return;
6977
+ }
6978
+ if (!this.#mousePosition) {
6979
+ return;
6980
+ }
6981
+ const contextMenuPosition = this.#sceenPositionToGraphPosition(this.#mousePosition);
6982
+ let items = this.#mainNodeSubsystem.nodeFactory().newNodeSubmenus(this.#mainNodeSubsystem, contextMenuPosition);
6983
+ this.#openQuickMenu = {
6984
+ Menu: new QuickMenu({
6985
+ subMenus: items
6986
+ }),
6987
+ Position: contextMenuPosition
6988
+ };
6484
6989
  }
6485
6990
  #fileDrop(file) {
6486
6991
  if (this.currentView().fileDrop(file)) {
@@ -6546,8 +7051,25 @@ var NodeFlowGraph = class {
6546
7051
  this.#openQuickMenu = null;
6547
7052
  this.#contextMenuEntryHovering = null;
6548
7053
  this.#mousePosition = mousePosition;
7054
+ if (this.#minimap.contains(mousePosition)) {
7055
+ this.#lookAtFromMinimap(mousePosition);
7056
+ this.#draggingMinimap = true;
7057
+ return;
7058
+ }
6549
7059
  this.currentView().clickStart(mousePosition, this.#camera, ctrlKey);
6550
7060
  }
7061
+ #draggingMinimap = false;
7062
+ #lookAtFromMinimap(mousePosition) {
7063
+ const target = { x: 0, y: 0 };
7064
+ this.#minimap.graphPositionOf(mousePosition, target);
7065
+ this.#camera.position.x = -(target.x * this.#camera.zoom) + this.#canvas.width / 2;
7066
+ this.#camera.position.y = -(target.y * this.#camera.zoom) + this.#canvas.height / 2;
7067
+ requestRender();
7068
+ }
7069
+ /** The minimap, so it can be turned on and sized by the host. */
7070
+ minimap() {
7071
+ return this.#minimap;
7072
+ }
6551
7073
  currentView() {
6552
7074
  return this.#views[this.#currentView];
6553
7075
  }
@@ -6567,6 +7089,12 @@ var NodeFlowGraph = class {
6567
7089
  getSelectedNodes() {
6568
7090
  return this.#mainNodeSubsystem.getSelectedNodes();
6569
7091
  }
7092
+ unselectAllNodes() {
7093
+ this.#mainNodeSubsystem.unselectAllNodes();
7094
+ }
7095
+ removeSelectedNodes() {
7096
+ return this.#mainNodeSubsystem.removeSelectedNodes();
7097
+ }
6570
7098
  /**
6571
7099
  * Returns a copy of the current camera orientation (screen-space pan + zoom).
6572
7100
  */
@@ -6682,10 +7210,37 @@ var NodeFlowGraph = class {
6682
7210
  Position: contextMenuPosition
6683
7211
  };
6684
7212
  }
7213
+ #openScopedMenu(dangling) {
7214
+ if (this.#mousePosition === void 0) {
7215
+ return;
7216
+ }
7217
+ const position = this.#sceenPositionToGraphPosition(this.#mousePosition);
7218
+ const menu = this.#mainNodeSubsystem.nodeFactory().openMenu(
7219
+ this.#mainNodeSubsystem,
7220
+ position,
7221
+ {
7222
+ needs: dangling.needs,
7223
+ types: dangling.types,
7224
+ onCreated: dangling.connectTo
7225
+ }
7226
+ );
7227
+ this.#openedContextMenu = {
7228
+ Menu: new ContextMenu(menu),
7229
+ Position: position
7230
+ };
7231
+ requestRender();
7232
+ }
6685
7233
  #clickEnd() {
7234
+ this.#draggingMinimap = false;
6686
7235
  this.currentView().clickEnd();
6687
7236
  }
6688
7237
  #mouseDragEvent(delta) {
7238
+ if (this.#draggingMinimap) {
7239
+ if (this.#mousePosition !== void 0) {
7240
+ this.#lookAtFromMinimap(this.#mousePosition);
7241
+ }
7242
+ return;
7243
+ }
6689
7244
  let draggingSomething = this.currentView().mouseDragEvent(delta, this.#camera.zoom);
6690
7245
  if (!draggingSomething) {
6691
7246
  this.#camera.position.x += delta.x;
@@ -6694,12 +7249,45 @@ var NodeFlowGraph = class {
6694
7249
  }
6695
7250
  #lastFrameCursor;
6696
7251
  #cursor;
6697
- #render() {
6698
- if (this.#canvas.parentNode !== null) {
6699
- var rect = this.#canvas.parentNode.getBoundingClientRect();
6700
- this.#canvas.width = rect.width;
6701
- this.#canvas.height = rect.height;
7252
+ #minimap;
7253
+ #stopRendering;
7254
+ #sizeObserver;
7255
+ #matchCanvasToParent() {
7256
+ const parent = this.#canvas.parentNode;
7257
+ if (parent === null) {
7258
+ return;
7259
+ }
7260
+ const rect = parent.getBoundingClientRect();
7261
+ if (this.#canvas.width === rect.width && this.#canvas.height === rect.height) {
7262
+ return;
6702
7263
  }
7264
+ this.#canvas.width = rect.width;
7265
+ this.#canvas.height = rect.height;
7266
+ }
7267
+ #watchCanvasSize() {
7268
+ this.#matchCanvasToParent();
7269
+ const parent = this.#canvas.parentNode;
7270
+ if (parent === null || typeof ResizeObserver === "undefined") {
7271
+ return;
7272
+ }
7273
+ this.#sizeObserver = new ResizeObserver(() => {
7274
+ this.#matchCanvasToParent();
7275
+ requestRender();
7276
+ });
7277
+ this.#sizeObserver.observe(parent);
7278
+ }
7279
+ requestRender() {
7280
+ requestRender();
7281
+ }
7282
+ renderContinuously() {
7283
+ return renderContinuously();
7284
+ }
7285
+ dispose() {
7286
+ this.#stopRendering();
7287
+ this.#sizeObserver?.disconnect();
7288
+ }
7289
+ #render() {
7290
+ this.#matchCanvasToParent();
6703
7291
  this.#cursor = "default" /* Default */;
6704
7292
  TimeExecution("Render_Background", this.#renderBackground.bind(this));
6705
7293
  TimeExecution("Render_View_" + this.#currentView, () => {
@@ -6708,13 +7296,15 @@ var NodeFlowGraph = class {
6708
7296
  this.#cursor = results?.cursorStyle;
6709
7297
  }
6710
7298
  });
7299
+ TimeExecution("Render_Minimap", () => {
7300
+ this.#minimap.render(this.#ctx, this.#canvas, this.#camera, this.#mainNodeSubsystem.getNodes());
7301
+ });
6711
7302
  TimeExecution("Render_Context", this.#renderContextMenu.bind(this));
6712
7303
  TimeExecution("Render_QuickMenu", this.#renderQuickMenu.bind(this));
6713
7304
  if (this.#lastFrameCursor !== this.#cursor) {
6714
7305
  this.#canvas.style.cursor = this.#cursor;
6715
7306
  }
6716
7307
  this.#lastFrameCursor = this.#cursor;
6717
- window.requestAnimationFrame(this.#render.bind(this));
6718
7308
  }
6719
7309
  #renderBackground() {
6720
7310
  this.#backgroundRenderer(this.#canvas, this.#ctx, this.#camera.position, this.#camera.zoom);
@@ -6756,6 +7346,7 @@ var NodeFlowGraph = class {
6756
7346
  GlobalWidgetFactory,
6757
7347
  ImageWidget,
6758
7348
  MessageType,
7349
+ Minimap,
6759
7350
  NodeFactory,
6760
7351
  NodeFlowGraph,
6761
7352
  NodeState,
@@ -6769,6 +7360,10 @@ var NodeFlowGraph = class {
6769
7360
  Theme,
6770
7361
  ToggleWidget,
6771
7362
  WidgetFactory,
6772
- portsCompatible
7363
+ portConfigTypes,
7364
+ portsCompatible,
7365
+ renderContinuously,
7366
+ requestRender,
7367
+ typeSetsCompatible
6773
7368
  });
6774
7369
  //# sourceMappingURL=index.js.map