Ver Fonte

refactor

clovis há 1 mês atrás
pai
commit
633caa3b73
34 ficheiros alterados com 1544 adições e 1155 exclusões
  1. 134 120
      package-lock.json
  2. 15 55
      src/ClockBuilder.tsx
  3. 3 3
      src/ComponentTests.tsx
  4. 1 14
      src/assets/ClockTimeline/ClockTimeline.module.css
  5. 9 25
      src/assets/ClockTimeline/ClockTimeline.tsx
  6. 23 71
      src/assets/ClockTimeline/TimelineRow.tsx
  7. 3 0
      src/assets/ClockTimeline/index.ts
  8. 0 0
      src/assets/SelectedBlockPanel.module.css
  9. 80 0
      src/assets/SelectedBlockPanel.tsx
  10. 0 0
      src/assets/Selector/MachineSelector.module.css
  11. 6 6
      src/assets/Selector/MachineSelector.tsx
  12. 0 0
      src/assets/Selector/SelectFactorioMenu.module.css
  13. 15 55
      src/assets/Selector/SelectFactorioMenu.tsx
  14. 1 0
      src/assets/Selector/SelectSignal.module.css
  15. 7 14
      src/assets/Selector/SelectSignal.tsx
  16. 51 49
      src/assets/Simulator.tsx
  17. 1 1
      src/assets/components/ClockWizard.tsx
  18. 57 28
      src/assets/components/ModuleSlots.tsx
  19. 0 117
      src/assets/components/SelectedBlockPanel.tsx
  20. 9 2
      src/assets/types.ts
  21. 1 1
      src/blueprint/Blueprintbuilder.ts
  22. 0 77
      src/engine/ClassicView.tsx
  23. 131 0
      src/engine/Dashboard/ClassicView.tsx
  24. 378 0
      src/engine/Dashboard/Dashboard.module.css
  25. 129 0
      src/engine/Dashboard/FactorioSimulationDashBoard.tsx
  26. 151 0
      src/engine/Dashboard/GraphView.tsx
  27. 281 0
      src/engine/Dashboard/TimelineView.tsx
  28. 43 0
      src/engine/Dashboard/types.ts
  29. 0 120
      src/engine/FactorioSimulationDashBoard.tsx
  30. 0 103
      src/engine/GraphView.tsx
  31. 0 160
      src/engine/SimulationDashboard.module.css
  32. 0 95
      src/engine/TimelineView.tsx
  33. 3 4
      src/engine/simulator.test.ts
  34. 12 35
      src/engine/simulator.ts

Diff do ficheiro suprimidas por serem muito extensas
+ 134 - 120
package-lock.json


+ 15 - 55
src/ClockBuilder.tsx

@@ -1,14 +1,14 @@
 import { useEffect, useMemo, useRef, useState } from "react";
-import ClockTimeline from "./assets/ClockTimeline";
 import { buildBlueprint } from "./blueprint/Blueprintbuilder";
 import { encodeBlueprintFileBrowser } from "./blueprint/parser";
 import type { ClockBlock } from "./assets/types";
 import styles from "./ClockBuilder.module.css";
-import SelectedBlockPanel from "./assets/components/SelectedBlockPanel";
-import SelectSignal from "./assets/SelectSignal";
+import SelectedBlockPanel from "./assets/SelectedBlockPanel";
+import SelectSignal from "./assets/Selector/SelectSignal";
 import ExpressionInput from "./assets/components/ExpressionInpux";
 import { useClockStore } from "./store/useClockStore";
 import ClockWizard from "./assets/components/ClockWizard";
+import ClockTimeline from "./assets/ClockTimeline";
 
 export default function ClockBuilder() {
   //  Subscribe to the Global Store
@@ -33,20 +33,14 @@ export default function ClockBuilder() {
     itemsPerSecond: 240,
   });
   const [output, setOutput] = useState("");
-  const [status, setStatus] = useState<{ text: string; ok: boolean } | null>(
-    null,
-  );
+  const [status, setStatus] = useState<{ text: string; ok: boolean } | null>(null);
 
   // Derived Data for Exports/Generation
   // We convert our normalized Objects back to Arrays on the fly for the Factorio generator
-  const rowsArray = useMemo(
-    () => rowOrder.map((id) => rows[id]),
-    [rows, rowOrder],
-  );
+  const rowsArray = useMemo(() => rowOrder.map((id) => rows[id]), [rows, rowOrder]);
   const blocksArray = useMemo(() => Object.values(blocks), [blocks]);
   const totalCombinators = useMemo(() => rowsArray.length + 1, [rowsArray]);
 
-  // Keyboard Shortcuts (Ctrl+D and Delete)
   useEffect(() => {
     const isEditable = (el: Element | null) =>
       !!el &&
@@ -117,12 +111,8 @@ export default function ClockBuilder() {
         const parsed = JSON.parse(reader.result as string);
 
         // Re-normalize the saved array data before injecting it into the store
-        const loadedRows = Object.fromEntries(
-          parsed.rows.map((r: any) => [r.id, r]),
-        );
-        const loadedBlocks = Object.fromEntries(
-          parsed.blocks.map((b: any) => [b.id, b]),
-        );
+        const loadedRows = Object.fromEntries(parsed.rows.map((r: any) => [r.id, r]));
+        const loadedBlocks = Object.fromEntries(parsed.blocks.map((b: any) => [b.id, b]));
         const loadedRowOrder = parsed.rows.map((r: any) => r.id);
 
         loadState({
@@ -191,17 +181,11 @@ export default function ClockBuilder() {
 
         <div className={styles.field}>
           <label>Clock signal icon</label>
-          <SelectSignal
-            value={clockSignal?.name}
-            onSelectSignal={(_, sig) => setClockSignal(sig)}
-          />
+          <SelectSignal value={clockSignal?.name} onSelectSignal={(_, sig) => setClockSignal(sig)} />
         </div>
         <div className={styles.field}>
           <label>Throughput unit</label>
-          <select
-            value={displayUnit}
-            onChange={(e) => setDisplayUnit(e.target.value as "s" | "m")}
-          >
+          <select value={displayUnit} onChange={(e) => setDisplayUnit(e.target.value as "s" | "m")}>
             <option value="s">items/s</option>
             <option value="m">items/min</option>
           </select>
@@ -211,9 +195,7 @@ export default function ClockBuilder() {
           <input
             type="text"
             value={beltReference.name}
-            onChange={(e) =>
-              setBeltReference({ ...beltReference, name: e.target.value })
-            }
+            onChange={(e) => setBeltReference({ ...beltReference, name: e.target.value })}
           />
           <input
             type="number"
@@ -239,11 +221,7 @@ export default function ClockBuilder() {
           <button className={styles.generateBtn} onClick={handleGenerate}>
             Generate blueprint
           </button>
-          <button
-            className={styles.copyBtn}
-            onClick={handleCopy}
-            disabled={!output}
-          >
+          <button className={styles.copyBtn} onClick={handleCopy} disabled={!output}>
             Copy string
           </button>
           <span className={styles.count}>
@@ -252,31 +230,13 @@ export default function ClockBuilder() {
           <button className={styles.copyBtn} onClick={handleSaveFile}>
             Save clock
           </button>
-          <input
-            ref={fileInputRef}
-            type="file"
-            accept="application/json"
-            hidden
-            onChange={handleLoadFile}
-          />
-          <button
-            className={styles.copyBtn}
-            onClick={() => fileInputRef.current?.click()}
-          >
+          <input ref={fileInputRef} type="file" accept="application/json" hidden onChange={handleLoadFile} />
+          <button className={styles.copyBtn} onClick={() => fileInputRef.current?.click()}>
             Load clock
           </button>
-          {status && (
-            <span className={status.ok ? styles.statusOk : styles.statusErr}>
-              {status.text}
-            </span>
-          )}
+          {status && <span className={status.ok ? styles.statusOk : styles.statusErr}>{status.text}</span>}
         </div>
-        <textarea
-          className={styles.output}
-          readOnly
-          value={output}
-          placeholder="Blueprint string will appear here…"
-        />
+        <textarea className={styles.output} readOnly value={output} placeholder="Blueprint string will appear here…" />
       </div>
     </div>
   );

+ 3 - 3
src/ComponentTests.tsx

@@ -2,12 +2,12 @@ import { FormControl, InputLabel, MenuItem, Select, TextField, type SelectChange
 import "simplebar-react/dist/simplebar.min.css";
 
 import data from "./assets/data/2.0/data.json";
-import SelectMenu from "./assets/SelectFactorioMenu";
+import SelectMenu from "./assets/Selector/SelectFactorioMenu";
 import { useMemo, useState } from "react";
 import { decodeBlueprintFileBrowser } from "./blueprint/parser";
-import MachineSelector from "./assets/MachineSelector";
+import MachineSelector from "./assets/Selector/MachineSelector";
 import Icon from "./assets/icon";
-import SelectSignal from "./assets/SelectSignal";
+import SelectSignal from "./assets/Selector/SelectSignal";
 
 function ComponentTests() {
   const [blueprint, setBlueprint] = useState("");

+ 1 - 14
src/assets/ClockTimeline.module.css → src/assets/ClockTimeline/ClockTimeline.module.css

@@ -224,6 +224,7 @@
   font-size: 10px;
   cursor: pointer;
 }
+
 .signalRemove:hover {
   color: #d9614f;
 }
@@ -236,17 +237,3 @@
   padding: 3px 6px;
   font-size: 11px;
 }
-.autoFillBtn {
-  background-color: #5eb663;
-  border: none;
-  color: #000;
-  font-weight: 600;
-  border-radius: 4px;
-  padding: 4px 8px;
-  font-size: 11px;
-  cursor: pointer;
-  white-space: nowrap;
-}
-.autoFillBtn:hover {
-  background-color: #92e897;
-}

+ 9 - 25
src/assets/ClockTimeline.tsx → src/assets/ClockTimeline/ClockTimeline.tsx

@@ -1,9 +1,9 @@
 import { useMemo } from "react";
-import Icon from "./icon";
 import styles from "./ClockTimeline.module.css";
-import { ACTION_PRESETS } from "./types";
-import TimelineRow from "./components/TimelineRow";
-import { useClockStore } from "../store/useClockStore";
+import { useClockStore } from "../../store/useClockStore";
+import { ACTION_PRESETS } from "../types";
+import Icon from "../icon";
+import TimelineRow from "./TimelineRow";
 
 type Props = {};
 
@@ -44,31 +44,18 @@ export default function ClockTimeline({}: Props) {
     <div className={styles.wrap}>
       <div className={styles.palette}>
         {ACTION_PRESETS.map((p) => (
-          <div
-            key={p.id}
-            className={styles.chip}
-            draggable
-            onDragStart={(e) => onPaletteDragStart(e, p.id)}
-          >
-            {p.fromItem && (
-              <Icon iconName={`item/${p.fromItem}.png`} size={20} />
-            )}
+          <div key={p.id} className={styles.chip} draggable onDragStart={(e) => onPaletteDragStart(e, p.id)}>
+            {p.fromItem && <Icon iconName={`item/${p.fromItem}.png`} size={20} />}
             <span>{p.label}</span>
             <span className={styles.chipTicks}>{p.ticks}t</span>
           </div>
         ))}
-        <span className={styles.hint}>
-          Drag onto a row · Shift-click to multi-select · Ctrl+D to duplicate
-        </span>
+        <span className={styles.hint}>Drag onto a row · Shift-click to multi-select · Ctrl+D to duplicate</span>
       </div>
 
       <div className={styles.ruler}>
         {majorTicks.map((t) => (
-          <div
-            key={t}
-            className={styles.rulerTick}
-            style={{ left: `${(t / duration) * 100}%` }}
-          >
+          <div key={t} className={styles.rulerTick} style={{ left: `${(t / duration) * 100}%` }}>
             <span>{t}</span>
           </div>
         ))}
@@ -77,10 +64,7 @@ export default function ClockTimeline({}: Props) {
 
       <div className={styles.rows}>
         {alignmentTick !== null && (
-          <div
-            className={styles.alignGuide}
-            style={{ left: `${(alignmentTick / duration) * 100}%` }}
-          />
+          <div className={styles.alignGuide} style={{ left: `${(alignmentTick / duration) * 100}%` }} />
         )}
 
         {rowOrder.map((rowId) => (

+ 23 - 71
src/assets/components/TimelineRow.tsx → src/assets/ClockTimeline/TimelineRow.tsx

@@ -1,16 +1,11 @@
 import { useRef } from "react";
 import { useClockStore } from "../../store/useClockStore";
 import { useTimelineDrag } from "../../hooks/useTimelineDrag";
-import SelectSignal, { type Signal } from "../../assets/SelectSignal";
-import ExpressionInput from "../../assets/components/ExpressionInpux";
-import Icon from "../../assets/icon";
-import styles from "../ClockTimeline.module.css";
-import {
-  ACTION_PRESETS,
-  expandBlockInstances,
-  getPreset,
-  type ClockBlock,
-} from "../../assets/types";
+import SelectSignal from "../Selector/SelectSignal";
+import ExpressionInput from "../components/ExpressionInpux";
+import Icon from "../icon";
+import styles from "./ClockTimeline.module.css";
+import { ACTION_PRESETS, expandBlockInstances, getPreset, type ClockBlock, type Signal } from "../types";
 
 type PackedInstance = {
   id: string;
@@ -23,9 +18,7 @@ type PackedInstance = {
 let uid = 0;
 const nextId = (prefix: string) => `${prefix}-${++uid}`;
 
-function packLanes(
-  instances: { id: string; blockId: string; start: number; duration: number }[],
-) {
+function packLanes(instances: { id: string; blockId: string; start: number; duration: number }[]) {
   const sorted = [...instances].sort((a, b) => a.start - b.start);
   const laneEnds: number[] = [];
   const packed: PackedInstance[] = [];
@@ -44,19 +37,9 @@ function packLanes(
 const LANE_HEIGHT = 30;
 const GAP = 4;
 export default function TimelineRow({ rowId }: { rowId: string }) {
-  const {
-    updateRow,
-    removeRow,
-    addBlocks,
-    selectBlocks,
-    blocks,
-    rows,
-    duration,
-    selectedBlockIds,
-    alignmentTick,
-  } = useClockStore();
-  const { onPointerDownBlock, onPointerMove, onPointerUp, pxToTick } =
-    useTimelineDrag();
+  const { updateRow, removeRow, addBlocks, selectBlocks, blocks, rows, duration, selectedBlockIds, alignmentTick } =
+    useClockStore();
+  const { onPointerDownBlock, onPointerMove, onPointerUp, pxToTick } = useTimelineDrag();
 
   const laneRef = useRef<HTMLDivElement>(null);
   const row = rows[rowId];
@@ -75,10 +58,7 @@ export default function TimelineRow({ rowId }: { rowId: string }) {
     if (!preset || !laneRef.current) return;
 
     const rect = laneRef.current.getBoundingClientRect();
-    const tick = Math.max(
-      0,
-      Math.min(duration - 1, pxToTick(rect.width, e.clientX - rect.left)),
-    );
+    const tick = Math.max(0, Math.min(duration - 1, pxToTick(rect.width, e.clientX - rect.left)));
 
     const newBlock = {
       id: `block-${Date.now()}`,
@@ -134,44 +114,27 @@ export default function TimelineRow({ rowId }: { rowId: string }) {
         <div className={styles.signalList}>
           {row.signals.map((s, i) => (
             <div key={`${s.name}-${i}`} className={styles.signalChip}>
-              <SelectSignal
-                value={s.name}
-                onSelectSignal={(_, sig) => updateRowSignal(i, sig)}
-              />
+              <SelectSignal value={s.name} onSelectSignal={(_, sig) => updateRowSignal(i, sig)} />
               {row.signals.length > 1 && (
-                <button
-                  className={styles.signalRemove}
-                  onClick={() => removeRowSignal(i)}
-                >
+                <button className={styles.signalRemove} onClick={() => removeRowSignal(i)}>
                 </button>
               )}
             </div>
           ))}
           <SelectSignal
+            className={styles.signalEmpty}
             key={row.signals.length}
             onSelectSignal={(_, sig) => addRowSignal(sig)}
           />
         </div>
         <div className={styles.rowStats}>
           <label>Stack</label>
-          <ExpressionInput
-            value={row.stackSize}
-            min={1}
-            onCommit={(v) => updateRow(rowId, { stackSize: v })}
-          />
+          <ExpressionInput value={row.stackSize} min={1} onCommit={(v) => updateRow(rowId, { stackSize: v })} />
           <label>Inserters</label>
-          <ExpressionInput
-            value={row.inserterCount}
-            min={1}
-            onCommit={(v) => updateRow(rowId, { inserterCount: v })}
-          />
+          <ExpressionInput value={row.inserterCount} min={1} onCommit={(v) => updateRow(rowId, { inserterCount: v })} />
         </div>
-        <select
-          className={styles.autoFillPreset}
-          defaultValue="chest_to_belt"
-          id={`autofill-preset-${row.id}`}
-        >
+        <select className={styles.autoFillPreset} defaultValue="chest_to_belt" id={`autofill-preset-${row.id}`}>
           {ACTION_PRESETS.filter((p) => p.id !== "custom").map((p) => (
             <option key={p.id} value={p.id}>
               {p.label}
@@ -179,11 +142,9 @@ export default function TimelineRow({ rowId }: { rowId: string }) {
           ))}
         </select>
         <button
-          className={styles.autoFillBtn}
+          className="button-green"
           onClick={() => {
-            const select = document.getElementById(
-              `autofill-preset-${row.id}`,
-            ) as HTMLSelectElement | null;
+            const select = document.getElementById(`autofill-preset-${row.id}`) as HTMLSelectElement | null;
             autoFillRow(select?.value ?? "chest_to_belt");
           }}
         >
@@ -207,8 +168,7 @@ export default function TimelineRow({ rowId }: { rowId: string }) {
           const block = blocks[inst.blockId];
           const preset = getPreset(block.presetId);
           const selected = selectedBlockIds.has(block.id);
-          const aligned =
-            alignmentTick !== null && inst.start === alignmentTick;
+          const aligned = alignmentTick !== null && inst.start === alignmentTick;
 
           return (
             <div
@@ -220,23 +180,15 @@ export default function TimelineRow({ rowId }: { rowId: string }) {
                 top: inst.lane * 34 + 4,
                 height: 30,
               }}
-              onPointerDown={(e) =>
-                onPointerDownBlock(e, block, "move", laneRef.current)
-              }
+              onPointerDown={(e) => onPointerDownBlock(e, block, "move", laneRef.current)}
             >
               <div className={styles.blockIcons}>
-                {preset.fromItem && (
-                  <Icon iconName={`item/${preset.fromItem}.png`} size={18} />
-                )}
-                {preset.toItem && (
-                  <Icon iconName={`item/${preset.toItem}.png`} size={18} />
-                )}
+                {preset.fromItem && <Icon iconName={`item/${preset.fromItem}.png`} size={18} />}
+                {preset.toItem && <Icon iconName={`item/${preset.toItem}.png`} size={18} />}
               </div>
               <div
                 className={styles.resizeHandle}
-                onPointerDown={(e) =>
-                  onPointerDownBlock(e, block, "resize", laneRef.current)
-                }
+                onPointerDown={(e) => onPointerDownBlock(e, block, "resize", laneRef.current)}
               />
             </div>
           );

+ 3 - 0
src/assets/ClockTimeline/index.ts

@@ -0,0 +1,3 @@
+import ClockTimeline from "./ClockTimeline";
+
+export default ClockTimeline;

+ 0 - 0
src/assets/components/SelectedBlockPanel.module.css → src/assets/SelectedBlockPanel.module.css


+ 80 - 0
src/assets/SelectedBlockPanel.tsx

@@ -0,0 +1,80 @@
+import styles from "./SelectedBlockPanel.module.css";
+import { ACTION_PRESETS, getPreset } from "./types";
+import Icon from "./icon";
+import ExpressionInput from "./components/ExpressionInpux";
+import { useClockStore } from "../store/useClockStore";
+import { useMemo } from "react";
+
+export default function SelectedBlockPanel() {
+  const { selectedBlockIds, removeBlocks, blocks, updateBlock } = useClockStore();
+
+  const blockId = useMemo(() => selectedBlockIds.keys().next().value, [selectedBlockIds]);
+  const block = useMemo(() => {
+    if (blockId) return blocks[blockId];
+  }, [blockId]);
+
+  if (selectedBlockIds.size > 1) {
+    return (
+      <div className={styles.wrap}>
+        <span className={styles.multiLabel}>{selectedBlockIds.size} blocks selected</span>
+        <button className={styles.removeBtn} onClick={() => removeBlocks(Array.from(selectedBlockIds))}>
+          Remove all
+        </button>
+      </div>
+    );
+  }
+
+  if (!blockId || !block) {
+    return <div className={styles.empty}>Select a block on the timeline to edit it.</div>;
+  }
+  const preset = getPreset(block.presetId);
+
+  return (
+    <div className={styles.wrap}>
+      <div className={styles.preview}>
+        {preset.fromItem && <Icon iconName={`item/${preset.fromItem}.png`} size={40} />}
+        <span className={styles.arrow}>→</span>
+        {preset.toItem && <Icon iconName={`item/${preset.toItem}.png`} size={40} />}
+      </div>
+
+      <div className={styles.fields}>
+        <div className={styles.field}>
+          <label>Action</label>
+          <select
+            value={block.presetId}
+            onChange={(e) => {
+              const p = getPreset(e.target.value);
+              updateBlock(blockId, { presetId: p.id, duration: p.ticks + 1 });
+            }}
+          >
+            {ACTION_PRESETS.map((p) => (
+              <option key={p.id} value={p.id}>
+                {p.label}
+              </option>
+            ))}
+          </select>
+        </div>
+        <div className={styles.field}>
+          <label>Start (tick)</label>
+          <ExpressionInput value={block.start} min={0} onCommit={(v) => updateBlock(blockId, { start: v })} />
+        </div>
+        <div className={styles.field}>
+          <label>Duration (ticks)</label>
+          <ExpressionInput value={block.duration} min={1} onCommit={(v) => updateBlock(blockId, { duration: v })} />
+        </div>
+        <div className={styles.field}>
+          <label>Count</label>
+          <ExpressionInput value={block.count} min={1} onCommit={(v) => updateBlock(blockId, { count: v })} />
+        </div>
+        <div className={styles.field}>
+          <label>Repeat ×</label>
+          <ExpressionInput value={block.repeat ?? 1} min={1} onCommit={(v) => updateBlock(blockId, { repeat: v })} />
+        </div>
+      </div>
+
+      <button className={styles.removeBtn} onClick={() => removeBlocks([blockId])}>
+        Remove block
+      </button>
+    </div>
+  );
+}

+ 0 - 0
src/assets/MachineSelector.module.css → src/assets/Selector/MachineSelector.module.css


+ 6 - 6
src/assets/MachineSelector.tsx → src/assets/Selector/MachineSelector.tsx

@@ -1,13 +1,13 @@
 import React, { useCallback, useEffect, useMemo, useState, type CSSProperties } from "react";
-import data from "../assets/data/2.0/data.json";
+import data from "../../assets/data/2.0/data.json";
 import styles from "./MachineSelector.module.css";
-import Icon from "./icon";
+import Icon from "../icon";
 import { Autocomplete, Box, Popper, TextField, InputAdornment } from "@mui/material";
 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 Hook
+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";
 
 const recipeList = data.recipeGroup.flatMap((r) => r.subGroup.flatMap((s) => (s.children ?? []) as Recipe[]));
 const machines = data.machines as Machine[];

+ 0 - 0
src/assets/SelectFactorioMenu.module.css → src/assets/Selector/SelectFactorioMenu.module.css


+ 15 - 55
src/assets/SelectFactorioMenu.tsx → src/assets/Selector/SelectFactorioMenu.tsx

@@ -1,11 +1,11 @@
 import { useState, type CSSProperties, useMemo } from "react";
-import Icon from "./icon";
+import Icon from "../icon";
 import styles from "./SelectFactorioMenu.module.css";
-import Tooltip from "./Tooltip";
+import Tooltip from "../Tooltip";
 import SimpleBar from "simplebar-react";
 
-import data from "../assets/data/2.0/data.json";
-import { useQualityScroller } from "../hooks/useQualityScroller";
+import data from "../../assets/data/2.0/data.json";
+import { useQualityScroller } from "../../hooks/useQualityScroller";
 
 const qualityLevels = data.qualityLevels;
 // Utility type definitions
@@ -45,19 +45,12 @@ const ITEM_PER_ROW = 10;
  * @param {string} search - User’s search query (space separated).
  * @returns {Category<MenuItem>} - A filtered copy of the category.
  */
-function filterCategory(
-  category: Category<MenuItem>,
-  search: string,
-): Category<MenuItem> {
+function filterCategory(category: Category<MenuItem>, search: string): Category<MenuItem> {
   const searchKeys = search.toLowerCase().split(" ");
 
   // Filter the children inside a single sub‑group
-  function filterSubgroupChildren(
-    subgroup: SubGroup<MenuItem>,
-  ): SubGroup<MenuItem> {
-    const children = subgroup.children.filter((item) =>
-      searchKeys.every((k) => item.name.includes(k)),
-    );
+  function filterSubgroupChildren(subgroup: SubGroup<MenuItem>): SubGroup<MenuItem> {
+    const children = subgroup.children.filter((item) => searchKeys.every((k) => item.name.includes(k)));
     return {
       ...subgroup,
       children,
@@ -65,9 +58,7 @@ function filterCategory(
   }
   return {
     ...category,
-    subGroup: category.subGroup
-      .map(filterSubgroupChildren)
-      .filter((s) => s.children.length > 0),
+    subGroup: category.subGroup.map(filterSubgroupChildren).filter((s) => s.children.length > 0),
   };
 }
 
@@ -130,38 +121,21 @@ type SelectMenuProps = {
  * @component
  * @param {SelectMenuProps} props
  */
-function SelectMenu({
-  style,
-  className,
-  title,
-  categories,
-  onClose,
-  onSelectItem,
-}: SelectMenuProps) {
+function SelectMenu({ style, className, title, categories, onClose, onSelectItem }: SelectMenuProps) {
   const [category, setCategory] = useState(categories[0].name);
   const [item, selectItem] = useState("");
   const [showSearch, setShowSearch] = useState(false);
   const [search, setSearch] = useState("");
-  const { scrollRef, activeQuality, setQualityName } = useQualityScroller(
-    "normal",
-    undefined,
-    "factorio-menu-quality",
-  );
+  const { scrollRef, activeQuality, setQualityName } = useQualityScroller("normal", undefined, "factorio-menu-quality");
 
-  const activeCategory = useMemo(
-    () => categories.find((r) => r.name == category),
-    [category, categories],
-  );
+  const activeCategory = useMemo(() => categories.find((r) => r.name == category), [category, categories]);
   const handleItemSelect = (name: string) => {
     if (onSelectItem) onSelectItem(name, activeQuality.level);
     else selectItem(name);
   };
   // Apply filtering only if a search is active
   const filteredContent = useMemo(
-    () =>
-      search && activeCategory && showSearch
-        ? filterCategory(activeCategory, search)
-        : activeCategory,
+    () => (search && activeCategory && showSearch ? filterCategory(activeCategory, search) : activeCategory),
     [activeCategory, search, showSearch],
   );
   const rootClassName = `${styles.selectMenu} ${className ?? ""}`;
@@ -172,17 +146,8 @@ function SelectMenu({
         <div className={styles.selectMenuHeaderTitle}>{title ?? "Title"}</div>
         <div className={styles.selectMenuHeaderSpacer} />
         <div className={styles.selectMenuHeaderAction}>
-          {showSearch && (
-            <input
-              type="search"
-              onChange={(evt) => setSearch(evt.target.value)}
-              value={search}
-            />
-          )}
-          <button
-            className={`panel-button ${showSearch ? "active" : ""}`}
-            onClick={() => setShowSearch(!showSearch)}
-          >
+          {showSearch && <input type="search" onChange={(evt) => setSearch(evt.target.value)} value={search} />}
+          <button className={`panel-button ${showSearch ? "active" : ""}`} onClick={() => setShowSearch(!showSearch)}>
             <img src="/assets/search.png" alt="Search" />
           </button>
           <button className="panel-button" onClick={onClose}>
@@ -208,12 +173,7 @@ function SelectMenu({
       <div className={styles.selectMenuContent}>
         <SimpleBar style={{ maxHeight: 300 }}>
           {filteredContent?.subGroup.map((subgroup) => (
-            <SubGroupRow
-              key={subgroup.name}
-              subgroup={subgroup}
-              selectedItem={item}
-              onSelectItem={handleItemSelect}
-            />
+            <SubGroupRow key={subgroup.name} subgroup={subgroup} selectedItem={item} onSelectItem={handleItemSelect} />
           ))}
         </SimpleBar>
       </div>

+ 1 - 0
src/assets/SelectSignal.module.css → src/assets/Selector/SelectSignal.module.css

@@ -47,4 +47,5 @@
 .placeholder {
   color: #2a2a2a;
   white-space: nowrap;
+  padding: 0px 10px;
 }

+ 7 - 14
src/assets/SelectSignal.tsx → src/assets/Selector/SelectSignal.tsx

@@ -1,30 +1,23 @@
 import React, { useCallback, useState, type CSSProperties } from "react";
-import data from "../assets/data/2.0/data.json";
+import data from "../../assets/data/2.0/data.json";
 import styles from "./SelectSignal.module.css";
-import Icon from "./icon";
+import Icon from "../icon";
 import { Popper } from "@mui/material";
 import SelectMenu from "./SelectFactorioMenu";
-import Tooltip from "./Tooltip";
+import Tooltip from "../Tooltip";
+import type { Signal } from "../types";
 
 const signals = data.signalGroup.flatMap((r) => r.subGroup.flatMap((s) => (s.children ?? []) as Signal[]));
 
-export type Signal = {
-  name: string;
-  type: string;
-  subgroup: string;
-  icon: string;
-  order: string;
-  quality?: string;
-};
-
 type SelectSignalProps = {
   className?: string;
   style?: CSSProperties;
+  placeholder?: string;
   value?: string;
   onSelectSignal?: (itemName: string, signal: Signal | null) => void;
 };
 
-function SelectSignal({ style, className, value, onSelectSignal }: SelectSignalProps) {
+function SelectSignal({ style, className, value, placeholder, onSelectSignal }: SelectSignalProps) {
   const [internalSignal, setInternalSignal] = useState<null | Signal>(null);
   const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
   const [showMenu, setShowMenu] = useState(false);
@@ -62,7 +55,7 @@ function SelectSignal({ style, className, value, onSelectSignal }: SelectSignalP
       </button>
       <Popper open={showMenu} anchorEl={anchorEl} placement="right">
         <SelectMenu
-          title="Select signal"
+          title={placeholder ?? "Select signal"}
           categories={data.signalGroup}
           onSelectItem={onSelectRecipe}
           onClose={() => setShowMenu(false)}

+ 51 - 49
src/assets/Simulator.tsx

@@ -1,68 +1,70 @@
 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 FactorioSimulationDashboard from "../engine/Dashboard/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 copperCableRecipe: Recipe = {
+  name: "copper-cable",
+  icon: "item/copper-cable.png",
+  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 greenCircuitRecipe: Recipe = {
+  name: "electronic-circuit",
+  icon: "item/electronic-circuit.png",
+  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 assemblerSetup: MachineSetup = {
+  machine: { name: "assembling-machine-2", icon: "item/assembling-machine-2.png", 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 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 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();
+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");
+// 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");
+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");
+// 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 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];
+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));
+allInserters.forEach((ins) => orchestrator.registerInserter(ins));
+[cop1, cop2, cop3, circ1, circ2].forEach((m) => orchestrator.registerMachine(m));
 
-export function Simulator(){
-  return <FactorioSimulationDashboard orchestrator={orchestrator}></FactorioSimulationDashboard> 
-}
+export function Simulator() {
+  return <FactorioSimulationDashboard orchestrator={orchestrator}></FactorioSimulationDashboard>;
+}

+ 1 - 1
src/assets/components/ClockWizard.tsx

@@ -5,7 +5,7 @@ import { useClockStore } from "../../store/useClockStore";
 import type { Recipe } from "../../../scripts/factorio-dump/helpers/recipes.helper";
 import type { Machine, Module } from "../../../scripts/factorio-dump/process-data.models";
 
-import MachineSelector from "../MachineSelector";
+import MachineSelector from "../Selector/MachineSelector";
 import BeaconConfigurator, { type BeaconGroup } from "./BeaconConfigurator";
 import ExpressionInput from "./ExpressionInpux";
 import InputConfigurator from "./InputConfigurator";

+ 57 - 28
src/assets/components/ModuleSlots.tsx

@@ -1,28 +1,32 @@
-import React, { useState, useEffect, useMemo } from "react";
+import { useState, useEffect, useMemo } from "react";
 import { Popper } from "@mui/material";
 import styles from "./ModuleSlots.module.css";
 import data from "../../assets/data/2.0/data.json";
 import type { Module } from "../../../scripts/factorio-dump/process-data.models";
-import type { Category, MenuItem, SubGroup } from "../SelectFactorioMenu";
+import type { Category, MenuItem, SubGroup } from "../Selector/SelectFactorioMenu";
 import { orderedQualities, useQualityScroller } from "../../hooks/useQualityScroller";
 import Icon from "../icon";
-import SelectMenu from "../SelectFactorioMenu";
+import SelectMenu from "../Selector/SelectFactorioMenu";
 
 // Build category structure
-const prod = data.signalGroup.find(g => g.name === "production") as Category<MenuItem>;
-const moduleSubGroup = prod?.subGroup.find(s => s.name === "module") as SubGroup<MenuItem>;
+const prod = data.signalGroup.find((g) => g.name === "production") as Category<MenuItem>;
+const moduleSubGroup = prod?.subGroup.find((s) => s.name === "module") as SubGroup<MenuItem>;
 
 type ModuleSlotsProps = {
   maxSlots: number;
   allowedEffects?: string[];
-  onChange: (modules: { module: Module, qualityLevel: number }[]) => void;
+  onChange: (modules: { module: Module; qualityLevel: number }[]) => void;
 };
 
 function OccupiedSlot({ module, initialQualityLevel, onChangeQuality }: any) {
-  const initialQualityName = orderedQualities.find(q => q.level === initialQualityLevel)?.name || "normal";
-  const { scrollRef, activeQuality } = useQualityScroller(initialQualityName, (newQuality) => {
-    onChangeQuality(newQuality.level);
-  }, "factorio-module-quality"); // Persist module quality separately!
+  const initialQualityName = orderedQualities.find((q) => q.level === initialQualityLevel)?.name || "normal";
+  const { scrollRef, activeQuality } = useQualityScroller(
+    initialQualityName,
+    (newQuality) => {
+      onChangeQuality(newQuality.level);
+    },
+    "factorio-module-quality",
+  ); // Persist module quality separately!
 
   return (
     <div ref={scrollRef} className={styles.occupiedSlot}>
@@ -32,12 +36,12 @@ function OccupiedSlot({ module, initialQualityLevel, onChangeQuality }: any) {
 }
 
 export default function ModuleSlots({ maxSlots, allowedEffects, onChange }: ModuleSlotsProps) {
-  const [slots, setSlots] = useState<Array<{ module: Module, qualityLevel: number } | null>>([]);
+  const [slots, setSlots] = useState<Array<{ module: Module; qualityLevel: number } | null>>([]);
   const [activeSlot, setActiveSlot] = useState<number | null>(null);
   const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
 
   useEffect(() => {
-    setSlots(prev => {
+    setSlots((prev) => {
       const newSlots = Array(maxSlots).fill(null);
       for (let i = 0; i < Math.min(prev.length, maxSlots); i++) newSlots[i] = prev[i];
       return newSlots;
@@ -47,9 +51,9 @@ export default function ModuleSlots({ maxSlots, allowedEffects, onChange }: Modu
   const moduleCategories = useMemo(() => {
     let validModules = data.modules as Module[];
     if (allowedEffects && allowedEffects.length > 0) {
-      validModules = validModules.filter(m => {
+      validModules = validModules.filter((m) => {
         if (!m.effect) return true;
-        return Object.keys(m.effect).every(e => {
+        return Object.keys(m.effect).every((e) => {
           const effectValue = m.effect![e as keyof typeof m.effect] ?? 0;
           return effectValue <= 0 || allowedEffects.includes(e);
         });
@@ -66,13 +70,13 @@ export default function ModuleSlots({ maxSlots, allowedEffects, onChange }: Modu
     newSlots[activeSlot] = { module: selectedModule, qualityLevel };
     setSlots(newSlots);
     setActiveSlot(null);
-    onChange(newSlots.filter(s => s !== null) as any);
+    onChange(newSlots.filter((s) => s !== null) as any);
   };
 
   const handleFillAll = () => {
-    const template = slots.find(s => s !== null);
+    const template = slots.find((s) => s !== null);
     if (!template) return;
-    const newSlots = slots.map(s => s || template);
+    const newSlots = slots.map((s) => s || template);
     setSlots(newSlots);
     onChange(newSlots as any);
   };
@@ -83,28 +87,53 @@ export default function ModuleSlots({ maxSlots, allowedEffects, onChange }: Modu
     <div style={{ display: "flex", gap: "12px", alignItems: "center" }}>
       <div className={styles.slotsContainer}>
         {slots.map((slot, i) => (
-          <div 
-            key={i} 
-            className={styles.slot} 
-            onClick={(e) => { setAnchorEl(e.currentTarget); setActiveSlot(i); }}
-            onContextMenu={(e) => { e.preventDefault(); const n = [...slots]; n[i] = null; setSlots(n); onChange(n.filter(s=>s) as any); }}
+          <div
+            key={i}
+            className={styles.slot}
+            onClick={(e) => {
+              setAnchorEl(e.currentTarget);
+              setActiveSlot(i);
+            }}
+            onContextMenu={(e) => {
+              e.preventDefault();
+              const n = [...slots];
+              n[i] = null;
+              setSlots(n);
+              onChange(n.filter((s) => s) as any);
+            }}
           >
             {slot ? (
-              <OccupiedSlot module={slot.module} initialQualityLevel={slot.qualityLevel} onChangeQuality={(lvl: number) => { const n = [...slots]; n[i]!.qualityLevel = lvl; setSlots(n); onChange(n.filter(s=>s) as any); }} />
-            ) : <span className={styles.emptySlot}>+</span>}
+              <OccupiedSlot
+                module={slot.module}
+                initialQualityLevel={slot.qualityLevel}
+                onChangeQuality={(lvl: number) => {
+                  const n = [...slots];
+                  n[i]!.qualityLevel = lvl;
+                  setSlots(n);
+                  onChange(n.filter((s) => s) as any);
+                }}
+              />
+            ) : (
+              <span className={styles.emptySlot}>+</span>
+            )}
           </div>
         ))}
       </div>
-      
-      {slots.some(s => s !== null) && slots.some(s => s === null) && (
+
+      {slots.some((s) => s !== null) && slots.some((s) => s === null) && (
         <button onClick={handleFillAll} className={styles.fillBtn} title="Fill empty slots with the first module">
           Fill All
         </button>
       )}
 
       <Popper open={activeSlot !== null} anchorEl={anchorEl} placement="bottom-start" style={{ zIndex: 1300 }}>
-         <SelectMenu title="Select Module" categories={moduleCategories} onSelectItem={handleSelectModule} onClose={() => setActiveSlot(null)} />
+        <SelectMenu
+          title="Select Module"
+          categories={moduleCategories}
+          onSelectItem={handleSelectModule}
+          onClose={() => setActiveSlot(null)}
+        />
       </Popper>
     </div>
   );
-}
+}

+ 0 - 117
src/assets/components/SelectedBlockPanel.tsx

@@ -1,117 +0,0 @@
-import styles from "./SelectedBlockPanel.module.css";
-import { ACTION_PRESETS, getPreset } from "../types";
-import Icon from "../icon";
-import ExpressionInput from "./ExpressionInpux";
-import { useClockStore } from "../../store/useClockStore";
-import { useMemo } from "react";
-
-export default function SelectedBlockPanel() {
-  const { selectedBlockIds, removeBlocks, blocks, updateBlock } =
-    useClockStore();
-
-  const blockId = useMemo(
-    () => selectedBlockIds.keys().next().value,
-    [selectedBlockIds],
-  );
-  const block = useMemo(() => {
-    if (blockId) return blocks[blockId];
-  }, [blockId]);
-
-  if (selectedBlockIds.size > 1) {
-    return (
-      <div className={styles.wrap}>
-        <span className={styles.multiLabel}>
-          {selectedBlockIds.size} blocks selected
-        </span>
-        <button
-          className={styles.removeBtn}
-          onClick={() => removeBlocks(Array.from(selectedBlockIds))}
-        >
-          Remove all
-        </button>
-      </div>
-    );
-  }
-
-  if (!blockId || !block) {
-    return (
-      <div className={styles.empty}>
-        Select a block on the timeline to edit it.
-      </div>
-    );
-  }  
-  const preset = getPreset(block.presetId)
-
-
-  return (
-    <div className={styles.wrap}>
-      <div className={styles.preview}>
-        {preset.fromItem && (
-          <Icon iconName={`item/${preset.fromItem}.png`} size={40} />
-        )}
-        <span className={styles.arrow}>→</span>
-        {preset.toItem && (
-          <Icon iconName={`item/${preset.fromItem}.png`} size={40} />
-        )}
-      </div>
-
-      <div className={styles.fields}>
-        <div className={styles.field}>
-          <label>Action</label>
-          <select
-            value={block.presetId}
-            onChange={(e) => {
-              const p = getPreset(e.target.value);
-              updateBlock(blockId, { presetId: p.id, duration: p.ticks + 1 });
-            }}
-          >
-            {ACTION_PRESETS.map((p) => (
-              <option key={p.id} value={p.id}>
-                {p.label}
-              </option>
-            ))}
-          </select>
-        </div>
-        <div className={styles.field}>
-          <label>Start (tick)</label>
-          <ExpressionInput
-            value={block.start}
-            min={0}
-            onCommit={(v) => updateBlock(blockId, { start: v })}
-          />
-        </div>
-        <div className={styles.field}>
-          <label>Duration (ticks)</label>
-          <ExpressionInput
-            value={block.duration}
-            min={1}
-            onCommit={(v) => updateBlock(blockId, { duration: v })}
-          />
-        </div>
-        <div className={styles.field}>
-          <label>Count</label>
-          <ExpressionInput
-            value={block.count}
-            min={1}
-            onCommit={(v) => updateBlock(blockId, { count: v })}
-          />
-        </div>
-        <div className={styles.field}>
-          <label>Repeat ×</label>
-          <ExpressionInput
-            value={block.repeat ?? 1}
-            min={1}
-            onCommit={(v) => updateBlock(blockId, { repeat: v })}
-          />
-        </div>
-      </div>
-
-      <button
-        className={styles.removeBtn}
-        onClick={() => removeBlocks([blockId])}
-      >
-        Remove block
-      </button>
-    </div>
-  );
-}

+ 9 - 2
src/assets/types.ts

@@ -1,7 +1,5 @@
 // Shared types for the combinator clock timeline editor.
 
-import type { Signal } from "./SelectSignal";
-
 export type ActionPreset = {
   id: string;
   label: string;
@@ -22,6 +20,15 @@ export function getPreset(id: string): ActionPreset {
   return ACTION_PRESETS.find((p) => p.id === id) ?? ACTION_PRESETS[ACTION_PRESETS.length - 1];
 }
 
+export type Signal = {
+  name: string;
+  type: string;
+  subgroup: string;
+  icon: string;
+  order: string;
+  quality?: string;
+};
+
 /** A single activation Clock, placed freely on the timeline. */
 export type ClockBlock = {
   id: string;

+ 1 - 1
src/blueprint/Blueprintbuilder.ts

@@ -1,4 +1,3 @@
-import type { Signal } from "../assets/SelectSignal";
 import {
   expandBlockInstances,
   getPreset,
@@ -7,6 +6,7 @@ import {
   type ClockRow,
   type DeciderCombinator,
   type DeciderCondition,
+  type Signal,
 } from "../assets/types";
 
 const BP_VERSION = 562954249109505; // version stamp reused from a real 2.0 export

+ 0 - 77
src/engine/ClassicView.tsx

@@ -1,77 +0,0 @@
-
-import styles from './SimulationDashboard.module.css';
-import Icon from '../assets/icon';
-import type { InserterData, MachineData } from './FactorioSimulationDashBoard';
-
-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: MachineData[], inserters: InserterData[] }) {
-  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>
-  );
-}

+ 131 - 0
src/engine/Dashboard/ClassicView.tsx

@@ -0,0 +1,131 @@
+import Icon from "../../assets/icon";
+import styles from "./Dashboard.module.css";
+
+import type { MachineData, InserterData } from "./types";
+
+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" },
+] as const;
+
+interface ClassicViewProps {
+  machines: MachineData[];
+  inserters: InserterData[];
+}
+
+export default function ClassicView({ machines, inserters }: ClassicViewProps) {
+  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}
+                    </div>
+                  ))}
+                </div>
+                <div style={{ display: "flex", gap: "16px" }}>
+                  <strong>OUT:</strong>
+                  {Object.entries(machine.outputBuffer).map(([k, v]) => {
+                    const limit = machine.outputBlockLimits[k] || 0;
+                    const isFull = v >= 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} / {limit}
+                        </strong>
+                      </div>
+                    );
+                  })}
+                </div>
+              </div>
+            </div>
+          ))}
+        </div>
+      </div>
+    </div>
+  );
+}

+ 378 - 0
src/engine/Dashboard/Dashboard.module.css

@@ -0,0 +1,378 @@
+/* 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;
+}
+
+.statusBadge {
+  width: 150px;
+  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;
+}
+
+.timelineScrollArea {
+  overflow-x: auto;
+  overflow-y: hidden;
+  padding-bottom: 16px;
+  background-color: #212121;
+  border: 1px solid #5f5f5f;
+  border-radius: 4px;
+}
+
+.timelineGroup {
+  margin-bottom: 24px;
+  border-bottom: 2px solid #1a1a1a;
+  padding: 16px;
+}
+
+.timelineRow {
+  display: flex;
+  align-items: center;
+  margin-bottom: 4px;
+}
+
+.timelineLabel {
+  width: 140px;
+  min-width: 140px;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  font-weight: bold;
+  color: #e3e3e3;
+}
+
+.timelineGraph {
+  flex-grow: 1;
+  border-left: 1px solid #5f5f5f;
+  border-right: 1px solid #5f5f5f;
+  background-color: #1a1a1a;
+  position: relative;
+  cursor: crosshair;
+}
+
+.timelineGraph:hover {
+  background-color: #2b2b2b;
+}
+
+.tooltip {
+  position: fixed;
+  pointer-events: none;
+  background: #1a1a1a;
+  border: 1px solid #e39827;
+  color: #fff;
+  padding: 6px 10px;
+  border-radius: 4px;
+  font-size: 12px;
+  z-index: 9999;
+  transform: translate(-50%, -130%);
+  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.5);
+  white-space: nowrap;
+  font-family: monospace;
+}

+ 129 - 0
src/engine/Dashboard/FactorioSimulationDashBoard.tsx

@@ -0,0 +1,129 @@
+import { useState, useEffect, useRef } from "react";
+import styles from "./Dashboard.module.css";
+import ClassicView from "./ClassicView";
+import GraphView from "./GraphView";
+import TimelineView from "./TimelineView";
+import type { UIOrchestrator, HistorySnapshot } from "./types";
+
+export default function FactorioSimulationDashboard({ orchestrator }: { orchestrator: UIOrchestrator }) {
+  const [viewMode, setViewMode] = useState<"classic" | "graph" | "timeline">("classic");
+  const [tick, setTick] = useState<number>(0);
+  const [isPlaying, setIsPlaying] = useState<boolean>(false);
+  const [tps, setTps] = useState<number>(60);
+  const [stepAmount, setStepAmount] = useState<number>(1);
+
+  const [history, setHistory] = useState<HistorySnapshot[]>([]);
+  const requestRef = useRef<number>(1);
+  const lastUpdateRef = useRef<number>(0);
+
+  const MAX_HISTORY = 3000; // Increased to 3000 to capture 1440-tick jumps!
+
+  const executeTick = (): void => {
+    orchestrator.tick();
+    const currentTick = orchestrator.currentTick;
+    setTick(currentTick);
+
+    setHistory((prev) => {
+      const snapshot: HistorySnapshot = {
+        tick: currentTick,
+        machines: orchestrator.getMachines().map((m) => ({
+          isCrafting: m.isCrafting,
+          inputs: { ...m.inputBuffer },
+          outputs: { ...m.outputBuffer },
+        })),
+        inserters: orchestrator.getInserters().map((ins) => ({
+          state: ins.state,
+          held: ins.heldItems,
+        })),
+      };
+
+      const next = [...prev, snapshot];
+      if (next.length > MAX_HISTORY) next.shift();
+      return next;
+    });
+  };
+
+  const advanceSimulation = (time: number): void => {
+    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, 10) || 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
+            tick={orchestrator.currentTick}
+            machines={orchestrator.getMachines()}
+            inserters={orchestrator.getInserters()}
+          />
+        )}
+      </div>
+    </div>
+  );
+}

+ 151 - 0
src/engine/Dashboard/GraphView.tsx

@@ -0,0 +1,151 @@
+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;
+}
+
+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 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;
+
+    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" },
+      });
+    });
+
+    return { initialNodes: nodes, initialEdges: edges };
+  }, [orchestrator]);
+
+  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]);
+
+  return (
+    <div className={styles.graphWrapper}>
+      <ReactFlow nodes={nodes} edges={edges} nodeTypes={nodeTypes} fitView>
+        <Background color="#5f5f5f" gap={16} />
+        <Controls />
+      </ReactFlow>
+    </div>
+  );
+}

+ 281 - 0
src/engine/Dashboard/TimelineView.tsx

@@ -0,0 +1,281 @@
+import React, { useState, useRef, useEffect } from "react";
+import styles from "./Dashboard.module.css";
+import type { HistorySnapshot, MachineData, InserterData } from "./types";
+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" },
+];
+
+const MAX_HISTORY = 3000;
+const TICK_WIDTH = 2; // FIXED width per tick prevents tooltip shifting
+
+interface TimelineViewProps {
+  tick: number;
+  machines: MachineData[];
+  inserters: InserterData[];
+}
+
+export default function TimelineView({ tick, machines, inserters }: TimelineViewProps) {
+  const [tooltip, setTooltip] = useState<{ x: number; y: number; text: string } | null>(null);
+
+  // --- SCROLL LOCK MECHANIC ---
+  const scrollRef = useRef<HTMLDivElement>(null);
+  const isAutoScrollEnabled = useRef<boolean>(true);
+
+  // Monitor manual scrolling. If the user scrolls away from the right edge, pause auto-scroll.
+  const handleScroll = () => {
+    if (!scrollRef.current) return;
+    const { scrollLeft, scrollWidth, clientWidth } = scrollRef.current;
+    // If we are within 20px of the right edge, lock onto it
+    isAutoScrollEnabled.current = scrollWidth - scrollLeft - clientWidth < 20;
+  };
+
+  // Jump to the right edge whenever a new tick arrives (if auto-scroll is enabled)
+  useEffect(() => {
+    if (isAutoScrollEnabled.current && scrollRef.current) {
+      scrollRef.current.scrollLeft = scrollRef.current.scrollWidth;
+    }
+  }, [tick]);
+
+  // --- ZERO ALLOCATION HISTORY ---
+  const historyRef = useRef<HistorySnapshot[]>([]);
+
+  const lastTick = historyRef.current[historyRef.current.length - 1]?.tick;
+  if (tick !== lastTick) {
+    historyRef.current.push({
+      tick,
+      machines: machines.map((m) => ({
+        isCrafting: m.isCrafting,
+        inputs: { ...m.inputBuffer },
+        outputs: { ...m.outputBuffer },
+      })),
+      inserters: inserters.map((ins) => ({
+        state: ins.state,
+        held: ins.heldItems,
+      })),
+    });
+    if (historyRef.current.length > MAX_HISTORY) historyRef.current.shift();
+  }
+
+  const history = historyRef.current;
+
+  // Graph width strictly respects the 2px per tick rule, with a baseline minimum
+  const graphWidth = Math.max(800, history.length * TICK_WIDTH);
+
+  // --- ACCURATE TOOLTIP MATH ---
+  const handleMouseMove = (e: React.MouseEvent<SVGSVGElement>, label: string, valueFn: (h: HistorySnapshot) => any) => {
+    const rect = e.currentTarget.getBoundingClientRect();
+    const xInsideSvg = e.clientX - rect.left;
+
+    // Because TICK_WIDTH is exactly 2, we can just divide X by 2 to get the exact array index!
+    const tickIndex = Math.floor(xInsideSvg / TICK_WIDTH);
+
+    if (tickIndex >= 0 && tickIndex < history.length) {
+      setTooltip({
+        x: e.clientX,
+        y: e.clientY,
+        text: `Tick: ${history[tickIndex].tick} | ${label}: ${valueFn(history[tickIndex])}`,
+      });
+    } else {
+      setTooltip(null);
+    }
+  };
+
+  return (
+    <div className={styles.panel}>
+      <div className={styles.panelTitle} style={{ marginBottom: "16px" }}>
+        Live Simulation Timeline
+      </div>
+
+      {tooltip && (
+        <div className={styles.tooltip} style={{ left: tooltip.x, top: tooltip.y }}>
+          {tooltip.text}
+        </div>
+      )}
+
+      <div
+        className={styles.timelineScrollArea}
+        ref={scrollRef}
+        onScroll={handleScroll}
+        onMouseLeave={() => setTooltip(null)}
+      >
+        {/* ========================================== */}
+        {/* MACHINES BLOCK */}
+        {/* ========================================== */}
+        {machines.map((machine, mIndex) => {
+          let machineStatusPath = "";
+          let craftStart = -1;
+          for (let i = 0; i < history.length; i++) {
+            if (history[i].machines[mIndex]?.isCrafting) {
+              if (craftStart === -1) craftStart = i;
+            } else if (craftStart !== -1) {
+              machineStatusPath += ` M ${craftStart * TICK_WIDTH} 7.5 h ${(i - craftStart) * TICK_WIDTH}`;
+              craftStart = -1;
+            }
+          }
+          if (craftStart !== -1) {
+            machineStatusPath += ` M ${craftStart * TICK_WIDTH} 7.5 h ${(history.length - craftStart) * TICK_WIDTH}`;
+          }
+
+          return (
+            <div key={`m-${mIndex}`} className={styles.timelineGroup}>
+              {/* 1. INPUT ROWS */}
+              {Object.keys(machine.inputBuffer).map((itemKey) => {
+                const maxIn = Math.max(1, ...history.map((h) => h.machines[mIndex]?.inputs[itemKey] || 0));
+                const points = history
+                  .map((h, i) => `${i * TICK_WIDTH},${30 - ((h.machines[mIndex]?.inputs[itemKey] || 0) / maxIn) * 30}`)
+                  .join(" ");
+
+                return (
+                  <div key={`in-${itemKey}`} className={styles.timelineRow}>
+                    <div className={styles.timelineLabel} style={{ color: "#38bdf8" }}>
+                      <span>
+                        <Icon iconName={`item/${itemKey}.png`} size={24} />
+                      </span>
+                      <span>(in)</span>
+                    </div>
+                    <div className={styles.timelineGraph} style={{ height: "30px", width: graphWidth }}>
+                      <svg
+                        width="100%"
+                        height="100%"
+                        onMouseMove={(e) =>
+                          handleMouseMove(e, `Input ${itemKey}`, (h) => h.machines[mIndex]?.inputs[itemKey] || 0)
+                        }
+                      >
+                        <polyline points={points} fill="none" stroke="#38bdf8" strokeWidth="2" />
+                      </svg>
+                    </div>
+                  </div>
+                );
+              })}
+
+              {/* 2. STATUS ROW */}
+              <div className={styles.timelineRow}>
+                <div className={styles.timelineLabel} style={{ color: "#5eb663" }}>
+                  <Icon iconName={machine.setup.machine?.icon ?? ""} size={24} qualityLevel={machine.setup.machineQualityLevel} />
+                  <Icon iconName={machine.recipe?.icon ?? ""} size={24} />
+                </div>
+                <div
+                  className={styles.timelineGraph}
+                  style={{ height: "16px", width: graphWidth, display: "flex", alignItems: "center" }}
+                >
+                  <svg
+                    width="100%"
+                    height="100%"
+                    onMouseMove={(e) =>
+                      handleMouseMove(e, `Status`, (h) => (h.machines[mIndex]?.isCrafting ? "CRAFTING" : "HALTED"))
+                    }
+                  >
+                    <path d={machineStatusPath} stroke="#5eb663" strokeWidth="3" fill="none" />
+                  </svg>
+                </div>
+              </div>
+
+              {/* 3. OUTPUT ROWS */}
+              {Object.keys(machine.outputBuffer).map((itemKey) => {
+                const outLimit = machine.outputBlockLimits[itemKey] || 1;
+                const points = history
+                  .map(
+                    (h, i) => `${i * TICK_WIDTH},${30 - ((h.machines[mIndex]?.outputs[itemKey] || 0) / outLimit) * 30}`,
+                  )
+                  .join(" ");
+
+                return (
+                  <div key={`out-${itemKey}`} className={styles.timelineRow}>
+                    <div className={styles.timelineLabel} style={{ color: "#e39827" }}>
+                      <span>
+                        <Icon iconName={`item/${itemKey}.png`} size={24} />
+                      </span>
+                      <span>(out)</span>
+                    </div>
+                    <div className={styles.timelineGraph} style={{ height: "30px", width: graphWidth }}>
+                      <svg
+                        width="100%"
+                        height="100%"
+                        onMouseMove={(e) =>
+                          handleMouseMove(e, `Output ${itemKey}`, (h) => h.machines[mIndex]?.outputs[itemKey] || 0)
+                        }
+                      >
+                        <polyline points={points} fill="none" stroke="#e39827" strokeWidth="2" />
+                      </svg>
+                    </div>
+                  </div>
+                );
+              })}
+            </div>
+          );
+        })}
+
+        {/* ========================================== */}
+        {/* INSERTERS BLOCK */}
+        {/* ========================================== */}
+        {inserters.map((ins, iIndex) => {
+          const capacity = ins.handSize || 16;
+
+          const pointsHeld = history
+            .map((h, i) => `${i * TICK_WIDTH},${20 - ((h.inserters[iIndex]?.held || 0) / capacity) * 20}`)
+            .join(" ");
+
+          const statePaths = ["", "", "", "", ""];
+          let currentState = -1;
+          let stateStart = -1;
+
+          for (let i = 0; i < history.length; i++) {
+            const state = history[i].inserters[iIndex]?.state ?? 0;
+            if (state !== currentState) {
+              if (stateStart !== -1) {
+                statePaths[currentState] += ` M ${stateStart * TICK_WIDTH} 25.5 h ${(i - stateStart) * TICK_WIDTH}`;
+              }
+              currentState = state;
+              stateStart = i;
+            }
+          }
+          if (stateStart !== -1) {
+            statePaths[currentState] +=
+              ` M ${stateStart * TICK_WIDTH} 25.5 h ${(history.length - stateStart) * TICK_WIDTH}`;
+          }
+
+          return (
+            <div key={`i-${iIndex}`} className={styles.timelineGroup}>
+              <div className={styles.timelineRow}>
+                <div className={styles.timelineLabel} style={{ color: "#94a3b8" }}>
+                  <Icon iconName={`item/${ins.targetItemId}.png`} size={24} />
+                  Inserter
+                </div>
+
+                <div className={styles.timelineGraph} style={{ height: "30px", width: graphWidth }}>
+                  <svg
+                    width="100%"
+                    height="100%"
+                    onMouseMove={(e) =>
+                      handleMouseMove(
+                        e,
+                        `Inserter`,
+                        (h) =>
+                          `State: ${INSERTER_STATES[h.inserters[iIndex]?.state].label} | Held: ${h.inserters[iIndex]?.held}`,
+                      )
+                    }
+                  >
+                    <polyline points={pointsHeld} fill="none" stroke="#fcd34d" strokeWidth="2" />
+
+                    {statePaths.map(
+                      (pathD, idx) =>
+                        pathD && (
+                          <path key={idx} d={pathD} stroke={INSERTER_STATES[idx].color} strokeWidth="3" fill="none" />
+                        ),
+                    )}
+                  </svg>
+                </div>
+              </div>
+            </div>
+          );
+        })}
+      </div>
+    </div>
+  );
+}

+ 43 - 0
src/engine/Dashboard/types.ts

@@ -0,0 +1,43 @@
+import type { MachineSetup } from "../types";
+
+export interface MachineData {
+  id: string;
+  setup: MachineSetup;
+  recipe: { name: string; icon?: string };
+  isCrafting: boolean;
+  craftProgress: number;
+  inputBuffer: Record<string, number>;
+  outputBuffer: Record<string, number>;
+  outputBlockLimits: Record<string, number>;
+}
+
+export interface InserterData {
+  id: string;
+  targetItemId: string;
+  state: number;
+  heldItems: number;
+  swingCount: number;
+  handSize?: number;
+  source: any;
+  destination: any;
+}
+
+export interface HistorySnapshot {
+  tick: number;
+  machines: {
+    isCrafting: boolean;
+    inputs: Record<string, number>;
+    outputs: Record<string, number>;
+  }[];
+  inserters: {
+    state: number;
+    held: number;
+  }[];
+}
+
+export interface UIOrchestrator {
+  currentTick: number;
+  tick: () => void;
+  getMachines: () => MachineData[];
+  getInserters: () => InserterData[];
+}

+ 0 - 120
src/engine/FactorioSimulationDashBoard.tsx

@@ -1,120 +0,0 @@
-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 interface MachineData {
-  recipe: { name: string };
-  isCrafting: boolean;
-  craftProgress: number;
-  inputBuffer: Record<string, number>;
-  outputBuffer: Record<string, number>;
-  outputBlockLimits: Record<string, number>;
-}
-
-export interface InserterData {
-  targetItemId: string;
-  state: number;
-  heldItems: number;
-  swingCount: number;
-  handSize?: number;
-  source: any; // Ideally MachineData | ChestData
-  destination: any; // Ideally MachineData | ChestData
-}
-
-export interface HistorySnapshot {
-  tick: number;
-  machines: {
-    isCrafting: boolean;
-    out: number;
-    in: number;
-  }[];
-  inserters: {
-    state: number;
-    held: number;
-  }[];
-}
-
-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<HistorySnapshot[]>([]);
-  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>
-  );
-}

+ 0 - 103
src/engine/GraphView.tsx

@@ -1,103 +0,0 @@
-import { useMemo, useEffect, useState } from 'react';
-import ReactFlow, { Background, Controls, MarkerType } from 'reactflow';
-import 'reactflow/dist/style.css';
-import styles from './SimulationDashboard.module.css';
-import type { FactorioEngineOrchestrator } from './simulator';
-
-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: FactorioEngineOrchestrator, 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>
-  );
-}

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

@@ -1,160 +0,0 @@
-/* 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;
-}

+ 0 - 95
src/engine/TimelineView.tsx

@@ -1,95 +0,0 @@
-import type { HistorySnapshot, InserterData, MachineData } from './FactorioSimulationDashBoard';
-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: HistorySnapshot[], machines: MachineData[], inserters: InserterData[] }) {
-  
-  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>
-  );
-}

+ 3 - 4
src/engine/simulator.test.ts

@@ -589,7 +589,7 @@ describe("Factorio Strict Phase Orchestrator", () => {
 
     // --- PHASE 1: STABILIZATION ---
     // 16 items * 30 ticks = 488 ticks per complete buffer cycle
-    orchestrator.tickUntil(() => false, 480);
+    orchestrator.tickUntil(() => false, 5000);
 
     // Reset all swing counters to 0 to prepare for the measurement window
     allInserters.forEach((ins) => (ins.swingCount = 0));
@@ -605,13 +605,12 @@ describe("Factorio Strict Phase Orchestrator", () => {
     expect(outCirc1.swingCount).toBe(3);
     expect(outCirc2.swingCount).toBe(3);
 
-    
     // Validate Iron Inputs (3 swings each = 48 plates per machine)
     expect(inIron1.swingCount).toBe(3);
     expect(inIron2.swingCount).toBe(3);
 
     // Validate Copper Inputs (3 swings each = 48 plates per machine)
-    expect([inCop1.swingCount,inCop2.swingCount,inCop3.swingCount]).toBe([3,3,3]);
+    expect([inCop1.swingCount, inCop2.swingCount, inCop3.swingCount]).toStrictEqual([3, 3, 3]);
   });
 
   it("Clocked Baseline: 3:2 setup with staggered shared-inserter rows and exact 8-tick windows", () => {
@@ -707,7 +706,7 @@ describe("Factorio Strict Phase Orchestrator", () => {
     // --- PHASE 1: STABILIZATION ---
     // Let the factory run for 3 full cycles (1440 ticks) so all machine buffers fill,
     // the pipeline finishes, and steady-state clocked rhythm is established.
-    orchestrator.tickUntil(() => false, 1440);
+    orchestrator.tickUntil(() => false, 5000);
 
     // Reset counts for the true measurement
     allInserters.forEach((ins) => (ins.swingCount = 0));

+ 12 - 35
src/engine/simulator.ts

@@ -48,6 +48,7 @@ export enum InserterState {
 }
 
 export class MachineSimulator implements IContainer {
+  public id: string = crypto.randomUUID();
   public readonly type = ContainerType.Machine;
   public inputBuffer: Record<string, number> = {};
   public outputBuffer: Record<string, number> = {};
@@ -77,8 +78,7 @@ 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;
       }
     }
@@ -104,7 +104,7 @@ export class MachineSimulator implements IContainer {
 
     while (progressRemaining > 0) {
       if (!this.isCrafting) {
-        if (this.hasEnoughInputs() && !this.isOutputBlocked()) {
+        if (this.hasEnoughInputs()) {
           this.consumeInputs();
           this.isCrafting = true;
         } else {
@@ -143,8 +143,7 @@ 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;
   }
@@ -202,8 +201,7 @@ 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;
@@ -242,8 +240,7 @@ 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;
@@ -256,6 +253,7 @@ export class Belt implements IContainer {
   }
 }
 export class InserterSimulator {
+  public id: string = crypto.randomUUID();
   public state: InserterState = InserterState.Idle;
   public ticksInState = 0;
   public heldItems = 0;
@@ -327,11 +325,6 @@ export class InserterSimulator {
         break;
 
       case InserterState.Dropping:
-        if (!this.destination.canAccept(this.targetItemId)) {
-          this.ticksInState--;
-          break;
-        }
-
         if (this.ticksInState >= this.dropTicks) {
           this.destination.insert(this.targetItemId, this.heldItems);
 
@@ -353,9 +346,7 @@ 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)
     );
   }
 }
@@ -366,12 +357,7 @@ 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;
@@ -400,16 +386,11 @@ 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;
       }
@@ -541,11 +522,7 @@ 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);

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff