소스 검색

implement simulation UI

JAQUIN_C 1 개월 전
부모
커밋
2847ee2cda

+ 668 - 78
package-lock.json

@@ -15,6 +15,7 @@
         "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"
@@ -708,9 +709,6 @@
       "cpu": [
         "arm"
       ],
-      "libc": [
-        "glibc"
-      ],
       "license": "LGPL-3.0-or-later",
       "optional": true,
       "os": [
@@ -727,9 +725,6 @@
       "cpu": [
         "arm64"
       ],
-      "libc": [
-        "glibc"
-      ],
       "license": "LGPL-3.0-or-later",
       "optional": true,
       "os": [
@@ -746,9 +741,6 @@
       "cpu": [
         "ppc64"
       ],
-      "libc": [
-        "glibc"
-      ],
       "license": "LGPL-3.0-or-later",
       "optional": true,
       "os": [
@@ -765,9 +757,6 @@
       "cpu": [
         "riscv64"
       ],
-      "libc": [
-        "glibc"
-      ],
       "license": "LGPL-3.0-or-later",
       "optional": true,
       "os": [
@@ -784,9 +773,6 @@
       "cpu": [
         "s390x"
       ],
-      "libc": [
-        "glibc"
-      ],
       "license": "LGPL-3.0-or-later",
       "optional": true,
       "os": [
@@ -803,9 +789,6 @@
       "cpu": [
         "x64"
       ],
-      "libc": [
-        "glibc"
-      ],
       "license": "LGPL-3.0-or-later",
       "optional": true,
       "os": [
@@ -822,9 +805,6 @@
       "cpu": [
         "arm64"
       ],
-      "libc": [
-        "musl"
-      ],
       "license": "LGPL-3.0-or-later",
       "optional": true,
       "os": [
@@ -841,9 +821,6 @@
       "cpu": [
         "x64"
       ],
-      "libc": [
-        "musl"
-      ],
       "license": "LGPL-3.0-or-later",
       "optional": true,
       "os": [
@@ -860,9 +837,6 @@
       "cpu": [
         "arm"
       ],
-      "libc": [
-        "glibc"
-      ],
       "license": "Apache-2.0",
       "optional": true,
       "os": [
@@ -885,9 +859,6 @@
       "cpu": [
         "arm64"
       ],
-      "libc": [
-        "glibc"
-      ],
       "license": "Apache-2.0",
       "optional": true,
       "os": [
@@ -910,9 +881,6 @@
       "cpu": [
         "ppc64"
       ],
-      "libc": [
-        "glibc"
-      ],
       "license": "Apache-2.0",
       "optional": true,
       "os": [
@@ -935,9 +903,6 @@
       "cpu": [
         "riscv64"
       ],
-      "libc": [
-        "glibc"
-      ],
       "license": "Apache-2.0",
       "optional": true,
       "os": [
@@ -960,9 +925,6 @@
       "cpu": [
         "s390x"
       ],
-      "libc": [
-        "glibc"
-      ],
       "license": "Apache-2.0",
       "optional": true,
       "os": [
@@ -985,9 +947,6 @@
       "cpu": [
         "x64"
       ],
-      "libc": [
-        "glibc"
-      ],
       "license": "Apache-2.0",
       "optional": true,
       "os": [
@@ -1010,9 +969,6 @@
       "cpu": [
         "arm64"
       ],
-      "libc": [
-        "musl"
-      ],
       "license": "Apache-2.0",
       "optional": true,
       "os": [
@@ -1035,9 +991,6 @@
       "cpu": [
         "x64"
       ],
-      "libc": [
-        "musl"
-      ],
       "license": "Apache-2.0",
       "optional": true,
       "os": [
@@ -1422,6 +1375,276 @@
         "url": "https://opencollective.com/popperjs"
       }
     },
+    "node_modules/@reactflow/background": {
+      "version": "11.3.14",
+      "resolved": "https://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/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",
@@ -1515,9 +1738,6 @@
         "arm64"
       ],
       "dev": true,
-      "libc": [
-        "glibc"
-      ],
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1535,9 +1755,6 @@
         "arm64"
       ],
       "dev": true,
-      "libc": [
-        "musl"
-      ],
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1555,9 +1772,6 @@
         "ppc64"
       ],
       "dev": true,
-      "libc": [
-        "glibc"
-      ],
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1575,9 +1789,6 @@
         "s390x"
       ],
       "dev": true,
-      "libc": [
-        "glibc"
-      ],
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1593,9 +1804,6 @@
         "x64"
       ],
       "dev": true,
-      "libc": [
-        "glibc"
-      ],
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1613,9 +1821,6 @@
         "x64"
       ],
       "dev": true,
-      "libc": [
-        "musl"
-      ],
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1719,6 +1924,259 @@
         "assertion-error": "^2.0.1"
       }
     },
+    "node_modules/@types/d3": {
+      "version": "7.4.3",
+      "resolved": "https://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@types/d3-drag/-/d3-drag-3.0.7.tgz",
+      "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-selection": "*"
+      }
+    },
+    "node_modules/@types/d3-dsv": {
+      "version": "3.0.7",
+      "resolved": "https://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+      "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-color": "*"
+      }
+    },
+    "node_modules/@types/d3-path": {
+      "version": "3.1.1",
+      "resolved": "https://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@types/d3-transition/-/d3-transition-3.0.9.tgz",
+      "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-selection": "*"
+      }
+    },
+    "node_modules/@types/d3-zoom": {
+      "version": "3.0.8",
+      "resolved": "https://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
+      "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-interpolate": "*",
+        "@types/d3-selection": "*"
+      }
+    },
     "node_modules/@types/deep-eql": {
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
@@ -1731,6 +2189,12 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@types/geojson": {
+      "version": "7946.0.16",
+      "resolved": "https://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/@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,
@@ -2389,6 +2853,12 @@
         "url": "https://github.com/chalk/chalk?sponsor=1"
       }
     },
+    "node_modules/classcat": {
+      "version": "5.0.5",
+      "resolved": "https://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/classcat/-/classcat-5.0.5.tgz",
+      "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
+      "license": "MIT"
+    },
     "node_modules/cli-cursor": {
       "version": "5.0.0",
       "dev": true,
@@ -2502,6 +2972,111 @@
       "version": "3.2.3",
       "license": "MIT"
     },
+    "node_modules/d3-color": {
+      "version": "3.1.0",
+      "resolved": "https://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/d3-color/-/d3-color-3.1.0.tgz",
+      "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-dispatch": {
+      "version": "3.0.1",
+      "resolved": "https://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+      "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-drag": {
+      "version": "3.0.0",
+      "resolved": "https://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/d3-drag/-/d3-drag-3.0.0.tgz",
+      "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-dispatch": "1 - 3",
+        "d3-selection": "3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-ease": {
+      "version": "3.0.1",
+      "resolved": "https://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/d3-ease/-/d3-ease-3.0.1.tgz",
+      "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-interpolate": {
+      "version": "3.0.1",
+      "resolved": "https://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+      "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-color": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-selection": {
+      "version": "3.0.0",
+      "resolved": "https://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/d3-selection/-/d3-selection-3.0.0.tgz",
+      "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-timer": {
+      "version": "3.0.1",
+      "resolved": "https://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/d3-timer/-/d3-timer-3.0.1.tgz",
+      "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-transition": {
+      "version": "3.0.1",
+      "resolved": "https://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/d3-transition/-/d3-transition-3.0.1.tgz",
+      "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-color": "1 - 3",
+        "d3-dispatch": "1 - 3",
+        "d3-ease": "1 - 3",
+        "d3-interpolate": "1 - 3",
+        "d3-timer": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "peerDependencies": {
+        "d3-selection": "2 - 3"
+      }
+    },
+    "node_modules/d3-zoom": {
+      "version": "3.0.0",
+      "resolved": "https://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/d3-zoom/-/d3-zoom-3.0.0.tgz",
+      "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-dispatch": "1 - 3",
+        "d3-drag": "2 - 3",
+        "d3-interpolate": "1 - 3",
+        "d3-selection": "2 - 3",
+        "d3-transition": "2 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/debug": {
       "version": "4.4.3",
       "license": "MIT",
@@ -3330,9 +3905,6 @@
         "arm64"
       ],
       "dev": true,
-      "libc": [
-        "glibc"
-      ],
       "license": "MPL-2.0",
       "optional": true,
       "os": [
@@ -3354,9 +3926,6 @@
         "arm64"
       ],
       "dev": true,
-      "libc": [
-        "musl"
-      ],
       "license": "MPL-2.0",
       "optional": true,
       "os": [
@@ -3376,9 +3945,6 @@
         "x64"
       ],
       "dev": true,
-      "libc": [
-        "glibc"
-      ],
       "license": "MPL-2.0",
       "optional": true,
       "os": [
@@ -3400,9 +3966,6 @@
         "x64"
       ],
       "dev": true,
-      "libc": [
-        "musl"
-      ],
       "license": "MPL-2.0",
       "optional": true,
       "os": [
@@ -3960,6 +4523,24 @@
         "react-dom": ">=16.6.0"
       }
     },
+    "node_modules/reactflow": {
+      "version": "11.11.4",
+      "resolved": "https://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/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",
@@ -4556,6 +5137,15 @@
         "punycode": "^2.1.0"
       }
     },
+    "node_modules/use-sync-external-store": {
+      "version": "1.6.0",
+      "resolved": "https://artifactory.2b82.aws.cloud.airbus.corp:443/artifactory/api/npm/r-af5d-mydef-npm-virtual/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+      "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+      "license": "MIT",
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+      }
+    },
     "node_modules/v8-compile-cache-lib": {
       "version": "3.0.1",
       "dev": true,

+ 3 - 2
package.json

@@ -22,6 +22,7 @@
     "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"
@@ -47,7 +48,7 @@
     "tsconfig-paths": "^4.2.0",
     "typescript": "~5.9.3",
     "typescript-eslint": "^8.46.3",
-    "vitest": "^4.1.10",
-    "vite": "^8.2.0"
+    "vite": "^8.2.0",
+    "vitest": "^4.1.10"
   }
 }

+ 1 - 1
src/Layout.tsx

@@ -4,7 +4,7 @@ import { Outlet, useLocation, useNavigate } from "react-router";
 const NAV_ITEMS = [
   { label: "Clock Builder", path: "/" },
   { label: "Component Tests", path: "/tests" },
-  { label: "Wizard Tests", path: "/Wizar" },
+  { label: "Simulator", path: "/simulator" },
   { label: "InputConfigurator", path: "/InputConfigurator" },
 ];
 

+ 68 - 0
src/assets/Simulator.tsx

@@ -0,0 +1,68 @@
+import type { Recipe } from "../../scripts/factorio-dump/helpers/recipes.helper";
+import type { Machine } from "../../scripts/factorio-dump/process-data.models";
+import type { MachineSetup } from "../engine";
+import FactorioSimulationDashboard from "../engine/FactorioSimulationDashBoard";
+import { Chest, FactorioEngineOrchestrator, InserterSimulator, MachineSimulator } from "../engine/simulator";
+
+const orchestrator = new FactorioEngineOrchestrator();
+
+    const copperCableRecipe: Recipe = {
+      name: "copper-cable",
+      energy_required: 0.5,
+      ingredients: [{ type: "item", name: "copper-plate", amount: 1 }],
+      results: [{ type: "item", name: "copper-cable", amount: 2 }],
+    } as Recipe;
+
+    const greenCircuitRecipe: Recipe = {
+      name: "electronic-circuit",
+      energy_required: 0.5,
+      ingredients: [
+        { type: "item", name: "iron-plate", amount: 1 },
+        { type: "item", name: "copper-cable", amount: 3 },
+      ],
+      results: [{ type: "item", name: "electronic-circuit", amount: 1 }],
+    } as Recipe;
+
+    const assemblerSetup: MachineSetup = {
+      machine: { name: "assembling-machine-2", crafting_speed: 1 } as Machine,
+      machineModules: [],
+      beacons: [],
+      machineQualityLevel: 0,
+    };
+
+    const cop1 = new MachineSimulator(assemblerSetup, copperCableRecipe, { "copper-cable": 200 });
+    const cop2 = new MachineSimulator(assemblerSetup, copperCableRecipe, { "copper-cable": 200 });
+    const cop3 = new MachineSimulator(assemblerSetup, copperCableRecipe, { "copper-cable": 200 });
+
+    const circ1 = new MachineSimulator(assemblerSetup, greenCircuitRecipe, { "electronic-circuit": 200 });
+    const circ2 = new MachineSimulator(assemblerSetup, greenCircuitRecipe, { "electronic-circuit": 200 });
+
+    const sourceCopper = new Chest("copper-plate");
+    const sourceIron = new Chest("iron-plate");
+    const sinkGreenChips = new Chest();
+
+    // ALL inserters set to Stack Size 16
+    const inCop1 = new InserterSimulator(16, sourceCopper, cop1, "copper-plate");
+    const inCop2 = new InserterSimulator(16, sourceCopper, cop2, "copper-plate");
+    const inCop3 = new InserterSimulator(16, sourceCopper, cop3, "copper-plate");
+
+    const inIron1 = new InserterSimulator(16, sourceIron, circ1, "iron-plate");
+    const inIron2 = new InserterSimulator(16, sourceIron, circ2, "iron-plate");
+
+    // Direct insertions (Stack size 16)
+    const mid1 = new InserterSimulator(16, cop1, circ1, "copper-cable");
+    const mid2a = new InserterSimulator(16, cop2, circ1, "copper-cable");
+    const mid2b = new InserterSimulator(16, cop2, circ2, "copper-cable");
+    const mid3 = new InserterSimulator(16, cop3, circ2, "copper-cable");
+
+    const outCirc1 = new InserterSimulator(16, circ1, sinkGreenChips, "electronic-circuit");
+    const outCirc2 = new InserterSimulator(16, circ2, sinkGreenChips, "electronic-circuit");
+
+    const allInserters = [inCop1, inCop2, inCop3, inIron1, inIron2, mid1, mid2a, mid2b, mid3, outCirc1, outCirc2];
+
+    allInserters.forEach((ins) => orchestrator.registerInserter(ins));
+    [cop1, cop2, cop3, circ1, circ2].forEach((m) => orchestrator.registerMachine(m));
+
+export function Simulator(){
+  return <FactorioSimulationDashboard orchestrator={orchestrator}></FactorioSimulationDashboard> 
+}

+ 76 - 0
src/engine/ClassicView.tsx

@@ -0,0 +1,76 @@
+
+import styles from './SimulationDashboard.module.css';
+import Icon from '../assets/icon';
+
+const INSERTER_STATES = [
+  { label: 'IDLE', color: '#475569' },
+  { label: 'PICKING', color: '#d97706' },
+  { label: 'SWING_FWD', color: '#2563eb' },
+  { label: 'DROPPING', color: '#9333ea' },
+  { label: 'SWING_BACK', color: '#64748b' }
+];
+
+export default function ClassicView({ machines, inserters }: { machines: any[], inserters: any[] }) {
+  return (
+    <div className={styles.grid}>
+      <div className={styles.panel}>
+        <div className={styles.panelTitle}>INSERTERS (Phase 2)</div>
+        <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
+          {inserters.map((ins, i) => {
+            const st = INSERTER_STATES[ins.state] || INSERTER_STATES[0];
+            return (
+              <div key={i} style={{ display: 'flex', justifyContent: 'space-between', backgroundColor: '#212121', padding: '12px', borderRadius: '4px' }}>
+                <span style={{ width: '120px', fontWeight: 'bold' }}>{ins.targetItemId}</span>
+                <span style={{ backgroundColor: st.color, padding: '2px 8px', borderRadius: '4px', width: '90px', textAlign: 'center', fontSize: '12px', fontWeight: 'bold' }}>{st.label}</span>
+                <span style={{ width: '100px', color: '#8e8e8e' }}>Held: <span style={{ color: '#fff' }}>{ins.heldItems}</span></span>
+                <span style={{ color: '#8e8e8e' }}>Swings: {ins.swingCount}</span>
+              </div>
+            );
+          })}
+        </div>
+      </div>
+
+      <div className={styles.panel}>
+        <div className={styles.panelTitle}>MACHINES (Phase 3)</div>
+        <div>
+          {machines.map((machine, i) => (
+            <div key={i} className={styles.machineCard}>
+              <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: '8px', backgroundColor: '#1a1a1a', border: '1px solid #000', marginBottom: '16px' }}>
+                <div style={{ width: `${machine.craftProgress * 100}%`, height: '100%', backgroundColor: '#5eb663' }} />
+              </div>
+              <div style={{ display: 'flex', justifyContent: 'space-between', color: '#8e8e8e', fontSize: '13px' }}>
+                <div style={{ display: 'flex', gap: '16px' }}>
+                  <strong>IN:</strong>
+                  {Object.entries(machine.inputBuffer).map(([k, v]) => (
+                    <div key={k} style={{ display: 'flex', alignItems: 'center', gap: '4px', color: v === 0 ? '#fe5a5a' : '#e3e3e3' }}>
+                      <Icon iconName={`item/${k}.png`} size={16} /> {v as number}
+                    </div>
+                  ))}
+                </div>
+                <div style={{ display: 'flex', gap: '16px' }}>
+                  <strong>OUT:</strong>
+                  {Object.entries(machine.outputBuffer).map(([k, v]) => {
+                    const limit = machine.outputBlockLimits[k];
+                    const isFull = (v as number) >= limit;
+                    return (
+                      <div key={k} style={{ display: 'flex', alignItems: 'center', gap: '4px', color: isFull ? '#fe5a5a' : '#e3e3e3' }}>
+                        <Icon iconName={`item/${k}.png`} size={16} /> 
+                        <strong style={{ fontWeight: isFull ? 'bold' : 'normal' }}>{v as number} / {limit}</strong>
+                      </div>
+                    );
+                  })}
+                </div>
+              </div>
+            </div>
+          ))}
+        </div>
+      </div>
+    </div>
+  );
+}

+ 88 - 0
src/engine/FactorioSimulationDashBoard.tsx

@@ -0,0 +1,88 @@
+import  { useState, useEffect, useRef } from 'react';
+import styles from './SimulationDashboard.module.css';
+import ClassicView from './ClassicView';
+import GraphView from './GraphView';
+import TimelineView from './TimelineView';
+import type { FactorioEngineOrchestrator } from './simulator';
+
+export default function FactorioSimulationDashboard({ orchestrator }: { orchestrator: FactorioEngineOrchestrator }) {
+  const [viewMode, setViewMode] = useState<'classic' | 'graph' | 'timeline'>('classic');
+  const [tick, setTick] = useState(0);
+  const [isPlaying, setIsPlaying] = useState(false);
+  const [tps, setTps] = useState(60); 
+  const [stepAmount, setStepAmount] = useState(1); 
+  
+  const [history, setHistory] = useState<any[]>([]);
+  const requestRef = useRef<number>(1);
+  const lastUpdateRef = useRef<number>(0);
+
+  const executeTick = () => {
+    orchestrator.tick();
+    const currentTick = orchestrator.currentTick;
+    setTick(currentTick);
+
+    setHistory(prev => {
+      const snapshot = {
+        tick: currentTick,
+        machines: orchestrator.getMachines().map(m => ({ 
+          isCrafting: m.isCrafting, 
+          out: Object.values(m.outputBuffer)[0] || 0,
+          in: Object.values(m.inputBuffer).reduce((a,b) => a + (b as number), 0)
+        })),
+        inserters: orchestrator.getInserters().map(ins => ({ 
+          state: ins.state, 
+          held: ins.heldItems 
+        }))
+      };
+      const next = [...prev, snapshot];
+      if (next.length > 300) next.shift();
+      return next;
+    });
+  };
+
+  const advanceSimulation = (time: number) => {
+    if (!lastUpdateRef.current) lastUpdateRef.current = time;
+    if (time - lastUpdateRef.current >= 1000 / tps) {
+      executeTick();
+      lastUpdateRef.current = time;
+    }
+    if (isPlaying) requestRef.current = requestAnimationFrame(advanceSimulation);
+  };
+
+  useEffect(() => {
+    if (isPlaying) requestRef.current = requestAnimationFrame(advanceSimulation);
+    else cancelAnimationFrame(requestRef.current);
+    return () => cancelAnimationFrame(requestRef.current);
+  }, [isPlaying, tps]);
+
+  return (
+    <div className={styles.dashboard}>
+      <div className={styles.header}>
+        <div className={styles.controlsRow}>
+          <div style={{ color: '#e39827', fontSize: '24px', fontWeight: 'bold' }}>
+            TICK: {tick.toString().padStart(6, '0')}
+          </div>
+          
+          <button className={`${styles.button} ${viewMode === 'classic' ? styles.active : ''}`} onClick={() => setViewMode('classic')}>Classic</button>
+          <button className={`${styles.button} ${viewMode === 'graph' ? styles.active : ''}`} onClick={() => setViewMode('graph')}>Graph</button>
+          <button className={`${styles.button} ${viewMode === 'timeline' ? styles.active : ''}`} onClick={() => setViewMode('timeline')}>Timeline</button>
+        </div>
+        
+        <div className={styles.controlsRow}>
+          <button className={isPlaying ? styles['button-red'] : styles['button-green']} onClick={() => setIsPlaying(!isPlaying)}>
+            {isPlaying ? '⏸ PAUSE' : '▶ PLAY'}
+          </button>
+          
+          <input type="number" value={stepAmount} onChange={(e) => setStepAmount(Math.max(1, parseInt(e.target.value) || 1))} style={{ width: '60px', textAlign: 'center' }} />
+          <button className={`${styles.button} ${isPlaying ? styles.disabled : ''}`} onClick={() => { for(let i=0; i<stepAmount; i++) executeTick(); }} disabled={isPlaying}>⏭ STEP</button>
+        </div>
+      </div>
+
+      <div className={styles.viewContainer}>
+        {viewMode === 'classic' && <ClassicView machines={orchestrator.getMachines()} inserters={orchestrator.getInserters()} />}
+        {viewMode === 'graph' && <GraphView orchestrator={orchestrator} tick={tick} />}
+        {viewMode === 'timeline' && <TimelineView history={history} machines={orchestrator.getMachines()} inserters={orchestrator.getInserters()} />}
+      </div>
+    </div>
+  );
+}

+ 102 - 0
src/engine/GraphView.tsx

@@ -0,0 +1,102 @@
+import { useMemo, useEffect, useState } from 'react';
+import ReactFlow, { Background, Controls, MarkerType } from 'reactflow';
+import 'reactflow/dist/style.css';
+import styles from './SimulationDashboard.module.css';
+
+const MachineNode = ({ data }: any) => {
+  const machine = data.instance;
+  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 as number}</span>
+          ))}
+        </div>
+        <div style={{ display: 'flex', gap: '4px' }}>
+          <span style={{ color: '#8e8e8e' }}>OUT:</span>
+          {Object.entries(machine.outputBuffer).map(([k, v]) => {
+            const isFull = (v as number) >= machine.outputBlockLimits[k];
+            return <span key={k} style={{ color: isFull ? '#fe5a5a' : '#fff' }}>{v as number}</span>;
+          })}
+        </div>
+      </div>
+    </div>
+  );
+};
+
+const ChestNode = ({ data }: any) => (
+  <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 };
+
+export default function GraphView({ orchestrator, tick }: { orchestrator: any, tick: number }) {
+  const { initialNodes, initialEdges } = useMemo(() => {
+    const nodes: any[] = [];
+    const edges: any[] = [];
+    const objToId = new Map();
+    let idCounter = 0;
+
+    const getId = (obj: any, type: string, label: 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: any, i: number) => {
+      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]);
+
+  const [nodes, setNodes] = useState(initialNodes);
+  const [edges, setEdges] = useState(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]);
+
+  return (
+    <div className={styles.graphWrapper}>
+      <ReactFlow nodes={nodes} edges={edges} nodeTypes={nodeTypes} fitView>
+        <Background color="#5f5f5f" gap={16} />
+        <Controls />
+      </ReactFlow>
+    </div>
+  );
+}

+ 160 - 0
src/engine/SimulationDashboard.module.css

@@ -0,0 +1,160 @@
+/* Dashboard.module.css */
+
+.dashboard {
+  background-color: #212121;
+  color: #e2e8f0;
+  min-height: 100vh;
+  display: flex;
+  flex-direction: column;
+  font-family: monospace;
+}
+
+.header {
+  padding: 16px;
+  background-color: #313031;
+  border-bottom: 2px solid #000;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.controlsRow {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+}
+
+.viewContainer {
+  padding: 24px;
+  flex-grow: 1;
+  display: flex;
+  flex-direction: column;
+}
+
+/* --- Layout & Panel Styles --- */
+.grid {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 32px;
+}
+
+.panel {
+  background-color: #313031;
+  border: 1px solid #5f5f5f;
+  padding: 16px;
+  border-radius: 4px;
+}
+
+.panelTitle {
+  color: #e3e3e3;
+  font-size: 1.1rem;
+  font-weight: bold;
+  border-bottom: 1px solid #5f5f5f;
+  padding-bottom: 8px;
+  margin-bottom: 16px;
+}
+
+.machineCard {
+  background-color: #212121;
+  border: 1px solid #000;
+  padding: 16px;
+  border-radius: 4px;
+  margin-bottom: 16px;
+}
+
+/* FIX: Fixed width prevents the layout jumping when switching from CRAFTING to HALTED */
+.statusBadge {
+  width: 100px;
+  text-align: right;
+  display: inline-block;
+  font-weight: bold;
+}
+
+.graphWrapper {
+  flex-grow: 1;
+  min-height: 700px;
+  height: 100%;
+  width: 100%;
+  background-color: #1a1a1a;
+}
+
+.timelineRow {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+  margin-bottom: 24px;
+}
+
+.timelineLabel {
+  width: 150px;
+  font-weight: bold;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.timelineGraph {
+  flex-grow: 1;
+  background-color: #212121;
+  border: 1px solid #000;
+  position: relative;
+}
+
+/* --- USER PROVIDED FACTORIO BUTTONS --- */
+.panel-button {
+  border: none;
+  cursor: pointer;
+  position: relative;
+  background-color: #313031;
+  height: 24px;
+  aspect-ratio: 1;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow: inset 8px 0px 4px -8px #000, inset -8px 0px 4px -8px #000, inset 0px 8px 4px -8px #000, inset 0px -8px 4px -8px #000, inset 0px 8px 4px -8px #fff, inset 0px -8px 2px -8px #432400, 0px 0px 4px 0px #000;
+}
+.panel-button:active, .panel-button.active {
+  box-shadow: inset 0px 9px 2px -8px #000, inset 8px 0px 4px -8px #563a10, inset 8px 0px 4px -8px #563a10, inset -8px 0px 4px -8px #563a10, inset -8px 0px 4px -8px #563a10, inset 0px -9px 2px -8px #fff, 0px 0px 4px 0px #000;
+  background-color: #f1be64;
+  filter: none;
+  outline: 0;
+}
+.panel-button.hover, .panel-button:focus, .panel-button:hover {
+  color: #000;
+  text-decoration: none;
+  outline: 0;
+  box-shadow: inset 8px 0px 4px -8px #000, inset -8px 0px 4px -8px #000, inset 0px 9px 2px -8px #fff, inset 0px -8px 4px -8px hsl(36, 94%, 20%), 0px 0px 4px 0px #000, inset 0px 0px 4px 2px #f9b44b;
+  background-color: #e39827;
+  filter: drop-shadow(0 0 2px #f9b44b);
+}
+input:focus { border: none; filter: drop-shadow(0 0 2px #f9b44b); }
+.button {
+  background-color: #8e8e8e; padding: 10px 12px; font-size: 100%; text-align: left; color: #000; font-weight: 600; display: inline-block; vertical-align: baseline; min-width: 128px; border: none; line-height: inherit; white-space: nowrap; box-shadow: inset 8px 0px 4px -8px #000, inset -8px 0px 4px -8px #000, inset 0px 10px 2px -8px #e3e3e3, inset 0px 10px 2px -8px #282828, inset 0px -9px 2px -8px #000, 0px 0px 4px 0px #000; position: relative; margin-right: 14px; cursor: pointer; user-select: none; height: 36px; text-align: left;
+}
+.button:hover, .button.hover {
+  box-shadow: inset 8px 0px 4px -8px #000, inset -8px 0px 4px -8px #000, inset 0px 9px 2px -8px #fff, inset 0px 8px 4px -8px #000, inset 0px -8px 4px -8px #000, inset 0px -9px 2px -8px #432400, 0px 0px 4px 0px #000, inset 0px 0px 4px 2px #f9b44b; background-color: #e39827; filter: drop-shadow(0 0 2px #f9b44b);
+}
+.button:active, .button.active {
+  box-shadow: inset 0px 10px 2px -8px #000, inset 0px 9px 2px -8px #000, inset 8px 0px 4px -8px #563a10, inset 8px 0px 4px -8px #563a10, inset -8px 0px 4px -8px #563a10, inset -8px 0px 4px -8px #563a10, inset 0px 9px 2px -8px #563a10, inset 0px -9px 2px -8px #563a10, inset 0px -8.5px 0px -8px #563a10, 0px 0px 4px 0px #000; background-color: #f1be64;
+}
+.button.disabled {
+  background-color: #3d3d3d; color: #818181; box-shadow: inset 8px 0px 4px -8px #000, inset -8px 0px 4px -8px #000, inset 0px 8px 4px -8px #000, inset 0px -6px 4px -8px #818181, inset 0px -8px 4px -8px #000, 0px 0px 4px 0px #000;
+}
+.button-green {
+  background-color: #5eb663; padding: 10px 12px; font-size: 100%; color: #000; font-weight: 600; display: inline-block; min-width: 128px; border: none; box-shadow: inset 8px 0px 4px -8px #000, inset -8px 0px 4px -8px #000, inset 0px 10px 2px -8px #95df99, inset 0px 10px 2px -8px #163218, inset 0px -9px 2px -8px #000, 0px 0px 4px 0px #000; margin-right: 14px; cursor: pointer; user-select: none; height: 36px;
+}
+.button-green:hover {
+  box-shadow: inset 8px 0px 4px -8px #000, inset -8px 0px 4px -8px #000, inset 0px 9px 2px -8px #cdf1cf, inset 0px 8px 4px -8px #000, inset 0px -8px 4px -8px #000, inset 0px -9px 2px -8px #432400, 0px 0px 4px 0px #000, inset 0px 0px 4px 2px #34be3c; background-color: #92e897; filter: drop-shadow(0 0 2px #34be3c);
+}
+.button-red {
+  background-color: #fe5a5a; padding: 10px 12px; font-size: 100%; color: #000; font-weight: 600; display: inline-block; min-width: 128px; border: none; box-shadow: inset 8px 0px 4px -8px #000, inset -8px 0px 4px -8px #000, inset 0px 10px 2px -8px #fda1a1, inset 0px 10px 2px -8px #8b0101, inset 0px -9px 2px -8px #000, 0px 0px 4px 0px #000; margin-right: 14px; cursor: pointer; user-select: none; height: 36px;
+}
+.button-red:hover {
+  box-shadow: inset 8px 0px 4px -8px #000, inset -8px 0px 4px -8px #000, inset 0px 9px 2px -8px #f8eaea, inset 0px 8px 4px -8px #000, inset 0px -8px 4px -8px #000, inset 0px -9px 2px -8px #432400, 0px 0px 4px 0px #000, inset 0px 0px 4px 2px #c35353; background-color: #ff9b9b; filter: drop-shadow(0 0 2px #c35353);
+}
+input[type="text"], input[type="number"] {
+  height: 36px; background: #8e8e8e; border-radius: 4px; padding: 6px; border: none; box-shadow: inset 0px 4px 1px -2px #000, inset 0px -4px 1px -2px #c5c5c5, inset 2px 0px 1px 0px #5f5f5f, inset -2px 0px 1px 0px #5f5f5f, inset 0px -2px 2px 0px #5f5f5f, 0px 0px 4px 1px #2e2521; outline: none;
+}
+input[type="text"]:focus, input[type="number"]:focus {
+  color: #000; background: #f0dab4; box-shadow: inset 0px 4px 2px -2px #000, inset 0px -1px 1px 0px #74624b, inset 0px -4px 2px -2px #e0e0e0, inset 2px 0px 2px 0px #a6885c, inset -2px 0px 2px 0px #a6885c, 0px 0px 4px 1px #2e2521;
+}

+ 94 - 0
src/engine/TimelineView.tsx

@@ -0,0 +1,94 @@
+import styles from './SimulationDashboard.module.css';
+
+const MAX_HISTORY = 300;
+const INSERTER_STATES = ['#475569', '#d97706', '#2563eb', '#9333ea', '#64748b'];
+
+export default function TimelineView({ history, machines, inserters }: { history: any[], machines: any[], inserters: any[] }) {
+  
+  return (
+    <div className={styles.panel}>
+      <div className={styles.panelTitle}>Historical Buffer States (Last {MAX_HISTORY} Ticks)</div>
+      
+      {/* MACHINE TIMELINES */}
+      {machines.map((machine, mIndex) => {
+        const outLimit = Object.values(machine.outputBlockLimits)[0] || 1;
+        
+        // Find max input dynamically to scale the input curve
+        const maxIn = Math.max(1, ...history.map(h => h.machines[mIndex]?.in || 0));
+
+        // Shift points so they enter from the right and scroll left
+        const pointsOut = history.map((h, i) => `${MAX_HISTORY - history.length + i},${45 - ((h.machines[mIndex]?.out || 0) / outLimit * 45)}`).join(' ');
+        const pointsIn = history.map((h, i) => `${MAX_HISTORY - history.length + i},${45 - ((h.machines[mIndex]?.in || 0) / maxIn * 45)}`).join(' ');
+
+        return (
+          <div key={`m-${mIndex}`} className={styles.timelineRow}>
+            <div className={styles.timelineLabel} style={{ color: '#5eb663' }}>{machine.recipe.name}</div>
+            
+            <div className={styles.timelineGraph} style={{ height: '50px' }}>
+              <svg viewBox={`0 0 ${MAX_HISTORY} 50`} preserveAspectRatio="none" style={{ width: '100%', height: '100%', display: 'block' }}>
+                {/* 1. Input Curve (Blue) */}
+                <polyline points={pointsIn} fill="none" stroke="#38bdf8" strokeWidth="2" vectorEffect="non-scaling-stroke" />
+                {/* 2. Output Curve (Orange) */}
+                <polyline points={pointsOut} fill="none" stroke="#e39827" strokeWidth="2" vectorEffect="non-scaling-stroke" />
+                
+                {/* 3. 3px Status Bar at bottom (mapped to exact tick coordinates) */}
+                <g>
+                  {history.map((h, i) => (
+                    <rect 
+                      key={i} 
+                      x={MAX_HISTORY - history.length + i} 
+                      y={47} 
+                      width={1} 
+                      height={3} 
+                      fill={h.machines[mIndex]?.isCrafting ? '#5eb663' : '#fe5a5a'} 
+                    />
+                  ))}
+                </g>
+              </svg>
+            </div>
+            
+            <div style={{ fontSize: '10px', color: '#8e8e8e', width: '50px' }}>
+              <span style={{ color: '#38bdf8' }}>IN</span><br/>
+              <span style={{ color: '#e39827' }}>OUT</span>
+            </div>
+          </div>
+        );
+      })}
+
+      <div className={styles.panelTitle} style={{ marginTop: '32px' }}>Inserter Activity & Held Items</div>
+
+      {/* INSERTER TIMELINES */}
+      {inserters.map((ins, iIndex) => {
+        const capacity = ins.handSize || 16;
+        const pointsHeld = history.map((h, i) => `${MAX_HISTORY - history.length + i},${25 - ((h.inserters[iIndex]?.held || 0) / capacity * 25)}`).join(' ');
+
+        return (
+          <div key={`i-${iIndex}`} className={styles.timelineRow}>
+            <div className={styles.timelineLabel} style={{ color: '#94a3b8' }}>{ins.targetItemId}</div>
+            
+            <div className={styles.timelineGraph} style={{ height: '30px' }}>
+              <svg viewBox={`0 0 ${MAX_HISTORY} 30`} preserveAspectRatio="none" style={{ width: '100%', height: '100%', display: 'block' }}>
+                {/* 1. Held Items Curve (Yellow) */}
+                <polyline points={pointsHeld} fill="none" stroke="#fcd34d" strokeWidth="2" vectorEffect="non-scaling-stroke" />
+                
+                {/* 2. 3px Status Bar at bottom */}
+                <g>
+                  {history.map((h, i) => (
+                    <rect 
+                      key={i} 
+                      x={MAX_HISTORY - history.length + i} 
+                      y={27} 
+                      width={1} 
+                      height={3} 
+                      fill={INSERTER_STATES[h.inserters[iIndex]?.state] || '#000'} 
+                    />
+                  ))}
+                </g>
+              </svg>
+            </div>
+          </div>
+        );
+      })}
+    </div>
+  );
+}

+ 9 - 12
src/engine/simulator.test.ts

@@ -588,8 +588,8 @@ describe("Factorio Strict Phase Orchestrator", () => {
     [cop1, cop2, cop3, circ1, circ2].forEach((m) => orchestrator.registerMachine(m));
 
     // --- PHASE 1: STABILIZATION ---
-    // Run for 600 ticks (~10 seconds) to fill all buffers and start the cascades
-    orchestrator.tickUntil(() => false, 300);
+    // 16 items * 30 ticks = 488 ticks per complete buffer cycle
+    orchestrator.tickUntil(() => false, 480);
 
     // Reset all swing counters to 0 to prepare for the measurement window
     allInserters.forEach((ins) => (ins.swingCount = 0));
@@ -598,23 +598,20 @@ describe("Factorio Strict Phase Orchestrator", () => {
     // Run for exactly 1440 ticks.
     // Speed 1 machine = 30 ticks per craft. 1440 / 30 = 48 crafts perfectly.
     // 48 crafts * 1 plate = 48 plates. 48 / 16 stack size = exactly 3 swings!
-    const targetTick = orchestrator.currentTick + 1452;
+    const targetTick = orchestrator.currentTick + 1440;
     orchestrator.tickUntil(() => false, targetTick);
 
-    // Validate Copper Inputs (3 swings each = 48 plates per machine)
-    expect(inCop1.swingCount).toBe(3);
-    expect(inCop2.swingCount).toBe(3);
-    expect(inCop3.swingCount).toBe(3);
+    // Validate Outputs (3 swings each = 48 chips per machine)
+    expect(outCirc1.swingCount).toBe(3);
+    expect(outCirc2.swingCount).toBe(3);
 
+    
     // Validate Iron Inputs (3 swings each = 48 plates per machine)
-    expect(circ1.outputBuffer["electronic-circuit"]).toBe(0);
-    console.log(circ1.inputBuffer);
     expect(inIron1.swingCount).toBe(3);
     expect(inIron2.swingCount).toBe(3);
 
-    // Validate Outputs (3 swings each = 48 chips per machine)
-    expect(outCirc1.swingCount).toBe(3);
-    expect(outCirc2.swingCount).toBe(3);
+    // Validate Copper Inputs (3 swings each = 48 plates per machine)
+    expect([inCop1.swingCount,inCop2.swingCount,inCop3.swingCount]).toBe([3,3,3]);
   });
 
   it("Clocked Baseline: 3:2 setup with staggered shared-inserter rows and exact 8-tick windows", () => {

+ 62 - 24
src/engine/simulator.ts

@@ -63,7 +63,7 @@ export class MachineSimulator implements IContainer {
   private solidResults: { name: string; amount: number }[] = [];
 
   private overloadLimits: Record<string, number> = {};
-  private outputBlockLimits: Record<string, number> = {};
+  public outputBlockLimits: Record<string, number> = {};
 
   constructor(
     public setup: MachineSetup,
@@ -77,7 +77,8 @@ export class MachineSimulator implements IContainer {
     for (const ing of ingredients) {
       if (ing.type === "item") {
         this.solidIngredients.push({ name: ing.name, amount: ing.amount });
-        this.overloadLimits[ing.name] = ing.amount * this.timings.overloadMultiplier;
+        this.overloadLimits[ing.name] =
+          ing.amount * this.timings.overloadMultiplier;
         this.inputBuffer[ing.name] = 0;
       }
     }
@@ -142,7 +143,8 @@ export class MachineSimulator implements IContainer {
 
   private isOutputBlocked(): boolean {
     for (const res of this.solidResults) {
-      if (this.outputBuffer[res.name] >= this.outputBlockLimits[res.name]) return true;
+      if (this.outputBuffer[res.name] >= this.outputBlockLimits[res.name])
+        return true;
     }
     return false;
   }
@@ -200,7 +202,8 @@ export class Chest implements IContainer {
 
   public extract(itemId: string, maxAmount: number): number {
     if (this.getAvailable(itemId)) {
-      this.extractedCounts[itemId] = (this.extractedCounts[itemId] || 0) + maxAmount;
+      this.extractedCounts[itemId] =
+        (this.extractedCounts[itemId] || 0) + maxAmount;
       return maxAmount;
     }
     return 0;
@@ -239,7 +242,8 @@ export class Belt implements IContainer {
   public extract(itemId: string, maxAmount: number): number {
     if (this.getAvailable(itemId)) {
       const amount = Math.min(maxAmount, 4);
-      this.extractedCounts[itemId] = (this.extractedCounts[itemId] || 0) + amount;
+      this.extractedCounts[itemId] =
+        (this.extractedCounts[itemId] || 0) + amount;
       return amount;
     }
     return 0;
@@ -349,7 +353,9 @@ export class InserterSimulator {
 
   protected canWakeUp(): boolean {
     return (
-      this.isActive && this.source.getAvailable(this.targetItemId) > 0 && this.destination.canAccept(this.targetItemId)
+      this.isActive &&
+      this.source.getAvailable(this.targetItemId) > 0 &&
+      this.destination.canAccept(this.targetItemId)
     );
   }
 }
@@ -360,7 +366,12 @@ export class FilterableInserterSimulator extends InserterSimulator {
   public useDynamicFilters = false;
   public dynamicFilters: string[] = [];
 
-  constructor(handSize: number, source: IContainer, destination: IContainer, filters: string[] = []) {
+  constructor(
+    handSize: number,
+    source: IContainer,
+    destination: IContainer,
+    filters: string[] = [],
+  ) {
     // Pass the first filter as a dummy fallback to super()
     super(handSize, source, destination, filters[0] || "");
     this.staticFilters = filters;
@@ -389,11 +400,16 @@ export class FilterableInserterSimulator extends InserterSimulator {
   protected override canWakeUp(): boolean {
     if (!this.isActive) return false;
 
-    const activeFilters = this.useDynamicFilters ? this.dynamicFilters : this.staticFilters;
+    const activeFilters = this.useDynamicFilters
+      ? this.dynamicFilters
+      : this.staticFilters;
 
     // Scan the filters in order of priority (left to right in Factorio UI)
     for (const itemId of activeFilters) {
-      if (this.source.getAvailable(itemId) > 0 && this.destination.canAccept(itemId)) {
+      if (
+        this.source.getAvailable(itemId) > 0 &&
+        this.destination.canAccept(itemId)
+      ) {
         this.currentTargetItem = itemId;
         return true;
       }
@@ -501,9 +517,19 @@ export class FactorioEngineOrchestrator {
 
   // Entity Managers (Strict Execution Order!)
   private rows = new Map<string, OptimizedClockRow>();
+  private rowsArray: OptimizedClockRow[] = [];
   private belts: IContainer[] = [];
   private inserters: InserterSimulator[] = [];
   private machines: MachineSimulator[] = [];
+  public getMachines() {
+    return this.machines;
+  }
+  public getInserters() {
+    return this.inserters;
+  }
+
+  private sleepingOrHoveringInserters: InserterSimulator[] = [];
+  private successfullyPickedInserters: InserterSimulator[] = [];
 
   public registerBelt(belt: IContainer) {
     this.belts.push(belt);
@@ -515,9 +541,14 @@ export class FactorioEngineOrchestrator {
   public registerInserter(inserter: InserterSimulator) {
     this.inserters.push(inserter);
   }
-  public registerRow(rowId: string, blocks: { start: number; end: number }[], cycleDuration: number) {
+  public registerRow(
+    rowId: string,
+    blocks: { start: number; end: number }[],
+    cycleDuration: number,
+  ) {
     const row = new OptimizedClockRow(rowId, blocks, cycleDuration);
     this.rows.set(rowId, row);
+    this.rowsArray.push(row);
   }
 
   public bindInserterToRow(inserter: InserterSimulator, rowId: string) {
@@ -532,38 +563,45 @@ export class FactorioEngineOrchestrator {
 
   public tick() {
     // --- CLOCK & NETWORK STATE UPDATES ---
-    for (const row of this.rows.values()) {
-      row.tick(this.currentTick);
+    for (let i = 0; i < this.rowsArray.length; i++) {
+      this.rowsArray[i].tick(this.currentTick);
     }
     // Belts
-    for (const belt of this.belts) {
+    for (let i = 0; i < this.belts.length; i++) {
+      const belt = this.belts[i];
       if (belt.tick) belt.tick();
     }
     // Inserters
-    const sleepingOrHoveringInserters: InserterSimulator[] = [];
-    const successfullyPickedInserters: InserterSimulator[] = [];
+    this.sleepingOrHoveringInserters.length = 0;
+    this.successfullyPickedInserters.length = 0;
 
-    for (const inserter of this.inserters) {
+    for (let i = 0; i < this.inserters.length; i++) {
+      const inserter = this.inserters[i];
       const heldBefore = inserter.heldItems;
 
       inserter.tick();
 
-      const pickedItemsThisTick = inserter.heldItems > heldBefore;
-
-      if (pickedItemsThisTick) {
+      if (inserter.heldItems > heldBefore) {
         // It got items, so it yields priority for the next tick
-        successfullyPickedInserters.push(inserter);
+        this.successfullyPickedInserters.push(inserter);
       } else {
         // It got nothing (starved), so it maintains its priority at the front of the line
-        sleepingOrHoveringInserters.push(inserter);
+        this.sleepingOrHoveringInserters.push(inserter);
       }
     }
 
-    this.inserters = [...sleepingOrHoveringInserters, ...successfullyPickedInserters];
+    this.inserters.length = 0;
+
+    for (let i = 0; i < this.sleepingOrHoveringInserters.length; i++) {
+      this.inserters.push(this.sleepingOrHoveringInserters[i]);
+    }
+    for (let i = 0; i < this.successfullyPickedInserters.length; i++) {
+      this.inserters.push(this.successfullyPickedInserters[i]);
+    }
 
     // Assemblers
-    for (const machine of this.machines) {
-      machine.tick();
+    for (let i = 0; i < this.machines.length; i++) {
+      this.machines[i].tick();
     }
 
     this.currentTick++;

+ 2 - 0
src/main.tsx

@@ -7,6 +7,7 @@ import ComponentTests from "./ComponentTests.tsx";
 import Layout from "./Layout.tsx";
 import ClockBuilder from "./ClockBuilder.tsx";
 import ClockWizard from "./assets/components/ClockWizard.tsx";
+import { Simulator } from "./assets/Simulator.tsx";
 
 const root = document.getElementById("root");
 
@@ -19,6 +20,7 @@ ReactDOM.createRoot(root as HTMLElement).render(
           <Route index element={<ClockBuilder />} />
           <Route path="tests" element={<ComponentTests />} />
           <Route path="InputConfigurator" element={<ClockWizard />} />
+          <Route path="simulator" element={<Simulator />} />
         </Route>
       </Routes>
     </BrowserRouter>