Browse Source

try topology with graph

clovis 1 month ago
parent
commit
2dd22b2977

+ 75 - 501
package-lock.json

@@ -11,11 +11,11 @@
         "@emotion/react": "^11.14.0",
         "@emotion/styled": "^11.14.1",
         "@mui/material": "^7.3.5",
+        "@xyflow/react": "^12.11.3",
         "pako": "^2.1.0",
         "react": "^19.2.0",
         "react-dom": "^19.2.0",
         "react-router": "^8.3.0",
-        "reactflow": "^11.11.4",
         "sharp": "^0.35.3",
         "simplebar-react": "^3.3.2",
         "zustand": "^5.0.14"
@@ -1417,276 +1417,6 @@
         "url": "https://opencollective.com/popperjs"
       }
     },
-    "node_modules/@reactflow/background": {
-      "version": "11.3.14",
-      "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz",
-      "integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==",
-      "license": "MIT",
-      "dependencies": {
-        "@reactflow/core": "11.11.4",
-        "classcat": "^5.0.3",
-        "zustand": "^4.4.1"
-      },
-      "peerDependencies": {
-        "react": ">=17",
-        "react-dom": ">=17"
-      }
-    },
-    "node_modules/@reactflow/background/node_modules/zustand": {
-      "version": "4.5.7",
-      "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
-      "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
-      "license": "MIT",
-      "dependencies": {
-        "use-sync-external-store": "^1.2.2"
-      },
-      "engines": {
-        "node": ">=12.7.0"
-      },
-      "peerDependencies": {
-        "@types/react": ">=16.8",
-        "immer": ">=9.0.6",
-        "react": ">=16.8"
-      },
-      "peerDependenciesMeta": {
-        "@types/react": {
-          "optional": true
-        },
-        "immer": {
-          "optional": true
-        },
-        "react": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/@reactflow/controls": {
-      "version": "11.2.14",
-      "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz",
-      "integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==",
-      "license": "MIT",
-      "dependencies": {
-        "@reactflow/core": "11.11.4",
-        "classcat": "^5.0.3",
-        "zustand": "^4.4.1"
-      },
-      "peerDependencies": {
-        "react": ">=17",
-        "react-dom": ">=17"
-      }
-    },
-    "node_modules/@reactflow/controls/node_modules/zustand": {
-      "version": "4.5.7",
-      "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
-      "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
-      "license": "MIT",
-      "dependencies": {
-        "use-sync-external-store": "^1.2.2"
-      },
-      "engines": {
-        "node": ">=12.7.0"
-      },
-      "peerDependencies": {
-        "@types/react": ">=16.8",
-        "immer": ">=9.0.6",
-        "react": ">=16.8"
-      },
-      "peerDependenciesMeta": {
-        "@types/react": {
-          "optional": true
-        },
-        "immer": {
-          "optional": true
-        },
-        "react": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/@reactflow/core": {
-      "version": "11.11.4",
-      "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz",
-      "integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==",
-      "license": "MIT",
-      "dependencies": {
-        "@types/d3": "^7.4.0",
-        "@types/d3-drag": "^3.0.1",
-        "@types/d3-selection": "^3.0.3",
-        "@types/d3-zoom": "^3.0.1",
-        "classcat": "^5.0.3",
-        "d3-drag": "^3.0.0",
-        "d3-selection": "^3.0.0",
-        "d3-zoom": "^3.0.0",
-        "zustand": "^4.4.1"
-      },
-      "peerDependencies": {
-        "react": ">=17",
-        "react-dom": ">=17"
-      }
-    },
-    "node_modules/@reactflow/core/node_modules/zustand": {
-      "version": "4.5.7",
-      "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
-      "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
-      "license": "MIT",
-      "dependencies": {
-        "use-sync-external-store": "^1.2.2"
-      },
-      "engines": {
-        "node": ">=12.7.0"
-      },
-      "peerDependencies": {
-        "@types/react": ">=16.8",
-        "immer": ">=9.0.6",
-        "react": ">=16.8"
-      },
-      "peerDependenciesMeta": {
-        "@types/react": {
-          "optional": true
-        },
-        "immer": {
-          "optional": true
-        },
-        "react": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/@reactflow/minimap": {
-      "version": "11.7.14",
-      "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz",
-      "integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==",
-      "license": "MIT",
-      "dependencies": {
-        "@reactflow/core": "11.11.4",
-        "@types/d3-selection": "^3.0.3",
-        "@types/d3-zoom": "^3.0.1",
-        "classcat": "^5.0.3",
-        "d3-selection": "^3.0.0",
-        "d3-zoom": "^3.0.0",
-        "zustand": "^4.4.1"
-      },
-      "peerDependencies": {
-        "react": ">=17",
-        "react-dom": ">=17"
-      }
-    },
-    "node_modules/@reactflow/minimap/node_modules/zustand": {
-      "version": "4.5.7",
-      "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
-      "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
-      "license": "MIT",
-      "dependencies": {
-        "use-sync-external-store": "^1.2.2"
-      },
-      "engines": {
-        "node": ">=12.7.0"
-      },
-      "peerDependencies": {
-        "@types/react": ">=16.8",
-        "immer": ">=9.0.6",
-        "react": ">=16.8"
-      },
-      "peerDependenciesMeta": {
-        "@types/react": {
-          "optional": true
-        },
-        "immer": {
-          "optional": true
-        },
-        "react": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/@reactflow/node-resizer": {
-      "version": "2.2.14",
-      "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz",
-      "integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==",
-      "license": "MIT",
-      "dependencies": {
-        "@reactflow/core": "11.11.4",
-        "classcat": "^5.0.4",
-        "d3-drag": "^3.0.0",
-        "d3-selection": "^3.0.0",
-        "zustand": "^4.4.1"
-      },
-      "peerDependencies": {
-        "react": ">=17",
-        "react-dom": ">=17"
-      }
-    },
-    "node_modules/@reactflow/node-resizer/node_modules/zustand": {
-      "version": "4.5.7",
-      "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
-      "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
-      "license": "MIT",
-      "dependencies": {
-        "use-sync-external-store": "^1.2.2"
-      },
-      "engines": {
-        "node": ">=12.7.0"
-      },
-      "peerDependencies": {
-        "@types/react": ">=16.8",
-        "immer": ">=9.0.6",
-        "react": ">=16.8"
-      },
-      "peerDependenciesMeta": {
-        "@types/react": {
-          "optional": true
-        },
-        "immer": {
-          "optional": true
-        },
-        "react": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/@reactflow/node-toolbar": {
-      "version": "1.3.14",
-      "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz",
-      "integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==",
-      "license": "MIT",
-      "dependencies": {
-        "@reactflow/core": "11.11.4",
-        "classcat": "^5.0.3",
-        "zustand": "^4.4.1"
-      },
-      "peerDependencies": {
-        "react": ">=17",
-        "react-dom": ">=17"
-      }
-    },
-    "node_modules/@reactflow/node-toolbar/node_modules/zustand": {
-      "version": "4.5.7",
-      "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
-      "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
-      "license": "MIT",
-      "dependencies": {
-        "use-sync-external-store": "^1.2.2"
-      },
-      "engines": {
-        "node": ">=12.7.0"
-      },
-      "peerDependencies": {
-        "@types/react": ">=16.8",
-        "immer": ">=9.0.6",
-        "react": ">=16.8"
-      },
-      "peerDependenciesMeta": {
-        "@types/react": {
-          "optional": true
-        },
-        "immer": {
-          "optional": true
-        },
-        "react": {
-          "optional": true
-        }
-      }
-    },
     "node_modules/@rolldown/binding-android-arm64": {
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.2.tgz",
@@ -1980,102 +1710,12 @@
         "assertion-error": "^2.0.1"
       }
     },
-    "node_modules/@types/d3": {
-      "version": "7.4.3",
-      "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
-      "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
-      "license": "MIT",
-      "dependencies": {
-        "@types/d3-array": "*",
-        "@types/d3-axis": "*",
-        "@types/d3-brush": "*",
-        "@types/d3-chord": "*",
-        "@types/d3-color": "*",
-        "@types/d3-contour": "*",
-        "@types/d3-delaunay": "*",
-        "@types/d3-dispatch": "*",
-        "@types/d3-drag": "*",
-        "@types/d3-dsv": "*",
-        "@types/d3-ease": "*",
-        "@types/d3-fetch": "*",
-        "@types/d3-force": "*",
-        "@types/d3-format": "*",
-        "@types/d3-geo": "*",
-        "@types/d3-hierarchy": "*",
-        "@types/d3-interpolate": "*",
-        "@types/d3-path": "*",
-        "@types/d3-polygon": "*",
-        "@types/d3-quadtree": "*",
-        "@types/d3-random": "*",
-        "@types/d3-scale": "*",
-        "@types/d3-scale-chromatic": "*",
-        "@types/d3-selection": "*",
-        "@types/d3-shape": "*",
-        "@types/d3-time": "*",
-        "@types/d3-time-format": "*",
-        "@types/d3-timer": "*",
-        "@types/d3-transition": "*",
-        "@types/d3-zoom": "*"
-      }
-    },
-    "node_modules/@types/d3-array": {
-      "version": "3.2.2",
-      "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
-      "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
-      "license": "MIT"
-    },
-    "node_modules/@types/d3-axis": {
-      "version": "3.0.6",
-      "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
-      "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
-      "license": "MIT",
-      "dependencies": {
-        "@types/d3-selection": "*"
-      }
-    },
-    "node_modules/@types/d3-brush": {
-      "version": "3.0.6",
-      "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
-      "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
-      "license": "MIT",
-      "dependencies": {
-        "@types/d3-selection": "*"
-      }
-    },
-    "node_modules/@types/d3-chord": {
-      "version": "3.0.6",
-      "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
-      "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
-      "license": "MIT"
-    },
     "node_modules/@types/d3-color": {
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
       "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
       "license": "MIT"
     },
-    "node_modules/@types/d3-contour": {
-      "version": "3.0.6",
-      "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
-      "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
-      "license": "MIT",
-      "dependencies": {
-        "@types/d3-array": "*",
-        "@types/geojson": "*"
-      }
-    },
-    "node_modules/@types/d3-delaunay": {
-      "version": "6.0.4",
-      "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
-      "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
-      "license": "MIT"
-    },
-    "node_modules/@types/d3-dispatch": {
-      "version": "3.0.7",
-      "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
-      "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==",
-      "license": "MIT"
-    },
     "node_modules/@types/d3-drag": {
       "version": "3.0.7",
       "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
@@ -2085,54 +1725,6 @@
         "@types/d3-selection": "*"
       }
     },
-    "node_modules/@types/d3-dsv": {
-      "version": "3.0.7",
-      "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
-      "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
-      "license": "MIT"
-    },
-    "node_modules/@types/d3-ease": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
-      "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
-      "license": "MIT"
-    },
-    "node_modules/@types/d3-fetch": {
-      "version": "3.0.7",
-      "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
-      "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
-      "license": "MIT",
-      "dependencies": {
-        "@types/d3-dsv": "*"
-      }
-    },
-    "node_modules/@types/d3-force": {
-      "version": "3.0.10",
-      "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
-      "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
-      "license": "MIT"
-    },
-    "node_modules/@types/d3-format": {
-      "version": "3.0.4",
-      "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
-      "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
-      "license": "MIT"
-    },
-    "node_modules/@types/d3-geo": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.1.tgz",
-      "integrity": "sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w==",
-      "license": "MIT",
-      "dependencies": {
-        "@types/geojson": "*"
-      }
-    },
-    "node_modules/@types/d3-hierarchy": {
-      "version": "3.1.7",
-      "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
-      "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
-      "license": "MIT"
-    },
     "node_modules/@types/d3-interpolate": {
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
@@ -2142,78 +1734,12 @@
         "@types/d3-color": "*"
       }
     },
-    "node_modules/@types/d3-path": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
-      "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
-      "license": "MIT"
-    },
-    "node_modules/@types/d3-polygon": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
-      "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
-      "license": "MIT"
-    },
-    "node_modules/@types/d3-quadtree": {
-      "version": "3.0.6",
-      "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
-      "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
-      "license": "MIT"
-    },
-    "node_modules/@types/d3-random": {
-      "version": "3.0.4",
-      "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.4.tgz",
-      "integrity": "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==",
-      "license": "MIT"
-    },
-    "node_modules/@types/d3-scale": {
-      "version": "4.0.9",
-      "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
-      "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
-      "license": "MIT",
-      "dependencies": {
-        "@types/d3-time": "*"
-      }
-    },
-    "node_modules/@types/d3-scale-chromatic": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
-      "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
-      "license": "MIT"
-    },
     "node_modules/@types/d3-selection": {
       "version": "3.0.11",
       "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
       "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
       "license": "MIT"
     },
-    "node_modules/@types/d3-shape": {
-      "version": "3.1.8",
-      "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
-      "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
-      "license": "MIT",
-      "dependencies": {
-        "@types/d3-path": "*"
-      }
-    },
-    "node_modules/@types/d3-time": {
-      "version": "3.0.4",
-      "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
-      "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
-      "license": "MIT"
-    },
-    "node_modules/@types/d3-time-format": {
-      "version": "4.0.3",
-      "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
-      "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
-      "license": "MIT"
-    },
-    "node_modules/@types/d3-timer": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
-      "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
-      "license": "MIT"
-    },
     "node_modules/@types/d3-transition": {
       "version": "3.0.9",
       "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
@@ -2243,12 +1769,6 @@
       "dev": true,
       "license": "MIT"
     },
-    "node_modules/@types/geojson": {
-      "version": "7946.0.16",
-      "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
-      "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
-      "license": "MIT"
-    },
     "node_modules/@types/json-schema": {
       "version": "7.0.15",
       "dev": true,
@@ -2284,7 +1804,7 @@
     },
     "node_modules/@types/react-dom": {
       "version": "19.2.3",
-      "dev": true,
+      "devOptional": true,
       "license": "MIT",
       "peerDependencies": {
         "@types/react": "^19.2.0"
@@ -2663,6 +2183,76 @@
         "url": "https://opencollective.com/vitest"
       }
     },
+    "node_modules/@xyflow/react": {
+      "version": "12.11.3",
+      "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.11.3.tgz",
+      "integrity": "sha512-G3jogHz2GWUtIOkhavUGno2YzY9u6fILIJBttfsBendb0/HWB90JG+sOTAvlIMEwyvq9zgy9V9ZQSwyQjR5QzQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@xyflow/system": "0.0.80",
+        "classcat": "^5.0.3",
+        "zustand": "^4.4.0"
+      },
+      "peerDependencies": {
+        "@types/react": ">=17",
+        "@types/react-dom": ">=17",
+        "react": ">=17",
+        "react-dom": ">=17"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "@types/react-dom": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@xyflow/react/node_modules/zustand": {
+      "version": "4.5.7",
+      "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
+      "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
+      "license": "MIT",
+      "dependencies": {
+        "use-sync-external-store": "^1.2.2"
+      },
+      "engines": {
+        "node": ">=12.7.0"
+      },
+      "peerDependencies": {
+        "@types/react": ">=16.8",
+        "immer": ">=9.0.6",
+        "react": ">=16.8"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "immer": {
+          "optional": true
+        },
+        "react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@xyflow/system": {
+      "version": "0.0.80",
+      "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.80.tgz",
+      "integrity": "sha512-ywc3ZqG91brzWrH1WlwMdIX4goOfrpBy6AbLdVSaof/Xx9l138ijIKRExM6EkMro2F+OImGmSiA/WKcXvKVcfA==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-drag": "^3.0.7",
+        "@types/d3-interpolate": "^3.0.4",
+        "@types/d3-selection": "^3.0.10",
+        "@types/d3-transition": "^3.0.8",
+        "@types/d3-zoom": "^3.0.8",
+        "d3-drag": "^3.0.0",
+        "d3-interpolate": "^3.0.1",
+        "d3-selection": "^3.0.0",
+        "d3-zoom": "^3.0.0"
+      }
+    },
     "node_modules/acorn": {
       "version": "8.15.0",
       "dev": true,
@@ -4195,7 +3785,9 @@
       "license": "MIT"
     },
     "node_modules/nanoid": {
-      "version": "3.3.17",
+      "version": "3.3.18",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
+      "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
       "dev": true,
       "funding": [
         {
@@ -4559,24 +4151,6 @@
         "react-dom": ">=16.6.0"
       }
     },
-    "node_modules/reactflow": {
-      "version": "11.11.4",
-      "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz",
-      "integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==",
-      "license": "MIT",
-      "dependencies": {
-        "@reactflow/background": "11.3.14",
-        "@reactflow/controls": "11.2.14",
-        "@reactflow/core": "11.11.4",
-        "@reactflow/minimap": "11.7.14",
-        "@reactflow/node-resizer": "2.2.14",
-        "@reactflow/node-toolbar": "1.3.14"
-      },
-      "peerDependencies": {
-        "react": ">=17",
-        "react-dom": ">=17"
-      }
-    },
     "node_modules/resolve": {
       "version": "1.22.11",
       "license": "MIT",

+ 1 - 1
package.json

@@ -18,11 +18,11 @@
     "@emotion/react": "^11.14.0",
     "@emotion/styled": "^11.14.1",
     "@mui/material": "^7.3.5",
+    "@xyflow/react": "^12.11.3",
     "pako": "^2.1.0",
     "react": "^19.2.0",
     "react-dom": "^19.2.0",
     "react-router": "^8.3.0",
-    "reactflow": "^11.11.4",
     "sharp": "^0.35.3",
     "simplebar-react": "^3.3.2",
     "zustand": "^5.0.14"

+ 2 - 9
src/ClockBuilder.tsx

@@ -10,6 +10,7 @@ import { useClockStore } from "./store/useClockStore";
 import ClockWizard from "./assets/components/ClockWizard";
 import ClockTimeline from "./assets/ClockTimeline";
 import Select from "./assets/components/Select";
+import { downloadJSON } from "./utils";
 
 export default function ClockBuilder() {
   //  Subscribe to the Global Store
@@ -91,15 +92,7 @@ export default function ClockBuilder() {
       rows: rowsArray,
       blocks: blocksArray,
     };
-    const blob = new Blob([JSON.stringify(config, null, 2)], {
-      type: "application/json",
-    });
-    const url = URL.createObjectURL(blob);
-    const a = document.createElement("a");
-    a.href = url;
-    a.download = `clock-${new Date().toISOString().slice(0, 19).replace(/[:T]/g, "-")}.json`;
-    a.click();
-    URL.revokeObjectURL(url);
+    downloadJSON(config, `clock-${new Date().toISOString().slice(0, 19).replace(/[:T]/g, "-")}.json`);
   };
 
   const fileInputRef = useRef<HTMLInputElement>(null);

+ 2 - 2
src/assets/Selector/BeaconConfigurator.tsx

@@ -1,7 +1,7 @@
 import data from "../../assets/data/2.0/data.json"; // Adjust path if needed
 import ModuleSlots from "./ModuleSlots";
 import type { Beacon, Module } from "../../../scripts/factorio-dump/process-data.models";
-import { useQualityScroller } from "../../hooks/useQualityScroller";
+import { qualityShortcut, useQualityScroller } from "../../hooks/useQualityScroller";
 import Icon from "../icon";
 import NumberInput from "../components/NumberInput";
 import styles from "./BeaconConfigurator.module.css";
@@ -35,7 +35,7 @@ function BeaconIconWrapper({
   const { scrollRef, activeQuality } = useQualityScroller("normal", qualityLevel, (q) => onChangeQuality(q.level));
 
   return (
-    <div ref={scrollRef} className={styles.iconWrapper} title="Alt+Scroll to change beacon quality">
+    <div ref={scrollRef} className={styles.iconWrapper} title={qualityShortcut + " to change beacon quality"}>
       <label>Beacon</label>
       <Icon iconName={beacon.icon ?? ""} size={44} qualityLevel={activeQuality.level} />
     </div>

+ 2 - 3
src/assets/Selector/MachineSelector.tsx

@@ -7,7 +7,7 @@ import SelectMenu from "./SelectFactorioMenu";
 import Tooltip from "../Tooltip";
 import type { Recipe } from "../../../scripts/factorio-dump/helpers/recipes.helper";
 import type { Machine } from "../../../scripts/factorio-dump/process-data.models";
-import { useQualityScroller } from "../../hooks/useQualityScroller";
+import { qualityShortcut, useQualityScroller } from "../../hooks/useQualityScroller";
 import Select, { type SelectOption } from "../components/Select";
 
 const recipeList = data.recipeGroup.flatMap((r) => r.subGroup.flatMap((s) => (s.children ?? []) as Recipe[]));
@@ -42,7 +42,6 @@ function MachineSelector({ style, className, placeholder, value, onChange }: Mac
     "normal", // initial (fallback)
     currentQualityLevel, // controlled state!
     (newQuality) => {
-      // Direct, pure callback when user alt+scrolls
       onChange?.({ machine, qualityLevel: newQuality.level, recipe });
     },
   );
@@ -123,7 +122,7 @@ function MachineSelector({ style, className, placeholder, value, onChange }: Mac
         </button>
       </div>
       <div ref={scrollRef} className="field" style={{ minWidth: 250 }}>
-        <label>Machine (alt + scroll for changing quality)</label>
+        <label>Machine ({qualityShortcut} for changing quality)</label>
         <Select
           className={styles.bigSelect}
           value={machine ? machine.id : ""}

+ 2 - 1
src/assets/components/QualityIcon.tsx

@@ -3,6 +3,7 @@ import { useState, useEffect, useRef } from "react";
 import styles from "./QualityIcon.module.css";
 import data from "../../assets/data/2.0/data.json";
 import Icon from "../icon";
+import { qualityShortcut } from "../../hooks/useQualityScroller";
 
 const QUALITY_LEVELS = data.qualityLevels.map((q) => q.name); // ["normal", "uncommon", "rare", "epic", "legendary"]
 
@@ -51,7 +52,7 @@ export default function QualityIcon({
   const qualityData = data.qualityLevels.find((q) => q.name === quality);
 
   return (
-    <div ref={containerRef} className={styles.wrap} title="Alt + Scroll to change quality">
+    <div ref={containerRef} className={styles.wrap} title={qualityShortcut + " to change quality"}>
       <Icon iconName={iconName} size={size} qualityLevel={qualityData?.level} />
     </div>
   );

+ 136 - 134
src/engine/Dashboard/GraphView.tsx

@@ -1,151 +1,153 @@
-import { useMemo, useEffect, useState } from "react";
-import ReactFlow, { Background, Controls, MarkerType } from "reactflow";
-import type { NodeProps, Node, Edge } from "reactflow";
-import "reactflow/dist/style.css";
-import styles from "./Dashboard.module.css";
+// import { useMemo, useEffect, useState } from "react";
+// import ReactFlow, { Background, Controls, MarkerType } from "reactflow";
+// import type { NodeProps, Node, Edge } from "reactflow";
+// import "reactflow/dist/style.css";
+// import styles from "./Dashboard.module.css";
 import type { MachineData, UIOrchestrator } from "./types";
 
-// Type for React Flow Node Data
-interface CustomNodeData {
-  instance: MachineData;
-  label: string;
-  currentTick?: number;
-}
+// // Type for React Flow Node Data
+// interface CustomNodeData {
+//   instance: MachineData;
+//   label: string;
+//   currentTick?: number;
+// }
 
-const MachineNode = ({ data }: NodeProps<CustomNodeData>) => {
-  const machine = data.instance as MachineData;
-  return (
-    <div
-      className={styles.machineCard}
-      style={{ margin: 0, minWidth: "220px", boxShadow: "inset 0 0 10px rgba(0,0,0,0.5)" }}
-    >
-      <div style={{ display: "flex", justifyContent: "space-between", marginBottom: "8px" }}>
-        <span style={{ color: "#5eb663", fontWeight: "bold" }}>{machine.recipe.name}</span>
-        <span className={styles.statusBadge} style={{ color: machine.isCrafting ? "#5eb663" : "#fe5a5a" }}>
-          {machine.isCrafting ? "CRAFTING" : "HALTED"}
-        </span>
-      </div>
-      <div
-        style={{
-          width: "100%",
-          height: "6px",
-          backgroundColor: "#1a1a1a",
-          border: "1px solid #000",
-          marginBottom: "8px",
-        }}
-      >
-        <div style={{ width: `${machine.craftProgress * 100}%`, height: "100%", backgroundColor: "#5eb663" }} />
-      </div>
-      <div style={{ display: "flex", justifyContent: "space-between", fontSize: "11px" }}>
-        <div style={{ display: "flex", gap: "4px" }}>
-          <span style={{ color: "#8e8e8e" }}>IN:</span>
-          {Object.entries(machine.inputBuffer).map(([k, v]) => (
-            <span key={k} style={{ color: v === 0 ? "#fe5a5a" : "#fff" }}>
-              {v}
-            </span>
-          ))}
-        </div>
-        <div style={{ display: "flex", gap: "4px" }}>
-          <span style={{ color: "#8e8e8e" }}>OUT:</span>
-          {Object.entries(machine.outputBuffer).map(([k, v]) => {
-            const isFull = v >= (machine.outputBlockLimits[k] || 0);
-            return (
-              <span key={k} style={{ color: isFull ? "#fe5a5a" : "#fff" }}>
-                {v}
-              </span>
-            );
-          })}
-        </div>
-      </div>
-    </div>
-  );
-};
+// const MachineNode = ({ data }: NodeProps<CustomNodeData>) => {
+//   const machine = data.instance as MachineData;
+//   return (
+//     <div
+//       className={styles.machineCard}
+//       style={{ margin: 0, minWidth: "220px", boxShadow: "inset 0 0 10px rgba(0,0,0,0.5)" }}
+//     >
+//       <div style={{ display: "flex", justifyContent: "space-between", marginBottom: "8px" }}>
+//         <span style={{ color: "#5eb663", fontWeight: "bold" }}>{machine.recipe.name}</span>
+//         <span className={styles.statusBadge} style={{ color: machine.isCrafting ? "#5eb663" : "#fe5a5a" }}>
+//           {machine.isCrafting ? "CRAFTING" : "HALTED"}
+//         </span>
+//       </div>
+//       <div
+//         style={{
+//           width: "100%",
+//           height: "6px",
+//           backgroundColor: "#1a1a1a",
+//           border: "1px solid #000",
+//           marginBottom: "8px",
+//         }}
+//       >
+//         <div style={{ width: `${machine.craftProgress * 100}%`, height: "100%", backgroundColor: "#5eb663" }} />
+//       </div>
+//       <div style={{ display: "flex", justifyContent: "space-between", fontSize: "11px" }}>
+//         <div style={{ display: "flex", gap: "4px" }}>
+//           <span style={{ color: "#8e8e8e" }}>IN:</span>
+//           {Object.entries(machine.inputBuffer).map(([k, v]) => (
+//             <span key={k} style={{ color: v === 0 ? "#fe5a5a" : "#fff" }}>
+//               {v}
+//             </span>
+//           ))}
+//         </div>
+//         <div style={{ display: "flex", gap: "4px" }}>
+//           <span style={{ color: "#8e8e8e" }}>OUT:</span>
+//           {Object.entries(machine.outputBuffer).map(([k, v]) => {
+//             const isFull = v >= (machine.outputBlockLimits[k] || 0);
+//             return (
+//               <span key={k} style={{ color: isFull ? "#fe5a5a" : "#fff" }}>
+//                 {v}
+//               </span>
+//             );
+//           })}
+//         </div>
+//       </div>
+//     </div>
+//   );
+// };
 
-const ChestNode = ({ data }: NodeProps<CustomNodeData>) => (
-  <div
-    style={{
-      backgroundColor: "#3d3d3d",
-      padding: "12px",
-      borderRadius: "4px",
-      border: "2px solid #8e8e8e",
-      minWidth: "120px",
-      textAlign: "center",
-      color: "#e3e3e3",
-      fontWeight: "bold",
-    }}
-  >
-    {data.label}
-  </div>
-);
+// const ChestNode = ({ data }: NodeProps<CustomNodeData>) => (
+//   <div
+//     style={{
+//       backgroundColor: "#3d3d3d",
+//       padding: "12px",
+//       borderRadius: "4px",
+//       border: "2px solid #8e8e8e",
+//       minWidth: "120px",
+//       textAlign: "center",
+//       color: "#e3e3e3",
+//       fontWeight: "bold",
+//     }}
+//   >
+//     {data.label}
+//   </div>
+// );
 
-const nodeTypes = { machine: MachineNode, chest: ChestNode };
+// const nodeTypes = { machine: MachineNode, chest: ChestNode };
 
 interface GraphViewProps {
   orchestrator: UIOrchestrator;
   tick: number;
 }
-
 export default function GraphView({ orchestrator, tick }: GraphViewProps) {
-  const { initialNodes, initialEdges } = useMemo(() => {
-    const nodes: Node<CustomNodeData>[] = [];
-    const edges: Edge[] = [];
-    const objToId = new Map<any, string>();
-    let idCounter = 0;
+  return <div></div>;
+}
+// export default function GraphView({ orchestrator, tick }: GraphViewProps) {
+//   const { initialNodes, initialEdges } = useMemo(() => {
+//     const nodes: Node<CustomNodeData>[] = [];
+//     const edges: Edge[] = [];
+//     const objToId = new Map<any, string>();
+//     let idCounter = 0;
 
-    const getId = (obj: any, type: string, label: string): string => {
-      if (!objToId.has(obj)) {
-        const id = `node_${idCounter++}`;
-        objToId.set(obj, id);
-        nodes.push({
-          id,
-          type,
-          position: { x: (idCounter % 3) * 300, y: Math.floor(idCounter / 3) * 150 },
-          data: { instance: obj, label },
-        });
-      }
-      return objToId.get(obj)!;
-    };
+//     const getId = (obj: any, type: string, label: string): string => {
+//       if (!objToId.has(obj)) {
+//         const id = `node_${idCounter++}`;
+//         objToId.set(obj, id);
+//         nodes.push({
+//           id,
+//           type,
+//           position: { x: (idCounter % 3) * 300, y: Math.floor(idCounter / 3) * 150 },
+//           data: { instance: obj, label },
+//         });
+//       }
+//       return objToId.get(obj)!;
+//     };
 
-    orchestrator.getInserters().forEach((ins, i) => {
-      edges.push({
-        id: `edge_${i}`,
-        source: getId(ins.source, ins.source.recipe ? "machine" : "chest", "Source"),
-        target: getId(ins.destination, ins.destination.recipe ? "machine" : "chest", "Destination"),
-        label: ins.targetItemId,
-        animated: false,
-        style: { stroke: "#8e8e8e", strokeWidth: 2 },
-        markerEnd: { type: MarkerType.ArrowClosed, color: "#8e8e8e" },
-      });
-    });
+//     orchestrator.getInserters().forEach((ins, i) => {
+//       edges.push({
+//         id: `edge_${i}`,
+//         source: getId(ins.source, ins.source.recipe ? "machine" : "chest", "Source"),
+//         target: getId(ins.destination, ins.destination.recipe ? "machine" : "chest", "Destination"),
+//         label: ins.targetItemId,
+//         animated: false,
+//         style: { stroke: "#8e8e8e", strokeWidth: 2 },
+//         markerEnd: { type: MarkerType.ArrowClosed, color: "#8e8e8e" },
+//       });
+//     });
 
-    return { initialNodes: nodes, initialEdges: edges };
-  }, [orchestrator]);
+//     return { initialNodes: nodes, initialEdges: edges };
+//   }, [orchestrator]);
 
-  const [nodes, setNodes] = useState<Node<CustomNodeData>[]>(initialNodes);
-  const [edges, setEdges] = useState<Edge[]>(initialEdges);
+//   const [nodes, setNodes] = useState<Node<CustomNodeData>[]>(initialNodes);
+//   const [edges, setEdges] = useState<Edge[]>(initialEdges);
 
-  useEffect(() => {
-    setNodes((nds) => nds.map((n) => ({ ...n, data: { ...n.data, currentTick: tick } })));
-    setEdges((eds) =>
-      eds.map((edge, i) => {
-        const ins = orchestrator.getInserters()[i];
-        const isSwinging = ins.state > 0 && ins.state < 4;
-        return {
-          ...edge,
-          animated: isSwinging,
-          style: { stroke: isSwinging ? "#5eb663" : "#8e8e8e", strokeWidth: isSwinging ? 3 : 2 },
-          markerEnd: { type: MarkerType.ArrowClosed, color: isSwinging ? "#5eb663" : "#8e8e8e" },
-        };
-      }),
-    );
-  }, [tick, orchestrator]);
+//   useEffect(() => {
+//     setNodes((nds) => nds.map((n) => ({ ...n, data: { ...n.data, currentTick: tick } })));
+//     setEdges((eds) =>
+//       eds.map((edge, i) => {
+//         const ins = orchestrator.getInserters()[i];
+//         const isSwinging = ins.state > 0 && ins.state < 4;
+//         return {
+//           ...edge,
+//           animated: isSwinging,
+//           style: { stroke: isSwinging ? "#5eb663" : "#8e8e8e", strokeWidth: isSwinging ? 3 : 2 },
+//           markerEnd: { type: MarkerType.ArrowClosed, color: isSwinging ? "#5eb663" : "#8e8e8e" },
+//         };
+//       }),
+//     );
+//   }, [tick, orchestrator]);
 
-  return (
-    <div className={styles.graphWrapper}>
-      <ReactFlow nodes={nodes} edges={edges} nodeTypes={nodeTypes} fitView>
-        <Background color="#5f5f5f" gap={16} />
-        <Controls />
-      </ReactFlow>
-    </div>
-  );
-}
+//   return (
+//     <div className={styles.graphWrapper}>
+//       <ReactFlow nodes={nodes} edges={edges} nodeTypes={nodeTypes} fitView>
+//         <Background color="#5f5f5f" gap={16} />
+//         <Controls />
+//       </ReactFlow>
+//     </div>
+//   );
+// }

+ 61 - 0
src/engine/Topology/InserterEdge.tsx

@@ -0,0 +1,61 @@
+import { BaseEdge, EdgeLabelRenderer, getBezierPath, type EdgeProps } from "@xyflow/react";
+import Icon from "../../assets/icon";
+
+export default function InserterEdge({
+  id,
+  sourceX,
+  sourceY,
+  targetX,
+  targetY,
+  sourcePosition,
+  targetPosition,
+  style,
+  markerEnd,
+  data,
+}: EdgeProps) {
+  const [edgePath, labelX, labelY] = getBezierPath({
+    sourceX,
+    sourceY,
+    sourcePosition,
+    targetX,
+    targetY,
+    targetPosition,
+  });
+
+  // Default to stack inserter, but you can dynamically set this in EdgeInspector later!
+  const inserterIcon = (data?.inserterName as string) || "stack-inserter";
+
+  return (
+    <>
+      <BaseEdge id={id} path={edgePath} style={style} markerEnd={markerEnd} />
+
+      <EdgeLabelRenderer>
+        <div
+          style={{
+            position: "absolute",
+            transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
+            pointerEvents: "all", // Critical: allows this HTML element to receive clicks
+            display: "flex",
+            alignItems: "center",
+            justifyContent: "center",
+            background: "rgba(30, 30, 30, 0.8)",
+            padding: "4px",
+            borderRadius: "6px",
+            border: "1px solid #4a4a4a",
+            cursor: "pointer",
+          }}
+          className="nodrag nopan"
+          onClick={(e) => {
+            e.stopPropagation();
+            // Call the function we will inject into the data payload
+            if (data?.onEdgeSelect) {
+              (data.onEdgeSelect as (id: string) => void)(id);
+            }
+          }}
+        >
+          <Icon iconName={`item/${inserterIcon}.png`} size={32} qualityLevel={4} />
+        </div>
+      </EdgeLabelRenderer>
+    </>
+  );
+}

+ 189 - 88
src/engine/Topology/TopologyBuilder.tsx

@@ -1,119 +1,220 @@
-import { useState } from "react";
+import { useState, useRef, useCallback, useMemo } from "react";
+import {
+  ReactFlow,
+  Background,
+  Controls,
+  applyNodeChanges,
+  applyEdgeChanges,
+  addEdge as connectEdges,
+  type Node,
+  type Edge,
+  type Connection,
+  type NodeChange,
+  type EdgeChange,
+} from "@xyflow/react";
+import "@xyflow/react/dist/style.css";
+
 import styles from "./TopologyBuilder.module.css";
 import type { TopologyEdge, TopologyNode } from "./model";
 import { ContainerType } from "../simulator";
 import NodeInspector from "./NodeInspector";
 import EdgeInspector from "./EdgeInspector";
+import TopologyNodeUI from "./TopologyNodeUI";
+import InserterEdge from "./InserterEdge";
+import { downloadJSON } from "../../utils";
+
+const nodeTypes = { containerNode: TopologyNodeUI };
+const edgeTypes = { inserterEdge: InserterEdge };
+
+// FIX: Satisfy React Flow's strict generic constraints
+type FlowNodeData = TopologyNode & Record<string, unknown>;
+type FlowEdgeData = TopologyEdge & Record<string, unknown> & { onEdgeSelect?: (id: string) => void };
 
 export default function TopologyBuilder() {
-  const [nodes, setNodes] = useState<Record<string, TopologyNode>>({});
-  const [edges, setEdges] = useState<Record<string, TopologyEdge>>({});
+  // FIX: Properly typed as Arrays
+  const [nodes, setNodes] = useState<Node<FlowNodeData>[]>([]);
+  const [edges, setEdges] = useState<Edge<FlowEdgeData>[]>([]);
 
   const [selectedId, setSelectedId] = useState<string | null>(null);
   const [selectionType, setSelectionType] = useState<"node" | "edge" | null>(null);
+  const fileInputRef = useRef<HTMLInputElement>(null);
+
+  // Reusable callback for Edge clicks (passed into Edge payload)
+  const handleEdgeSelect = useCallback((id: string) => {
+    setSelectedId(id);
+    setSelectionType("edge");
+  }, []);
+
+  // --- React Flow Interaction Handlers ---
+  const onNodesChange = useCallback(
+    (changes: NodeChange<Node<FlowNodeData>>[]) =>
+      setNodes((nds) => applyNodeChanges(changes, nds) as Node<FlowNodeData>[]),
+    [],
+  );
+
+  const onEdgesChange = useCallback(
+    (changes: EdgeChange<Edge<FlowEdgeData>>[]) =>
+      setEdges((eds) => applyEdgeChanges(changes, eds) as Edge<FlowEdgeData>[]),
+    [],
+  );
+
+  const onConnect = useCallback(
+    (connection: Connection) => {
+      const id = `edge_${crypto.randomUUID().slice(0, 8)}`;
+      const newDomainEdge: TopologyEdge = {
+        id,
+        sourceId: connection.source,
+        destinationId: connection.target,
+        itemId: "",
+        stackSize: 16,
+      };
+
+      const newFlowEdge: Edge<FlowEdgeData> = {
+        id,
+        source: connection.source,
+        target: connection.target,
+        type: "inserterEdge",
+        data: { ...newDomainEdge, onEdgeSelect: handleEdgeSelect },
+        animated: true,
+      };
+
+      setEdges((eds) => connectEdges(newFlowEdge, eds));
+      setSelectedId(id);
+      setSelectionType("edge");
+    },
+    [handleEdgeSelect],
+  );
 
   const addNode = (type: ContainerType) => {
     const id = `node_${crypto.randomUUID().slice(0, 8)}`;
-    setNodes((prev) => ({
-      ...prev,
-      [id]: {
-        id,
-        name: `New ${type}`,
-        type,
-        ...(type === ContainerType.Machine
-          ? {
-              machineConfig: {
-                setup: { machine: null as any, machineModules: [], beacons: [], machineQualityLevel: 0 },
-                recipe: null,
-                multiplier: 1,
-              },
-            }
-          : {}),
-      },
-    }));
+    const domainNode: TopologyNode = {
+      id,
+      name: `New ${type}`,
+      type,
+      ...(type === ContainerType.Machine
+        ? {
+            machineConfig: {
+              setup: { machine: null as any, machineModules: [], beacons: [], machineQualityLevel: 0 },
+              recipe: null,
+              multiplier: 1,
+            },
+          }
+        : {}),
+    };
+
+    const flowNode: Node<FlowNodeData> = {
+      id,
+      type: "containerNode",
+      position: { x: Math.random() * 200 + 100, y: Math.random() * 200 + 100 },
+      data: domainNode,
+    };
+
+    setNodes((nds) => [...nds, flowNode]);
     setSelectedId(id);
     setSelectionType("node");
   };
 
-  const addEdge = () => {
-    const id = `edge_${crypto.randomUUID().slice(0, 8)}`;
-    setEdges((prev) => ({
-      ...prev,
-      [id]: { id, sourceId: "", destinationId: "", itemId: "", stackSize: 1 },
-    }));
-    setSelectedId(id);
-    setSelectionType("edge");
+  const domainNodesMap = useMemo(() => {
+    return nodes.reduce((acc, n) => ({ ...acc, [n.id]: n.data }), {} as Record<string, TopologyNode>);
+  }, [nodes]);
+
+  const updateDomainNode = (updated: TopologyNode) => {
+    setNodes((nds) => nds.map((n) => (n.id === updated.id ? { ...n, data: updated } : n)));
+  };
+
+  const updateDomainEdge = (updated: TopologyEdge) => {
+    setEdges((eds) =>
+      eds.map((e) =>
+        e.id === updated.id
+          ? {
+              ...e,
+              source: updated.sourceId,
+              target: updated.destinationId,
+              // Ensure we preserve the callback when replacing the data object!
+              data: { ...updated, onEdgeSelect: handleEdgeSelect },
+            }
+          : e,
+      ),
+    );
+  };
+
+  const handleExport = () => downloadJSON({ nodes, edges }, `topology-${Date.now()}.json`);
+  const handleImport = (event: React.ChangeEvent<HTMLInputElement>) => {
+    const file = event.target.files?.[0];
+    if (!file) return;
+    const reader = new FileReader();
+    reader.onload = (e) => {
+      try {
+        const payload = JSON.parse(e.target?.result as string);
+        // Re-inject the edge selection callback which is lost during JSON stringification
+        const restoredEdges = (payload.edges || []).map((edge: any) => ({
+          ...edge,
+          data: { ...edge.data, onEdgeSelect: handleEdgeSelect },
+        }));
+        setNodes(payload.nodes || []);
+        setEdges(restoredEdges);
+      } catch (err) {
+        alert("Invalid file.");
+      }
+    };
+    reader.readAsText(file);
+    if (fileInputRef.current) fileInputRef.current.value = "";
   };
 
   return (
-    <div className={styles.container}>
-      {/* LEFT PANE: Directory / Graph List */}
-      <div className={styles.sidebar}>
-        <div className={styles.sidebarHeader}>
-          <h3>Topology</h3>
-          <div className={styles.actions}>
-            <button onClick={() => addNode(ContainerType.Machine)}>+ Machine</button>
-            <button onClick={() => addNode(ContainerType.Chest)}>+ Chest</button>
-            <button onClick={() => addNode(ContainerType.Belt)}>+ Belt</button>
-            <button onClick={addEdge} style={{ marginLeft: "8px" }}>
-              + Inserter
-            </button>
-          </div>
-        </div>
+    <div className={styles.container} style={{ display: "flex", height: "100vh", width: "100%" }}>
+      <input type="file" accept=".json" ref={fileInputRef} style={{ display: "none" }} onChange={handleImport} />
 
-        <div className={styles.listSection}>
-          <h4>Nodes</h4>
-          {Object.values(nodes).map((node) => (
-            <div
-              key={node.id}
-              className={`${styles.listItem} ${selectedId === node.id ? styles.active : ""}`}
-              onClick={() => {
-                setSelectedId(node.id);
-                setSelectionType("node");
-              }}
-            >
-              {node.name} <span className={styles.badge}>{node.type}</span>
-            </div>
-          ))}
-
-          <h4 style={{ marginTop: "16px" }}>Inserters (Edges)</h4>
-          {Object.values(edges).map((edge) => {
-            const srcName = nodes[edge.sourceId]?.name || "?";
-            const dstName = nodes[edge.destinationId]?.name || "?";
-            return (
-              <div
-                key={edge.id}
-                className={`${styles.listItem} ${selectedId === edge.id ? styles.active : ""}`}
-                onClick={() => {
-                  setSelectedId(edge.id);
-                  setSelectionType("edge");
-                }}
-              >
-                {srcName} → {dstName}
-              </div>
-            );
-          })}
+      {/* LEFT PANE: React Flow Canvas */}
+      <div style={{ flexGrow: 1, position: "relative" }}>
+        <div style={{ position: "absolute", top: 16, left: 16, zIndex: 10, display: "flex", gap: "8px" }}>
+          <button onClick={() => addNode(ContainerType.Machine)}>+ Machine</button>
+          <button onClick={() => addNode(ContainerType.Chest)}>+ Chest</button>
+          <button onClick={() => addNode(ContainerType.Belt)}>+ Belt</button>
+          <button onClick={() => fileInputRef.current?.click()}>📂 Import</button>
+          <button onClick={handleExport}>💾 Export</button>
         </div>
+
+        <ReactFlow
+          nodes={nodes}
+          edges={edges}
+          onNodesChange={onNodesChange}
+          onEdgesChange={onEdgesChange}
+          onConnect={onConnect}
+          nodeTypes={nodeTypes}
+          edgeTypes={edgeTypes}
+          onNodeClick={(_, node) => {
+            setSelectedId(node.id);
+            setSelectionType("node");
+          }}
+          // Standard SVG line clicks:
+          onEdgeClick={(_, edge) => handleEdgeSelect(edge.id)}
+          onPaneClick={() => {
+            setSelectedId(null);
+            setSelectionType(null);
+          }}
+          fitView
+        >
+          <Background color="#3a3a3a" gap={20} />
+          <Controls />
+        </ReactFlow>
       </div>
 
-      {/* RIGHT PANE: Inspector */}
-      <div className={styles.inspector}>
-        {selectionType === "node" && selectedId && nodes[selectedId] && (
-          <NodeInspector
-            node={nodes[selectedId]}
-            onChange={(updated) => setNodes((prev) => ({ ...prev, [updated.id]: updated }))}
-          />
+      {/* RIGHT PANE: Inspectors */}
+      <div
+        className={styles.inspector}
+        style={{ width: 400, minWidth: 400, borderLeft: "1px solid #3a3a3a", background: "#1a1a1a" }}
+      >
+        {selectionType === "node" && selectedId && domainNodesMap[selectedId] && (
+          <NodeInspector node={domainNodesMap[selectedId]} onChange={updateDomainNode} />
         )}
-
-        {selectionType === "edge" && selectedId && edges[selectedId] && (
+        {selectionType === "edge" && selectedId && edges.find((e) => e.id === selectedId) && (
           <EdgeInspector
-            edge={edges[selectedId]}
-            nodes={nodes}
-            onChange={(updated) => setEdges((prev) => ({ ...prev, [updated.id]: updated }))}
+            edge={edges.find((e) => e.id === selectedId)!.data}
+            nodes={domainNodesMap}
+            onChange={updateDomainEdge}
           />
         )}
-
-        {!selectedId && <div className={styles.emptyState}>Select a node or inserter to configure</div>}
       </div>
     </div>
   );

+ 180 - 0
src/engine/Topology/TopologyBuilderV0.tsx

@@ -0,0 +1,180 @@
+import { useState, useRef } from "react";
+import styles from "./TopologyBuilder.module.css";
+import type { TopologyEdge, TopologyNode } from "./model";
+import { ContainerType } from "../simulator";
+import NodeInspector from "./NodeInspector";
+import EdgeInspector from "./EdgeInspector";
+import { downloadJSON } from "../../utils";
+
+export default function TopologyBuilder() {
+  const [nodes, setNodes] = useState<Record<string, TopologyNode>>({});
+  const [edges, setEdges] = useState<Record<string, TopologyEdge>>({});
+
+  const [selectedId, setSelectedId] = useState<string | null>(null);
+  const [selectionType, setSelectionType] = useState<"node" | "edge" | null>(null);
+
+  const fileInputRef = useRef<HTMLInputElement>(null);
+
+  const addNode = (type: ContainerType) => {
+    const id = `node_${crypto.randomUUID().slice(0, 8)}`;
+    setNodes((prev) => ({
+      ...prev,
+      [id]: {
+        id,
+        name: `New ${type}`,
+        type,
+        ...(type === ContainerType.Machine
+          ? {
+              machineConfig: {
+                setup: { machine: null as any, machineModules: [], beacons: [], machineQualityLevel: 0 },
+                recipe: null,
+                multiplier: 1,
+              },
+            }
+          : {}),
+      },
+    }));
+    setSelectedId(id);
+    setSelectionType("node");
+  };
+
+  const addEdge = () => {
+    const id = `edge_${crypto.randomUUID().slice(0, 8)}`;
+    setEdges((prev) => ({
+      ...prev,
+      // Default to Legendary Stack Inserter capacity (16)
+      [id]: { id, sourceId: "", destinationId: "", itemId: "", stackSize: 16 },
+    }));
+    setSelectedId(id);
+    setSelectionType("edge");
+  };
+
+  // --- IMPORT / EXPORT LOGIC ---
+  const handleExport = () => {
+    const payload = { nodes, edges };
+    downloadJSON(payload, `topology-${new Date().toISOString().slice(0, 10)}.json`);
+  };
+
+  const handleImport = (event: React.ChangeEvent<HTMLInputElement>) => {
+    const file = event.target.files?.[0];
+    if (!file) return;
+
+    const reader = new FileReader();
+    reader.onload = (e) => {
+      try {
+        const payload = JSON.parse(e.target?.result as string);
+        if (payload.nodes && payload.edges) {
+          setNodes(payload.nodes);
+          setEdges(payload.edges);
+          setSelectedId(null);
+          setSelectionType(null);
+        } else {
+          alert("Invalid topology file format.");
+        }
+      } catch (err) {
+        console.error("Failed to parse JSON", err);
+        alert("Failed to read file. Make sure it is a valid JSON.");
+      }
+    };
+    reader.readAsText(file);
+
+    // Reset input so the same file can be imported again if needed
+    if (fileInputRef.current) {
+      fileInputRef.current.value = "";
+    }
+  };
+
+  return (
+    <div className={styles.container}>
+      {/* Hidden file input for importing JSON */}
+      <input type="file" accept=".json" ref={fileInputRef} style={{ display: "none" }} onChange={handleImport} />
+
+      {/* LEFT PANE: Directory / Graph List */}
+      <div className={styles.sidebar}>
+        <div className={styles.sidebarHeader}>
+          <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "12px" }}>
+            <h3 style={{ margin: 0 }}>Topology</h3>
+            <div>
+              <button className="button icon" onClick={() => fileInputRef.current?.click()} title="Import JSON">
+                📂
+              </button>
+              <button className="button icon" onClick={handleExport} title="Export JSON">
+                💾
+              </button>
+            </div>
+          </div>
+          <div className={styles.actions}>
+            <button onClick={() => addNode(ContainerType.Machine)}>+ Machine</button>
+            <button onClick={() => addNode(ContainerType.Chest)}>+ Chest</button>
+            <button onClick={() => addNode(ContainerType.Belt)}>+ Belt</button>
+            <button onClick={addEdge} style={{ marginLeft: "8px" }}>
+              + Inserter
+            </button>
+          </div>
+        </div>
+
+        <div className={styles.listSection}>
+          <h4>Nodes</h4>
+          {Object.values(nodes).map((node) => (
+            <div
+              key={node.id}
+              className={`${styles.listItem} ${selectedId === node.id ? styles.active : ""}`}
+              onClick={() => {
+                setSelectedId(node.id);
+                setSelectionType("node");
+              }}
+            >
+              {node.name} <span className={styles.badge}>{node.type}</span>
+            </div>
+          ))}
+
+          <h4 style={{ marginTop: "16px" }}>Inserters (Edges)</h4>
+          {Object.values(edges).map((edge) => {
+            const srcName = nodes[edge.sourceId]?.name || "?";
+            const dstName = nodes[edge.destinationId]?.name || "?";
+            return (
+              <div
+                key={edge.id}
+                className={`${styles.listItem} ${selectedId === edge.id ? styles.active : ""}`}
+                onClick={() => {
+                  setSelectedId(edge.id);
+                  setSelectionType("edge");
+                }}
+              >
+                {srcName} → {dstName}
+              </div>
+            );
+          })}
+        </div>
+      </div>
+
+      {/* RIGHT PANE: Inspector */}
+      <div className={styles.inspector}>
+        {selectionType === "node" && selectedId && nodes[selectedId] && (
+          <NodeInspector
+            node={nodes[selectedId]}
+            onChange={(updated) => setNodes((prev) => ({ ...prev, [updated.id]: updated }))}
+          />
+        )}
+
+        {selectionType === "edge" && selectedId && edges[selectedId] && (
+          <EdgeInspector
+            edge={edges[selectedId]}
+            nodes={nodes}
+            onChange={(updated) => setEdges((prev) => ({ ...prev, [updated.id]: updated }))}
+          />
+        )}
+
+        {!selectedId && (
+          <div className={styles.emptyState}>
+            {Object.keys(nodes).length === 0 ? (
+              <span>Start by adding a Machine, Chest, or Belt on the left.</span>
+            ) : (
+              <span>Select a node or inserter to configure</span>
+            )}
+          </div>
+        )}
+      </div>
+    </div>
+  );
+}

+ 36 - 0
src/engine/Topology/TopologyNodeUI.tsx

@@ -0,0 +1,36 @@
+import { Handle, Position } from "@xyflow/react";
+import { ContainerType } from "../simulator";
+import type { TopologyNode } from "./model";
+import Icon from "../../assets/icon";
+
+export default function TopologyNodeUI({ data, selected }: { data: TopologyNode; selected: boolean }) {
+  // Determine icon based on node type
+  let iconName = "item/wooden-chest.png";
+  if (data.type === ContainerType.Belt) iconName = "item/transport-belt.png";
+  if (data.type === ContainerType.Machine && data.machineConfig?.setup.machine?.icon) {
+    iconName = data.machineConfig.setup.machine.icon;
+  }
+
+  return (
+    <div
+      style={{
+        background: "#242324",
+        border: `2px solid ${selected ? "#e39827" : "#3a3a3a"}`,
+        padding: "10px",
+        borderRadius: "8px",
+        minWidth: "120px",
+        textAlign: "center",
+        color: "#e3e3e3",
+      }}
+    >
+      {/* Input Handle (Left) */}
+      <Handle type="target" position={Position.Left} style={{ width: 12, height: 12, background: "#4CAF50" }} />
+
+      <Icon iconName={iconName} size={48} qualityLevel={data.machineConfig?.setup.machineQualityLevel || 0} />
+      <div style={{ fontSize: "12px", marginTop: "8px" }}>{data.name}</div>
+
+      {/* Output Handle (Right) */}
+      <Handle type="source" position={Position.Right} style={{ width: 12, height: 12, background: "#F44336" }} />
+    </div>
+  );
+}

+ 21 - 16
src/hooks/useQualityScroller.ts

@@ -2,23 +2,26 @@ import { useEffect, useRef, useState, useMemo, useCallback } from "react";
 import data from "../assets/data/2.0/data.json";
 
 export type Quality = {
-  type: "quality";
+  type: string;
   name: string;
   level: number;
-  color: {
-    r: number;
-    g: number;
-    b: number;
-  };
+  color:
+    | {
+        r: number;
+        g: number;
+        b: number;
+      }
+    | number[];
   order: string;
-  next_probability: number;
-  subgroup: "qualities";
-  hidden: boolean;
+  next_probability?: number;
+  subgroup: string;
+  hidden?: boolean;
   icon: string;
-  draw_sprite_by_default: boolean;
-  next: string;
+  draw_sprite_by_default?: boolean;
+  next?: string;
 };
 
+export const qualityShortcut = "Shift + scroll";
 //  Precompute the exact sequence by traversing the 'next' linked list
 const qualityMap = new Map<string, Quality>(data.qualityLevels.map((q: Quality) => [q.name, q]));
 const baseQuality = data.qualityLevels.find((q: Quality) => q.level === 0) || data.qualityLevels[0];
@@ -84,7 +87,7 @@ export function useQualityScroller(
     if (!el) return;
 
     const handleWheel = (e: WheelEvent) => {
-      if (!e.altKey) return;
+      if (!e.shiftKey) return;
       e.preventDefault();
 
       const currentName = effectiveNameRef.current;
@@ -98,19 +101,21 @@ export function useQualityScroller(
       if (nextIndex !== currentIndex) {
         const newQuality = orderedQualities[nextIndex];
 
-        // Only mutate internal state if we own it
         if (!isControlled) {
           setInternalQualityName(newQuality.name);
         }
 
-        // Always report changes upwards
         onChangeRef.current?.(newQuality);
       }
     };
 
+    // Bind wheel
     el.addEventListener("wheel", handleWheel, { passive: false });
-    return () => el.removeEventListener("wheel", handleWheel);
-  }, [isControlled]); // Rebind only if control mode changes
+
+    return () => {
+      el.removeEventListener("wheel", handleWheel);
+    };
+  }, [isControlled]);
 
   const activeQuality = useMemo(
     () => orderedQualities.find((q) => q.name === effectiveName) || baseQuality,

+ 2 - 2
src/index.css

@@ -84,8 +84,8 @@ input:focus {
 .button.icon {
   height: fit-content;
   min-width: auto;
-  padding: 0;
-  margin: 0;
+  padding: 6px;
+  margin: 2px;
 }
 .flex-column .button {
   margin-right: 0;

+ 1 - 1
src/main.tsx

@@ -8,7 +8,7 @@ import Layout from "./Layout.tsx";
 import ClockBuilder from "./ClockBuilder.tsx";
 import ClockWizard from "./assets/components/ClockWizard.tsx";
 import { Simulator } from "./assets/Simulator.tsx";
-import TopologyBuilder from "./engine/Topology/TopologyBuilder.tsx";
+import TopologyBuilder from "./engine/Topology/TopologyBuilderV0.tsx";
 
 const root = document.getElementById("root");
 

+ 11 - 0
src/utils.ts

@@ -0,0 +1,11 @@
+export function downloadJSON(payload: any, filename: string) {
+  const blob = new Blob([JSON.stringify(payload, null, 2)], {
+    type: "application/json",
+  });
+  const url = URL.createObjectURL(blob);
+  const a = document.createElement("a");
+  a.href = url;
+  a.download = filename;
+  a.click();
+  URL.revokeObjectURL(url);
+}