@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.mjs CHANGED
@@ -60,6 +60,14 @@ var Theme = {
60
60
  FontColor: "#afb9bb",
61
61
  DisabledFontColor: "#4a585c"
62
62
  },
63
+ Minimap: {
64
+ BackgroundColor: "rgba(10, 16, 18, 0.85)",
65
+ BorderColor: "#1c1c1c",
66
+ BorderRadius: 4,
67
+ Padding: 6,
68
+ NodeColor: "#6b7f85",
69
+ ViewportColor: "rgba(175, 185, 187, 0.25)"
70
+ },
63
71
  Note: {
64
72
  FontColor: "#afb9bb",
65
73
  FontSize: 16,
@@ -601,6 +609,43 @@ var ContextMenu = class _ContextMenu {
601
609
  }
602
610
  };
603
611
 
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
+
604
649
  // src/input.ts
605
650
  var LONG_PRESS_MS = 500;
606
651
  var LONG_PRESS_SLOP = 12;
@@ -634,15 +679,19 @@ var MouseObserver = class {
634
679
  this.#lastTouch = Zero();
635
680
  this.#lastMousePosition = Zero();
636
681
  ele.style.touchAction = "none";
637
- ele.addEventListener("mousedown", this.#down.bind(this), false);
638
- ele.addEventListener("touchstart", this.#touchStart.bind(this), { passive: false });
639
- document.addEventListener("mouseup", this.#up.bind(this), false);
640
- document.addEventListener("touchend", this.#touchEnd.bind(this), false);
641
- document.addEventListener("touchcancel", this.#touchEnd.bind(this), false);
642
- ele.addEventListener("mousemove", this.#move.bind(this), false);
643
- ele.addEventListener("touchmove", this.#touchMove.bind(this), { passive: false });
644
- ele.addEventListener("wheel", this.#wheel.bind(this), { passive: false });
645
- ele.addEventListener("drop", (ev) => {
682
+ const redraws = (handler) => (event) => {
683
+ handler(event);
684
+ requestRender();
685
+ };
686
+ ele.addEventListener("mousedown", redraws(this.#down.bind(this)), false);
687
+ ele.addEventListener("touchstart", redraws(this.#touchStart.bind(this)), { passive: false });
688
+ document.addEventListener("mouseup", redraws(this.#up.bind(this)), false);
689
+ document.addEventListener("touchend", redraws(this.#touchEnd.bind(this)), false);
690
+ document.addEventListener("touchcancel", redraws(this.#touchEnd.bind(this)), false);
691
+ ele.addEventListener("mousemove", redraws(this.#move.bind(this)), false);
692
+ ele.addEventListener("touchmove", redraws(this.#touchMove.bind(this)), { passive: false });
693
+ ele.addEventListener("wheel", redraws(this.#wheel.bind(this)), { passive: false });
694
+ ele.addEventListener("drop", redraws((ev) => {
646
695
  ev.preventDefault();
647
696
  if (ev.dataTransfer?.items) {
648
697
  [...ev.dataTransfer.items].forEach((item, i) => {
@@ -654,18 +703,18 @@ var MouseObserver = class {
654
703
  }
655
704
  });
656
705
  }
657
- });
658
- ele.addEventListener("dragover", (ev) => {
706
+ }));
707
+ ele.addEventListener("dragover", redraws((ev) => {
659
708
  ev.preventDefault();
660
709
  this.#moveCallback(this.#mousePos(ev));
661
- });
662
- ele.addEventListener("contextmenu", (evt) => {
710
+ }));
711
+ ele.addEventListener("contextmenu", redraws((evt) => {
663
712
  evt.preventDefault();
664
713
  if (Date.now() - this.#lastTouchAt < SUPPRESS_MOUSE_MS) {
665
714
  return;
666
715
  }
667
716
  this.#contextMenu(this.#mousePos(evt));
668
- }, false);
717
+ }), false);
669
718
  }
670
719
  #mousePos(event) {
671
720
  const rect = this.#ele.getBoundingClientRect();
@@ -762,6 +811,7 @@ var MouseObserver = class {
762
811
  this.#clicked = false;
763
812
  this.#clickStop();
764
813
  this.#contextMenu({ x, y });
814
+ requestRender();
765
815
  }, LONG_PRESS_MS);
766
816
  this.#clicked = true;
767
817
  this.#moveCallback(pos);
@@ -1198,6 +1248,7 @@ var Text = class _Text {
1198
1248
  if (!document.fonts.check(`16px "${this.#style.getFont()}"`)) {
1199
1249
  document.fonts.addEventListener("loadingdone", () => {
1200
1250
  this.#measured = false;
1251
+ requestRender();
1201
1252
  });
1202
1253
  }
1203
1254
  }
@@ -1409,12 +1460,22 @@ function portsCompatible(a, b) {
1409
1460
  if (!a || !b) {
1410
1461
  return false;
1411
1462
  }
1412
- const left = a.acceptedTypes();
1413
- const right = b.acceptedTypes();
1414
- if (left.length === 0 || right.length === 0) {
1463
+ return typeSetsCompatible(a.acceptedTypes(), b.acceptedTypes());
1464
+ }
1465
+ function typeSetsCompatible(a, b) {
1466
+ if (a.length === 0 || b.length === 0) {
1415
1467
  return true;
1416
1468
  }
1417
- return left.some((type) => right.includes(type));
1469
+ return a.some((type) => b.includes(type));
1470
+ }
1471
+ function portConfigTypes(config) {
1472
+ if (config.anyType === true) {
1473
+ return [];
1474
+ }
1475
+ if (config.acceptedTypes !== void 0 && config.acceptedTypes.length > 0) {
1476
+ return config.acceptedTypes;
1477
+ }
1478
+ return [config.type === void 0 ? "" : config.type];
1418
1479
  }
1419
1480
  function fallbackColor(type, s) {
1420
1481
  let value = 0;
@@ -1529,6 +1590,7 @@ var Port = class {
1529
1590
  this.#filledStyle.fillColor = fallbackColor(this.#dataType, 1);
1530
1591
  }
1531
1592
  this.#dataTypePopupElement = this.#buildTooltip();
1593
+ requestRender();
1532
1594
  }
1533
1595
  setDataType(type) {
1534
1596
  if (this.#dataType === type) {
@@ -1782,6 +1844,7 @@ var Popup = class {
1782
1844
  #buttonCSS;
1783
1845
  // Runtime ================================================================
1784
1846
  #popup;
1847
+ #keyHandler;
1785
1848
  constructor(config) {
1786
1849
  this.#title = config.title;
1787
1850
  this.#options = config.options;
@@ -1789,14 +1852,25 @@ var Popup = class {
1789
1852
  this.#onClose = config.onClose;
1790
1853
  this.#buttonCSS = config.buttonCSS === void 0 ? "flex-grow: 1; margin-right: 8px;" : config.buttonCSS;
1791
1854
  this.#popup = null;
1855
+ this.#keyHandler = null;
1792
1856
  }
1793
1857
  Hide() {
1794
1858
  if (this.#popup === null) {
1795
1859
  return;
1796
1860
  }
1861
+ if (this.#keyHandler !== null) {
1862
+ document.removeEventListener("keydown", this.#keyHandler, true);
1863
+ this.#keyHandler = null;
1864
+ }
1797
1865
  document.body.removeChild(this.#popup);
1798
1866
  this.#popup = null;
1799
1867
  }
1868
+ #close(option) {
1869
+ this.Hide();
1870
+ if (this.#onClose !== void 0) {
1871
+ this.#onClose(option);
1872
+ }
1873
+ }
1800
1874
  Show() {
1801
1875
  this.#popup = document.createElement("div");
1802
1876
  this.#popup.style.cssText = "position:absolute;top:0;width:100%;display:flex;height:100%;justify-content:center;align-items:center;background-color:#00000070;";
@@ -1818,14 +1892,20 @@ var Popup = class {
1818
1892
  const button = document.createElement("button");
1819
1893
  button.style.cssText = this.#buttonCSS;
1820
1894
  button.textContent = this.#options[i];
1821
- button.onclick = () => {
1822
- this.Hide();
1823
- if (this.#onClose !== void 0) {
1824
- this.#onClose(this.#options[i]);
1825
- }
1826
- };
1895
+ button.onclick = () => this.#close(this.#options[i]);
1827
1896
  buttonContainer.appendChild(button);
1828
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);
1829
1909
  }
1830
1910
  };
1831
1911
 
@@ -1934,8 +2014,8 @@ var NumberWidget = class {
1934
2014
  },
1935
2015
  text: { color: Theme.Widget.FontColor }
1936
2016
  }));
1937
- this.#callback = config?.callback;
1938
2017
  this.Set(startingWidgetValue(node, this.#nodeProperty, config?.value, 0));
2018
+ this.#callback = config?.callback;
1939
2019
  if (this.#nodeProperty !== void 0) {
1940
2020
  this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
1941
2021
  this.Set(newVal);
@@ -2377,8 +2457,8 @@ var StringWidget = class {
2377
2457
  },
2378
2458
  text: { color: Theme.Widget.FontColor }
2379
2459
  }));
2380
- this.#callback = config?.callback;
2381
2460
  this.Set(startingWidgetValue(node, this.#nodeProperty, config?.value, ""));
2461
+ this.#callback = config?.callback;
2382
2462
  if (this.#nodeProperty !== void 0) {
2383
2463
  this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
2384
2464
  this.Set(newVal);
@@ -2543,8 +2623,8 @@ var ToggleWidget = class {
2543
2623
  lightBorderColor: config?.disabledStyle?.lightBorderColor,
2544
2624
  lightColor: config?.disabledStyle?.lightColor === void 0 ? "#004400" : config?.enabledStyle?.lightColor
2545
2625
  });
2546
- this.#callback = config?.callback;
2547
2626
  this.Set(startingWidgetValue(node, this.#nodeProperty, config?.value, false));
2627
+ this.#callback = config?.callback;
2548
2628
  if (this.#nodeProperty !== void 0) {
2549
2629
  this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
2550
2630
  this.Set(newVal);
@@ -2607,6 +2687,7 @@ var ImageWidget = class {
2607
2687
  img.src = url;
2608
2688
  img.onload = () => {
2609
2689
  this.#image = img;
2690
+ requestRender();
2610
2691
  };
2611
2692
  img.onerror = (event) => {
2612
2693
  console.log("error loading image:", url, event);
@@ -3187,6 +3268,13 @@ function TimeExecution(name, callback) {
3187
3268
  }
3188
3269
 
3189
3270
  // src/nodes/publisher.ts
3271
+ function offersMatchingPort(config, filter) {
3272
+ const ports = filter.needs === "INPUT" /* Input */ ? config.inputs : config.outputs;
3273
+ if (ports === void 0) {
3274
+ return false;
3275
+ }
3276
+ return ports.some((port) => typeSetsCompatible(portConfigTypes(port), filter.types));
3277
+ }
3190
3278
  var Publisher = class {
3191
3279
  #name;
3192
3280
  #description;
@@ -3212,19 +3300,23 @@ var Publisher = class {
3212
3300
  unregister(nodeType) {
3213
3301
  return this.#registeredNodes.delete(nodeType);
3214
3302
  }
3215
- #recurseBuildMenu(graph, name, subMenu, position) {
3303
+ #recurseBuildMenu(graph, name, subMenu, position, filter) {
3216
3304
  const items = [];
3217
3305
  const subMenus = /* @__PURE__ */ new Map();
3218
3306
  for (let [key, nodeConfig] of subMenu) {
3219
3307
  const slashIndex = key.indexOf("/");
3220
3308
  const bracketIndex = key.indexOf("[");
3221
3309
  if (slashIndex === -1 || bracketIndex !== -1 && bracketIndex < slashIndex) {
3310
+ if (filter !== void 0 && !offersMatchingPort(nodeConfig, filter)) {
3311
+ continue;
3312
+ }
3222
3313
  items.push({
3223
3314
  name: key,
3224
3315
  callback: () => {
3225
3316
  const node = new FlowNode(nodeConfig);
3226
3317
  node.setPosition(position);
3227
3318
  graph.addNode(node);
3319
+ filter?.onCreated?.(node);
3228
3320
  }
3229
3321
  });
3230
3322
  } else {
@@ -3239,7 +3331,11 @@ var Publisher = class {
3239
3331
  }
3240
3332
  const menus = [];
3241
3333
  for (let [key, nodes] of subMenus) {
3242
- menus.push(this.#recurseBuildMenu(graph, key, nodes, position));
3334
+ const built = this.#recurseBuildMenu(graph, key, nodes, position, filter);
3335
+ if (filter !== void 0 && built.items?.length === 0 && built.subMenus?.length === 0) {
3336
+ continue;
3337
+ }
3338
+ menus.push(built);
3243
3339
  }
3244
3340
  return {
3245
3341
  name,
@@ -3247,8 +3343,8 @@ var Publisher = class {
3247
3343
  subMenus: menus
3248
3344
  };
3249
3345
  }
3250
- contextMenu(graph, position) {
3251
- return this.#recurseBuildMenu(graph, this.#name, this.#registeredNodes, position);
3346
+ contextMenu(graph, position, filter) {
3347
+ return this.#recurseBuildMenu(graph, this.#name, this.#registeredNodes, position, filter);
3252
3348
  }
3253
3349
  create(nodeType) {
3254
3350
  const config = this.#registeredNodes.get(nodeType);
@@ -3303,18 +3399,22 @@ var NodeFactory = class {
3303
3399
  }
3304
3400
  return node;
3305
3401
  }
3306
- newNodeSubmenus(graph, position) {
3402
+ newNodeSubmenus(graph, position, filter) {
3307
3403
  const menus = [];
3308
3404
  for (let [_, publisher] of this.#registeredPublishers) {
3309
- menus.push(publisher.contextMenu(graph, position));
3405
+ const built = publisher.contextMenu(graph, position, filter);
3406
+ if (filter !== void 0 && built.items?.length === 0 && built.subMenus?.length === 0) {
3407
+ continue;
3408
+ }
3409
+ menus.push(built);
3310
3410
  }
3311
3411
  return menus;
3312
3412
  }
3313
- openMenu(graph, position) {
3413
+ openMenu(graph, position, filter) {
3314
3414
  return {
3315
3415
  name: "New Node",
3316
3416
  group: nodeFlowGroup,
3317
- subMenus: this.newNodeSubmenus(graph, position)
3417
+ subMenus: this.newNodeSubmenus(graph, position, filter)
3318
3418
  };
3319
3419
  }
3320
3420
  };
@@ -3453,6 +3553,9 @@ var NodeSubsystem2 = class {
3453
3553
  CopyVector2(this.#boxSelectEnd_graphSpace, this.#boxSelectStart_graphSpace);
3454
3554
  this.#boxSelect = true;
3455
3555
  }
3556
+ if (!hoveringSomething && !ctrlKey) {
3557
+ this.unselectAllNodes();
3558
+ }
3456
3559
  return hoveringSomething || ctrlKey;
3457
3560
  }
3458
3561
  if (this.#portHovering.InputPort) {
@@ -3512,6 +3615,102 @@ var NodeSubsystem2 = class {
3512
3615
  }
3513
3616
  }
3514
3617
  }
3618
+ connectionsWithin(nodes) {
3619
+ const within = [];
3620
+ for (const connection of this.#connections) {
3621
+ const outNode = connection.outNode();
3622
+ const inNode = connection.inNode();
3623
+ if (outNode === null || inNode === null) {
3624
+ continue;
3625
+ }
3626
+ const from = nodes.indexOf(outNode);
3627
+ const to = nodes.indexOf(inNode);
3628
+ if (from === -1 || to === -1) {
3629
+ continue;
3630
+ }
3631
+ const outPort = connection.outPort();
3632
+ const inPort = connection.inPort();
3633
+ if (outPort === null || inPort === null) {
3634
+ continue;
3635
+ }
3636
+ within.push({
3637
+ fromNode: from,
3638
+ fromPort: this.#outputPortIndex(outNode, outPort),
3639
+ toNode: to,
3640
+ toPort: this.#inputPortIndex(inNode, inPort)
3641
+ });
3642
+ }
3643
+ return within;
3644
+ }
3645
+ #onConnectionReleased = null;
3646
+ setConnectionReleasedHandler(handler) {
3647
+ this.#onConnectionReleased = handler;
3648
+ }
3649
+ #describeDanglingConnection(conn) {
3650
+ const outNode = conn.outNode();
3651
+ const inNode = conn.inNode();
3652
+ if (outNode !== null && inNode === null) {
3653
+ const from = conn.outPort();
3654
+ if (from === null) {
3655
+ return null;
3656
+ }
3657
+ const fromIndex = this.#outputPortIndex(outNode, from);
3658
+ return {
3659
+ needs: "INPUT" /* Input */,
3660
+ types: from.acceptedTypes(),
3661
+ connectTo: (node) => {
3662
+ const to = this.#firstCompatiblePort(node, "INPUT" /* Input */, from);
3663
+ if (to !== -1 && fromIndex !== -1) {
3664
+ this.connectNodes(outNode, fromIndex, node, to);
3665
+ }
3666
+ }
3667
+ };
3668
+ }
3669
+ if (inNode !== null && outNode === null) {
3670
+ const to = conn.inPort();
3671
+ if (to === null) {
3672
+ return null;
3673
+ }
3674
+ const toIndex = this.#inputPortIndex(inNode, to);
3675
+ return {
3676
+ needs: "OUTPUT" /* Output */,
3677
+ types: to.acceptedTypes(),
3678
+ connectTo: (node) => {
3679
+ const from = this.#firstCompatiblePort(node, "OUTPUT" /* Output */, to);
3680
+ if (from !== -1 && toIndex !== -1) {
3681
+ this.connectNodes(node, from, inNode, toIndex);
3682
+ }
3683
+ }
3684
+ };
3685
+ }
3686
+ return null;
3687
+ }
3688
+ #outputPortIndex(node, port) {
3689
+ for (let i = 0; i < node.outputs(); i++) {
3690
+ if (node.outputPort(i) === port) {
3691
+ return i;
3692
+ }
3693
+ }
3694
+ return -1;
3695
+ }
3696
+ #inputPortIndex(node, port) {
3697
+ for (let i = 0; i < node.inputs(); i++) {
3698
+ if (node.inputPort(i) === port) {
3699
+ return i;
3700
+ }
3701
+ }
3702
+ return -1;
3703
+ }
3704
+ #firstCompatiblePort(node, side, against) {
3705
+ const count = side === "INPUT" /* Input */ ? node.inputs() : node.outputs();
3706
+ for (let i = 0; i < count; i++) {
3707
+ const port = side === "INPUT" /* Input */ ? node.inputPort(i) : node.outputPort(i);
3708
+ if (portsCompatible(port, against)) {
3709
+ return i;
3710
+ }
3711
+ }
3712
+ return -1;
3713
+ }
3515
3714
  clickEnd() {
3516
3715
  for (let i = 0; i < this.#nodesGrabbed.Count(); i++) {
3517
3716
  const node = this.#nodes[this.#nodesGrabbed.At(i)];
@@ -3526,14 +3725,20 @@ var NodeSubsystem2 = class {
3526
3725
  this.#boxSelect = false;
3527
3726
  }
3528
3727
  if (this.#widgetCurrentlyClicking !== null) {
3529
- this.#widgetCurrentlyClicking.ClickEnd();
3728
+ if (this.#widgetHovering === this.#widgetCurrentlyClicking) {
3729
+ this.#widgetCurrentlyClicking.ClickEnd();
3730
+ }
3530
3731
  this.#widgetCurrentlyClicking = null;
3531
3732
  }
3532
3733
  if (this.#connectionSelected === null) {
3533
3734
  return;
3534
3735
  }
3535
3736
  if (this.#portHovering === null) {
3737
+ const dangling = this.#describeDanglingConnection(this.#connectionSelected);
3536
3738
  this.#clearCurrentlySelectedConnection();
3739
+ if (dangling !== null) {
3740
+ this.#onConnectionReleased?.(dangling);
3741
+ }
3537
3742
  return;
3538
3743
  }
3539
3744
  const port = this.#portHovering.Port;
@@ -3600,6 +3805,7 @@ var NodeSubsystem2 = class {
3600
3805
  this.#idleConnectionRenderer
3601
3806
  );
3602
3807
  this.#connections.push(connection);
3808
+ requestRender();
3603
3809
  return connection;
3604
3810
  }
3605
3811
  getNodes() {
@@ -3660,6 +3866,7 @@ var NodeSubsystem2 = class {
3660
3866
  }
3661
3867
  addNode(node) {
3662
3868
  this.#nodes.push(node);
3869
+ requestRender();
3663
3870
  for (let i = 0; i < this.#registeredNodeAddedCallbacks.length; i++) {
3664
3871
  const callback = this.#registeredNodeAddedCallbacks[i];
3665
3872
  if (!callback) {
@@ -3706,6 +3913,7 @@ var NodeSubsystem2 = class {
3706
3913
  this.#removeNodeConnections(nodeIndex);
3707
3914
  const node = this.#nodes[nodeIndex];
3708
3915
  this.#nodes.splice(nodeIndex, 1);
3916
+ requestRender();
3709
3917
  for (let i = 0; i < this.#nodeRemovedCallbacks.length; i++) {
3710
3918
  this.#nodeRemovedCallbacks[i](node);
3711
3919
  }
@@ -3715,6 +3923,7 @@ var NodeSubsystem2 = class {
3715
3923
  this.#connections[index].clearPorts();
3716
3924
  }
3717
3925
  this.#connections.splice(index, 1);
3926
+ requestRender();
3718
3927
  }
3719
3928
  #removeConnection(connection, clearPorts) {
3720
3929
  const index = this.#connections.indexOf(connection);
@@ -3760,7 +3969,8 @@ var NodeSubsystem2 = class {
3760
3969
  this.#nodeFactory.openMenu(this, position)
3761
3970
  ]
3762
3971
  };
3763
- if (this.#nodesSelected().length > 0) {
3972
+ const selectedCount = this.#nodesSelected().length;
3973
+ if (selectedCount > 0) {
3764
3974
  organizeNodesSubMenu.items?.push({
3765
3975
  name: "Selected Nodes",
3766
3976
  group: nodeFlowGroup,
@@ -3768,6 +3978,13 @@ var NodeSubsystem2 = class {
3768
3978
  this.#organizeSelected(ctx);
3769
3979
  }
3770
3980
  });
3981
+ config.items?.push({
3982
+ name: selectedCount === 1 ? "Delete Selected Node" : `Delete ${selectedCount} Selected Nodes`,
3983
+ group: nodeFlowGroup,
3984
+ callback: () => {
3985
+ this.removeSelectedNodes();
3986
+ }
3987
+ });
3771
3988
  }
3772
3989
  if (this.#nodeHovering > -1) {
3773
3990
  const nodeToReview = this.#nodeHovering;
@@ -3841,6 +4058,29 @@ var NodeSubsystem2 = class {
3841
4058
  this.#selectNode(inputs[i], false);
3842
4059
  }
3843
4060
  }
4061
+ /** Drops the selection entirely. */
4062
+ unselectAllNodes() {
4063
+ let changed = false;
4064
+ for (let i = 0; i < this.#nodes.length; i++) {
4065
+ if (this.#nodes[i].selected()) {
4066
+ this.#nodes[i].unselect();
4067
+ changed = true;
4068
+ }
4069
+ }
4070
+ if (changed) {
4071
+ requestRender();
4072
+ }
4073
+ }
4074
+ /**
4075
+ * Removes every selected node, and the connections that touched them.
4076
+ */
4077
+ removeSelectedNodes() {
4078
+ const removed = this.getSelectedNodes();
4079
+ for (const node of removed) {
4080
+ this.removeNode(node);
4081
+ }
4082
+ return removed;
4083
+ }
3844
4084
  #selectNodeByIndex(nodeIndex, unselectOthers) {
3845
4085
  this.#selectNode(this.#nodes[nodeIndex], unselectOthers);
3846
4086
  }
@@ -3878,8 +4118,16 @@ var NodeSubsystem2 = class {
3878
4118
  this.#nodeHovering = -1;
3879
4119
  this.#boxSelectionNodes.Clear();
3880
4120
  const selectedBox_Screenspace = this.#boxSelectionScreenspaceBox(camera);
4121
+ const canvas = {
4122
+ Position: { x: 0, y: 0 },
4123
+ Size: { x: ctx.canvas.width, y: ctx.canvas.height }
4124
+ };
3881
4125
  for (let i = 0; i < this.#nodes.length; i++) {
3882
4126
  let state = 0 /* Idle */;
4127
+ const nodeBounds = this.#nodes[i].calculateBounds(ctx, camera);
4128
+ if (!BoxIntersection(nodeBounds, canvas) && this.#nodes[i].layoutPortsWithoutDrawing(nodeBounds, camera.zoom)) {
4129
+ continue;
4130
+ }
3883
4131
  if (mousePosition !== void 0 && !this.#boxSelect) {
3884
4132
  const intersection = this.#nodes[i].inBounds(ctx, camera, mousePosition);
3885
4133
  if (intersection.Node !== void 0 && intersection.PortIndex === void 0 && intersection.Widget === void 0) {
@@ -3900,8 +4148,8 @@ var NodeSubsystem2 = class {
3900
4148
  };
3901
4149
  }
3902
4150
  } else if (this.#boxSelect) {
3903
- const nodeBounds = this.#nodes[i].calculateBounds(ctx, camera);
3904
- if (BoxIntersection(selectedBox_Screenspace, nodeBounds)) {
4151
+ const nodeBounds2 = this.#nodes[i].calculateBounds(ctx, camera);
4152
+ if (BoxIntersection(selectedBox_Screenspace, nodeBounds2)) {
3905
4153
  state = 1 /* MouseOver */;
3906
4154
  this.#boxSelectionNodes.Push(i);
3907
4155
  }
@@ -3923,12 +4171,16 @@ var NodeSubsystem2 = class {
3923
4171
  render(ctx, camera, mousePosition) {
3924
4172
  this.#lastCtx = ctx;
3925
4173
  this.#cursor = "default" /* Default */;
4174
+ const awaitingLayout = this.#nodes.some((node) => !node.hasBeenLaidOut());
3926
4175
  TimeExecution("Render_Connections", () => {
3927
4176
  this.#renderConnections(ctx, camera, mousePosition);
3928
4177
  });
3929
4178
  TimeExecution("Render_Nodes", () => {
3930
4179
  this.#renderNodes(ctx, camera, mousePosition);
3931
4180
  });
4181
+ if (awaitingLayout) {
4182
+ requestRender();
4183
+ }
3932
4184
  if (this.#boxSelect) {
3933
4185
  const box = this.#boxSelectionScreenspaceBox(camera);
3934
4186
  ctx.setLineDash([Theme.BoxSelect.LineDashLength]);
@@ -4204,6 +4456,7 @@ var FlowNode = class {
4204
4456
  return;
4205
4457
  }
4206
4458
  this.#selected = true;
4459
+ requestRender();
4207
4460
  for (let i = 0; i < this.#onSelect.length; i++) {
4208
4461
  this.#onSelect[i]();
4209
4462
  }
@@ -4249,6 +4502,7 @@ var FlowNode = class {
4249
4502
  return;
4250
4503
  }
4251
4504
  this.#data[name] = value;
4505
+ requestRender();
4252
4506
  for (let i = 0; i < this.#registeredAnyPropertyChangeCallbacks.length; i++) {
4253
4507
  this.#registeredAnyPropertyChangeCallbacks[i](name, oldValue, value);
4254
4508
  }
@@ -4509,6 +4763,7 @@ var FlowNode = class {
4509
4763
  return;
4510
4764
  }
4511
4765
  this.#selected = false;
4766
+ requestRender();
4512
4767
  for (let i = 0; i < this.#onUnselect.length; i++) {
4513
4768
  this.#onUnselect[i]();
4514
4769
  }
@@ -4538,6 +4793,7 @@ var FlowNode = class {
4538
4793
  }
4539
4794
  setPosition(position) {
4540
4795
  CopyVector2(this.#position, position);
4796
+ requestRender();
4541
4797
  }
4542
4798
  getPosition() {
4543
4799
  return this.#position;
@@ -4600,10 +4856,12 @@ var FlowNode = class {
4600
4856
  }
4601
4857
  addWidget(widget) {
4602
4858
  this.#widgets.push(widget);
4859
+ requestRender();
4603
4860
  }
4604
4861
  insertWidget(index, widget) {
4605
4862
  const at = Math.max(0, Math.min(index, this.#widgets.length));
4606
4863
  this.#widgets.splice(at, 0, widget);
4864
+ requestRender();
4607
4865
  }
4608
4866
  removeWidget(widget) {
4609
4867
  const index = this.#widgets.indexOf(widget, 0);
@@ -4611,12 +4869,15 @@ var FlowNode = class {
4611
4869
  throw new Error("node does not contain widget");
4612
4870
  }
4613
4871
  this.#widgets.splice(index, 1);
4872
+ requestRender();
4614
4873
  }
4615
4874
  removeWidgetAt(index) {
4616
4875
  if (index < 0 || index >= this.#widgets.length) {
4617
4876
  return void 0;
4618
4877
  }
4619
- return this.#widgets.splice(index, 1)[0];
4878
+ const removed = this.#widgets.splice(index, 1)[0];
4879
+ requestRender();
4880
+ return removed;
4620
4881
  }
4621
4882
  moveWidget(from, to) {
4622
4883
  if (from < 0 || from >= this.#widgets.length) {
@@ -4627,9 +4888,11 @@ var FlowNode = class {
4627
4888
  }
4628
4889
  const [widget] = this.#widgets.splice(from, 1);
4629
4890
  this.#widgets.splice(to, 0, widget);
4891
+ requestRender();
4630
4892
  }
4631
4893
  clearWidgets() {
4632
4894
  this.#widgets.length = 0;
4895
+ requestRender();
4633
4896
  }
4634
4897
  getWidget(index) {
4635
4898
  return this.#widgets[index];
@@ -4701,6 +4964,7 @@ var FlowNode = class {
4701
4964
  return;
4702
4965
  }
4703
4966
  this.#title.set(cleaned);
4967
+ requestRender();
4704
4968
  for (let i = 0; i < this.#titleChangeCallback.length; i++) {
4705
4969
  const callback = this.#titleChangeCallback[i];
4706
4970
  callback(this, old, cleaned);
@@ -4719,6 +4983,7 @@ var FlowNode = class {
4719
4983
  }
4720
4984
  const old = this.#infoText;
4721
4985
  this.#infoText = cleaned;
4986
+ requestRender();
4722
4987
  for (let i = 0; i < this.#infoChangeCallback.length; i++) {
4723
4988
  const callback = this.#infoChangeCallback[i];
4724
4989
  callback(this, old, cleaned);
@@ -4758,7 +5023,12 @@ var FlowNode = class {
4758
5023
  clearMessages() {
4759
5024
  this.#messages = [];
4760
5025
  }
5026
+ hasBeenLaidOut() {
5027
+ return this.#laidOut;
5028
+ }
5029
+ #laidOut = false;
4761
5030
  render(ctx, camera, state, mousePosition, postProcess) {
5031
+ this.#laidOut = true;
4762
5032
  VectorPool.run(() => {
4763
5033
  const tempMeasurement = VectorPool.get();
4764
5034
  this.#inputPortPositions.Clear();
@@ -4894,8 +5164,58 @@ var FlowNode = class {
4894
5164
  const message = this.#messages[i];
4895
5165
  messageStart.y += message.render(ctx, camera.zoom, messageStart, !(state === 0 /* Idle */)) + 10 * camera.zoom;
4896
5166
  }
5167
+ this.#rememberPortOffsets(nodeBounds, camera.zoom);
4897
5168
  });
4898
5169
  }
5170
+ #portOffsets = { input: [], output: [] };
5171
+ #rememberPortOffsets(bounds, zoom) {
5172
+ if (zoom <= 0) {
5173
+ return;
5174
+ }
5175
+ const record = (positions, into) => {
5176
+ into.length = 0;
5177
+ for (let i = 0; i < positions.Count(); i++) {
5178
+ const box = positions.At(i);
5179
+ into.push({
5180
+ Position: {
5181
+ x: (box.Position.x - bounds.Position.x) / zoom,
5182
+ y: (box.Position.y - bounds.Position.y) / zoom
5183
+ },
5184
+ Size: {
5185
+ x: box.Size.x / zoom,
5186
+ y: box.Size.y / zoom
5187
+ }
5188
+ });
5189
+ }
5190
+ };
5191
+ record(this.#inputPortPositions, this.#portOffsets.input);
5192
+ record(this.#outputPortPositions, this.#portOffsets.output);
5193
+ }
5194
+ layoutPortsWithoutDrawing(bounds, zoom) {
5195
+ if (this.#portOffsets.input.length === 0 && this.#portOffsets.output.length === 0) {
5196
+ return false;
5197
+ }
5198
+ const project = (offsets, into) => {
5199
+ into.Clear();
5200
+ for (let i = 0; i < offsets.length; i++) {
5201
+ const offset = offsets[i];
5202
+ into.Push({
5203
+ Position: {
5204
+ x: bounds.Position.x + offset.Position.x * zoom,
5205
+ y: bounds.Position.y + offset.Position.y * zoom
5206
+ },
5207
+ Size: {
5208
+ x: offset.Size.x * zoom,
5209
+ y: offset.Size.y * zoom
5210
+ }
5211
+ });
5212
+ }
5213
+ };
5214
+ project(this.#portOffsets.input, this.#inputPortPositions);
5215
+ project(this.#portOffsets.output, this.#outputPortPositions);
5216
+ this.#widgetPositions.Clear();
5217
+ return true;
5218
+ }
4899
5219
  };
4900
5220
 
4901
5221
  // src/markdown/token.ts
@@ -5036,7 +5356,10 @@ function onFontsLoaded(callback) {
5036
5356
  if (typeof document === "undefined" || document.fonts === void 0) {
5037
5357
  return;
5038
5358
  }
5039
- document.fonts.addEventListener("loadingdone", () => callback());
5359
+ document.fonts.addEventListener("loadingdone", () => {
5360
+ callback();
5361
+ requestRender();
5362
+ });
5040
5363
  }
5041
5364
  var CodeBlockEntry = class {
5042
5365
  #text;
@@ -5786,6 +6109,7 @@ var NoteSubsystem = class {
5786
6109
  return;
5787
6110
  }
5788
6111
  this.#notes.push(note);
6112
+ requestRender();
5789
6113
  for (let i = 0; i < this.#onNoteAddedCallbacks.length; i++) {
5790
6114
  this.#onNoteAddedCallbacks[i](note);
5791
6115
  }
@@ -5908,6 +6232,7 @@ var NoteSubsystem = class {
5908
6232
  if (index > -1) {
5909
6233
  const noteRemoved = this.#notes[index];
5910
6234
  this.#notes.splice(index, 1);
6235
+ requestRender();
5911
6236
  for (let i = 0; i < this.#onNoteRemovedCallbacks.length; i++) {
5912
6237
  this.#onNoteRemovedCallbacks[i](noteRemoved);
5913
6238
  }
@@ -6244,6 +6569,124 @@ var QuickMenu = class {
6244
6569
  }
6245
6570
  };
6246
6571
 
6572
+ // src/minimap.ts
6573
+ var Minimap = class {
6574
+ #enabled;
6575
+ #width;
6576
+ #height;
6577
+ #margin;
6578
+ #backgroundColor;
6579
+ #borderColor;
6580
+ #nodeColor;
6581
+ #viewportColor;
6582
+ #box = { Position: { x: 0, y: 0 }, Size: { x: 0, y: 0 } };
6583
+ #scale = 1;
6584
+ #originX = 0;
6585
+ #originY = 0;
6586
+ // The padding the last frame was laid out with, so a click maps back
6587
+ // through the same geometry it was drawn with.
6588
+ #padding = Theme.Minimap.Padding;
6589
+ #drawn = false;
6590
+ constructor(config) {
6591
+ this.#enabled = config?.enabled === true;
6592
+ this.#width = config?.width === void 0 ? 200 : config.width;
6593
+ this.#height = config?.height === void 0 ? 150 : config.height;
6594
+ this.#margin = config?.margin === void 0 ? 16 : config.margin;
6595
+ this.#backgroundColor = config?.backgroundColor;
6596
+ this.#borderColor = config?.borderColor;
6597
+ this.#nodeColor = config?.nodeColor;
6598
+ this.#viewportColor = config?.viewportColor;
6599
+ }
6600
+ enabled() {
6601
+ return this.#enabled;
6602
+ }
6603
+ setEnabled(enabled) {
6604
+ this.#enabled = enabled;
6605
+ this.#drawn = false;
6606
+ }
6607
+ contains(position) {
6608
+ return this.#enabled && this.#drawn && InBox(this.#box, position);
6609
+ }
6610
+ graphPositionOf(position, out) {
6611
+ out.x = (position.x - this.#box.Position.x - this.#padding) / this.#scale + this.#originX;
6612
+ out.y = (position.y - this.#box.Position.y - this.#padding) / this.#scale + this.#originY;
6613
+ }
6614
+ render(ctx, canvas, camera, nodes) {
6615
+ if (!this.#enabled) {
6616
+ this.#drawn = false;
6617
+ return;
6618
+ }
6619
+ const flat = new Camera();
6620
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
6621
+ const boxes = [];
6622
+ for (let i = 0; i < nodes.length; i++) {
6623
+ const bounds = nodes[i].calculateBounds(ctx, flat);
6624
+ boxes.push(bounds);
6625
+ minX = Math.min(minX, bounds.Position.x);
6626
+ minY = Math.min(minY, bounds.Position.y);
6627
+ maxX = Math.max(maxX, bounds.Position.x + bounds.Size.x);
6628
+ maxY = Math.max(maxY, bounds.Position.y + bounds.Size.y);
6629
+ }
6630
+ const topLeft = { x: 0, y: 0 };
6631
+ const bottomRight = { x: 0, y: 0 };
6632
+ camera.screenSpaceToGraphSpace({ x: 0, y: 0 }, topLeft);
6633
+ camera.screenSpaceToGraphSpace({ x: canvas.width, y: canvas.height }, bottomRight);
6634
+ minX = Math.min(minX, topLeft.x);
6635
+ minY = Math.min(minY, topLeft.y);
6636
+ maxX = Math.max(maxX, bottomRight.x);
6637
+ maxY = Math.max(maxY, bottomRight.y);
6638
+ if (!isFinite(minX) || maxX <= minX || maxY <= minY) {
6639
+ this.#drawn = false;
6640
+ return;
6641
+ }
6642
+ this.#box.Size.x = this.#width;
6643
+ this.#box.Size.y = this.#height;
6644
+ this.#box.Position.x = canvas.width - this.#width - this.#margin;
6645
+ this.#box.Position.y = canvas.height - this.#height - this.#margin;
6646
+ this.#padding = Theme.Minimap.Padding;
6647
+ const usableWidth = this.#width - this.#padding * 2;
6648
+ const usableHeight = this.#height - this.#padding * 2;
6649
+ this.#scale = Math.min(usableWidth / (maxX - minX), usableHeight / (maxY - minY));
6650
+ this.#originX = minX;
6651
+ this.#originY = minY;
6652
+ const left = this.#box.Position.x + this.#padding;
6653
+ const top = this.#box.Position.y + this.#padding;
6654
+ const toMapX = (x) => left + (x - minX) * this.#scale;
6655
+ const toMapY = (y) => top + (y - minY) * this.#scale;
6656
+ ctx.fillStyle = this.#backgroundColor ?? Theme.Minimap.BackgroundColor;
6657
+ ctx.strokeStyle = this.#borderColor ?? Theme.Minimap.BorderColor;
6658
+ ctx.lineWidth = 1;
6659
+ ctx.beginPath();
6660
+ ctx.roundRect(
6661
+ this.#box.Position.x,
6662
+ this.#box.Position.y,
6663
+ this.#width,
6664
+ this.#height,
6665
+ Theme.Minimap.BorderRadius
6666
+ );
6667
+ ctx.fill();
6668
+ ctx.stroke();
6669
+ ctx.fillStyle = this.#nodeColor ?? Theme.Minimap.NodeColor;
6670
+ for (let i = 0; i < boxes.length; i++) {
6671
+ const b = boxes[i];
6672
+ ctx.fillRect(
6673
+ toMapX(b.Position.x),
6674
+ toMapY(b.Position.y),
6675
+ Math.max(1, b.Size.x * this.#scale),
6676
+ Math.max(1, b.Size.y * this.#scale)
6677
+ );
6678
+ }
6679
+ ctx.fillStyle = this.#viewportColor ?? Theme.Minimap.ViewportColor;
6680
+ ctx.fillRect(
6681
+ toMapX(topLeft.x),
6682
+ toMapY(topLeft.y),
6683
+ Math.max(1, (bottomRight.x - topLeft.x) * this.#scale),
6684
+ Math.max(1, (bottomRight.y - topLeft.y) * this.#scale)
6685
+ );
6686
+ this.#drawn = true;
6687
+ }
6688
+ };
6689
+
6247
6690
  // src/graph.ts
6248
6691
  function BuildBackgroundRenderer(backgroundColor) {
6249
6692
  return (canvas, context, position, scale) => {
@@ -6381,7 +6824,13 @@ var NodeFlowGraph = class {
6381
6824
  const backgroundColor = config?.backgroundColor === void 0 ? Theme.Graph.BackgroundColor : config.backgroundColor;
6382
6825
  this.#backgroundRenderer = BuildBackgroundRenderer(backgroundColor);
6383
6826
  }
6384
- window.requestAnimationFrame(this.#render.bind(this));
6827
+ this.#minimap = new Minimap(config?.minimap);
6828
+ this.#mainNodeSubsystem.setConnectionReleasedHandler((dangling) => {
6829
+ this.#openScopedMenu(dangling);
6830
+ });
6831
+ this.#watchCanvasSize();
6832
+ this.#stopRendering = onRenderRequested(this.#render.bind(this));
6833
+ requestRender();
6385
6834
  new MouseObserver(
6386
6835
  this.#canvas,
6387
6836
  this.#mouseDragEvent.bind(this),
@@ -6399,39 +6848,90 @@ var NodeFlowGraph = class {
6399
6848
  document.addEventListener(
6400
6849
  "keydown",
6401
6850
  (e) => {
6402
- if (document.activeElement) {
6403
- if (["INPUT", "TEXTAREA"].includes(document.activeElement.tagName)) return;
6404
- }
6405
- if (this.#openQuickMenu) {
6406
- if (e.code === "Escape") {
6407
- this.#openQuickMenu = null;
6408
- return;
6409
- }
6410
- if (e.code === "Enter") {
6411
- this.#openQuickMenu.Menu.execute();
6412
- this.#openQuickMenu = null;
6413
- return;
6414
- }
6415
- this.#openQuickMenu.Menu.keyboardEvent(e);
6851
+ this.#keyDown(e);
6852
+ requestRender();
6853
+ }
6854
+ );
6855
+ }
6856
+ #copyCallbacks = [];
6857
+ #pasteCallbacks = [];
6858
+ addCopyListener(callback) {
6859
+ this.#copyCallbacks.push(callback);
6860
+ }
6861
+ addPasteListener(callback) {
6862
+ this.#pasteCallbacks.push(callback);
6863
+ }
6864
+ #raiseCopy() {
6865
+ const nodes = this.#mainNodeSubsystem.getSelectedNodes();
6866
+ if (nodes.length === 0) {
6867
+ return;
6868
+ }
6869
+ const connections = this.#mainNodeSubsystem.connectionsWithin(nodes);
6870
+ this.#copyCallbacks.forEach((callback) => callback(nodes, connections));
6871
+ }
6872
+ #raisePaste() {
6873
+ const position = this.#mousePosition === void 0 ? { x: 0, y: 0 } : this.#sceenPositionToGraphPosition(this.#mousePosition);
6874
+ this.#pasteCallbacks.forEach((callback) => callback(position));
6875
+ }
6876
+ #deleteKey(e) {
6877
+ return e.code === "Delete" || e.code === "Backspace" || e.key === "Delete" || e.key === "Backspace";
6878
+ }
6879
+ #keyDown(e) {
6880
+ if (document.activeElement) {
6881
+ if (["INPUT", "TEXTAREA"].includes(document.activeElement.tagName)) return;
6882
+ }
6883
+ if (this.#deleteKey(e)) {
6884
+ if (this.#mainNodeSubsystem.getSelectedNodes().length > 0) {
6885
+ e.preventDefault();
6886
+ this.#mainNodeSubsystem.removeSelectedNodes();
6887
+ return;
6888
+ }
6889
+ }
6890
+ if (e.ctrlKey || e.metaKey) {
6891
+ const letter = e.code.startsWith("Key") ? e.code.substring(3).toLowerCase() : (e.key ?? "").toLowerCase();
6892
+ switch (letter) {
6893
+ case "c":
6894
+ this.#raiseCopy();
6416
6895
  return;
6417
- }
6418
- const spacePressed = e.key == " " || e.code == "Space";
6419
- if (!spacePressed) {
6896
+ case "v":
6897
+ this.#raisePaste();
6420
6898
  return;
6421
- }
6422
- if (!this.#mousePosition) {
6899
+ // duplicate
6900
+ case "d":
6901
+ e.preventDefault();
6902
+ this.#raiseCopy();
6903
+ this.#raisePaste();
6423
6904
  return;
6424
- }
6425
- const contextMenuPosition = this.#sceenPositionToGraphPosition(this.#mousePosition);
6426
- let items = this.#mainNodeSubsystem.nodeFactory().newNodeSubmenus(this.#mainNodeSubsystem, contextMenuPosition);
6427
- this.#openQuickMenu = {
6428
- Menu: new QuickMenu({
6429
- subMenus: items
6430
- }),
6431
- Position: contextMenuPosition
6432
- };
6433
6905
  }
6434
- );
6906
+ }
6907
+ if (this.#openQuickMenu) {
6908
+ if (e.code === "Escape") {
6909
+ this.#openQuickMenu = null;
6910
+ return;
6911
+ }
6912
+ if (e.code === "Enter") {
6913
+ this.#openQuickMenu.Menu.execute();
6914
+ this.#openQuickMenu = null;
6915
+ return;
6916
+ }
6917
+ this.#openQuickMenu.Menu.keyboardEvent(e);
6918
+ return;
6919
+ }
6920
+ const spacePressed = e.key == " " || e.code == "Space";
6921
+ if (!spacePressed) {
6922
+ return;
6923
+ }
6924
+ if (!this.#mousePosition) {
6925
+ return;
6926
+ }
6927
+ const contextMenuPosition = this.#sceenPositionToGraphPosition(this.#mousePosition);
6928
+ let items = this.#mainNodeSubsystem.nodeFactory().newNodeSubmenus(this.#mainNodeSubsystem, contextMenuPosition);
6929
+ this.#openQuickMenu = {
6930
+ Menu: new QuickMenu({
6931
+ subMenus: items
6932
+ }),
6933
+ Position: contextMenuPosition
6934
+ };
6435
6935
  }
6436
6936
  #fileDrop(file) {
6437
6937
  if (this.currentView().fileDrop(file)) {
@@ -6497,8 +6997,25 @@ var NodeFlowGraph = class {
6497
6997
  this.#openQuickMenu = null;
6498
6998
  this.#contextMenuEntryHovering = null;
6499
6999
  this.#mousePosition = mousePosition;
7000
+ if (this.#minimap.contains(mousePosition)) {
7001
+ this.#lookAtFromMinimap(mousePosition);
7002
+ this.#draggingMinimap = true;
7003
+ return;
7004
+ }
6500
7005
  this.currentView().clickStart(mousePosition, this.#camera, ctrlKey);
6501
7006
  }
7007
+ #draggingMinimap = false;
7008
+ #lookAtFromMinimap(mousePosition) {
7009
+ const target = { x: 0, y: 0 };
7010
+ this.#minimap.graphPositionOf(mousePosition, target);
7011
+ this.#camera.position.x = -(target.x * this.#camera.zoom) + this.#canvas.width / 2;
7012
+ this.#camera.position.y = -(target.y * this.#camera.zoom) + this.#canvas.height / 2;
7013
+ requestRender();
7014
+ }
7015
+ /** The minimap, so it can be turned on and sized by the host. */
7016
+ minimap() {
7017
+ return this.#minimap;
7018
+ }
6502
7019
  currentView() {
6503
7020
  return this.#views[this.#currentView];
6504
7021
  }
@@ -6518,6 +7035,12 @@ var NodeFlowGraph = class {
6518
7035
  getSelectedNodes() {
6519
7036
  return this.#mainNodeSubsystem.getSelectedNodes();
6520
7037
  }
7038
+ unselectAllNodes() {
7039
+ this.#mainNodeSubsystem.unselectAllNodes();
7040
+ }
7041
+ removeSelectedNodes() {
7042
+ return this.#mainNodeSubsystem.removeSelectedNodes();
7043
+ }
6521
7044
  /**
6522
7045
  * Returns a copy of the current camera orientation (screen-space pan + zoom).
6523
7046
  */
@@ -6633,10 +7156,37 @@ var NodeFlowGraph = class {
6633
7156
  Position: contextMenuPosition
6634
7157
  };
6635
7158
  }
7159
+ #openScopedMenu(dangling) {
7160
+ if (this.#mousePosition === void 0) {
7161
+ return;
7162
+ }
7163
+ const position = this.#sceenPositionToGraphPosition(this.#mousePosition);
7164
+ const menu = this.#mainNodeSubsystem.nodeFactory().openMenu(
7165
+ this.#mainNodeSubsystem,
7166
+ position,
7167
+ {
7168
+ needs: dangling.needs,
7169
+ types: dangling.types,
7170
+ onCreated: dangling.connectTo
7171
+ }
7172
+ );
7173
+ this.#openedContextMenu = {
7174
+ Menu: new ContextMenu(menu),
7175
+ Position: position
7176
+ };
7177
+ requestRender();
7178
+ }
6636
7179
  #clickEnd() {
7180
+ this.#draggingMinimap = false;
6637
7181
  this.currentView().clickEnd();
6638
7182
  }
6639
7183
  #mouseDragEvent(delta) {
7184
+ if (this.#draggingMinimap) {
7185
+ if (this.#mousePosition !== void 0) {
7186
+ this.#lookAtFromMinimap(this.#mousePosition);
7187
+ }
7188
+ return;
7189
+ }
6640
7190
  let draggingSomething = this.currentView().mouseDragEvent(delta, this.#camera.zoom);
6641
7191
  if (!draggingSomething) {
6642
7192
  this.#camera.position.x += delta.x;
@@ -6645,12 +7195,45 @@ var NodeFlowGraph = class {
6645
7195
  }
6646
7196
  #lastFrameCursor;
6647
7197
  #cursor;
6648
- #render() {
6649
- if (this.#canvas.parentNode !== null) {
6650
- var rect = this.#canvas.parentNode.getBoundingClientRect();
6651
- this.#canvas.width = rect.width;
6652
- this.#canvas.height = rect.height;
7198
+ #minimap;
7199
+ #stopRendering;
7200
+ #sizeObserver;
7201
+ #matchCanvasToParent() {
7202
+ const parent = this.#canvas.parentNode;
7203
+ if (parent === null) {
7204
+ return;
7205
+ }
7206
+ const rect = parent.getBoundingClientRect();
7207
+ if (this.#canvas.width === rect.width && this.#canvas.height === rect.height) {
7208
+ return;
6653
7209
  }
7210
+ this.#canvas.width = rect.width;
7211
+ this.#canvas.height = rect.height;
7212
+ }
7213
+ #watchCanvasSize() {
7214
+ this.#matchCanvasToParent();
7215
+ const parent = this.#canvas.parentNode;
7216
+ if (parent === null || typeof ResizeObserver === "undefined") {
7217
+ return;
7218
+ }
7219
+ this.#sizeObserver = new ResizeObserver(() => {
7220
+ this.#matchCanvasToParent();
7221
+ requestRender();
7222
+ });
7223
+ this.#sizeObserver.observe(parent);
7224
+ }
7225
+ requestRender() {
7226
+ requestRender();
7227
+ }
7228
+ renderContinuously() {
7229
+ return renderContinuously();
7230
+ }
7231
+ dispose() {
7232
+ this.#stopRendering();
7233
+ this.#sizeObserver?.disconnect();
7234
+ }
7235
+ #render() {
7236
+ this.#matchCanvasToParent();
6654
7237
  this.#cursor = "default" /* Default */;
6655
7238
  TimeExecution("Render_Background", this.#renderBackground.bind(this));
6656
7239
  TimeExecution("Render_View_" + this.#currentView, () => {
@@ -6659,13 +7242,15 @@ var NodeFlowGraph = class {
6659
7242
  this.#cursor = results?.cursorStyle;
6660
7243
  }
6661
7244
  });
7245
+ TimeExecution("Render_Minimap", () => {
7246
+ this.#minimap.render(this.#ctx, this.#canvas, this.#camera, this.#mainNodeSubsystem.getNodes());
7247
+ });
6662
7248
  TimeExecution("Render_Context", this.#renderContextMenu.bind(this));
6663
7249
  TimeExecution("Render_QuickMenu", this.#renderQuickMenu.bind(this));
6664
7250
  if (this.#lastFrameCursor !== this.#cursor) {
6665
7251
  this.#canvas.style.cursor = this.#cursor;
6666
7252
  }
6667
7253
  this.#lastFrameCursor = this.#cursor;
6668
- window.requestAnimationFrame(this.#render.bind(this));
6669
7254
  }
6670
7255
  #renderBackground() {
6671
7256
  this.#backgroundRenderer(this.#canvas, this.#ctx, this.#camera.position, this.#camera.zoom);
@@ -6706,6 +7291,7 @@ export {
6706
7291
  GlobalWidgetFactory,
6707
7292
  ImageWidget,
6708
7293
  MessageType,
7294
+ Minimap,
6709
7295
  NodeFactory,
6710
7296
  NodeFlowGraph,
6711
7297
  NodeState,
@@ -6719,6 +7305,10 @@ export {
6719
7305
  Theme,
6720
7306
  ToggleWidget,
6721
7307
  WidgetFactory,
6722
- portsCompatible
7308
+ portConfigTypes,
7309
+ portsCompatible,
7310
+ renderContinuously,
7311
+ requestRender,
7312
+ typeSetsCompatible
6723
7313
  };
6724
7314
  //# sourceMappingURL=index.mjs.map