Bläddra i källkod

rename index to model.ts

clovis 1 månad sedan
förälder
incheckning
a1208ae2a3

+ 1 - 1
src/ClockBuilder.tsx

@@ -1,7 +1,7 @@
 import { useEffect, useMemo, useRef, useState } from "react";
 import { buildBlueprint } from "./blueprint/Blueprintbuilder";
 import { encodeBlueprintFileBrowser } from "./blueprint/parser";
-import type { ClockBlock } from "./assets/types";
+import type { ClockBlock } from "./assets/ClockTimeline/model";
 import styles from "./ClockBuilder.module.css";
 import SelectedBlockPanel from "./assets/SelectedBlockPanel";
 import SelectSignal from "./assets/Selector/SelectSignal";

+ 1 - 1
src/assets/ClockTimeline/ClockTimeline.tsx

@@ -1,7 +1,7 @@
 import { useMemo } from "react";
 import styles from "./ClockTimeline.module.css";
 import { useClockStore } from "../../store/useClockStore";
-import { ACTION_PRESETS } from "../types";
+import { ACTION_PRESETS } from "./model";
 import Icon from "../icon";
 import TimelineRow from "./TimelineRow";
 

+ 1 - 1
src/assets/ClockTimeline/SignalEditorList.tsx

@@ -1,5 +1,5 @@
 import { useRef } from "react";
-import type { Signal } from "../types";
+import type { Signal } from "./model";
 
 import styles from "./ClockTimeline.module.css";
 import SelectSignal from "../Selector/SelectSignal";

+ 1 - 1
src/assets/ClockTimeline/TimelineRow.tsx

@@ -4,7 +4,7 @@ import { useTimelineDrag } from "../../hooks/useTimelineDrag";
 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";
+import { ACTION_PRESETS, expandBlockInstances, getPreset, type ClockBlock, type Signal } from "./model";
 import SignalEditorList from "./SignalEditorList";
 import Select from "../components/Select";
 import Preset from "../components/Preset";

+ 0 - 62
src/assets/types.ts → src/assets/ClockTimeline/model.ts

@@ -82,65 +82,3 @@ export function blockDescription(block: ClockBlock, index: number): string {
         : "";
   return `Activation ${index + 1}${itemLine}`;
 }
-
-type Entity = {
-  entity_number: number;
-  name: string;
-  position: {
-    x: number;
-    y: number;
-  };
-  direction: 1 | 2 | 3 | 4;
-};
-
-export type BlueprintSignal = {
-  type?: string;
-  name: string;
-  quality?: string;
-};
-type NetworkState = {
-  red: boolean;
-  green: boolean;
-};
-export type DeciderCondition = {
-  first_signal: BlueprintSignal;
-  comparator: "<" | ">" | "=" | "≥" | "≤" | "≠";
-  compare_type?: "and" | "or";
-  first_signal_networks?: NetworkState;
-} & ({ second_signal: BlueprintSignal; second_signal_networks?: NetworkState } | { constant?: number });
-
-type DeciderOutput = {
-  signal: BlueprintSignal;
-} & ({ copy_count_from_input: false; constant?: number } | { networks?: { red: boolean; green: boolean } });
-
-export type DeciderCombinator = Entity & {
-  name: "decider-combinator";
-  control_behavior: {
-    decider_conditions: {
-      conditions?: DeciderCondition[];
-      outputs?: DeciderOutput[];
-      else_outputs?: DeciderOutput[];
-    };
-  };
-  player_description: string;
-};
-export type Blueprint = {
-  blueprint: {
-    item: "blueprint";
-    version: 562954249109505;
-    icons: Array<{ signal: BlueprintSignal; index: 1 | 2 | 3 | 4 }>;
-    entities: [];
-    /**Each wires is represented by 4 number
-     * [a, b, c, d]
-     * a : entity_number of first entity
-     * b : wire connector (1-4) of first entity
-     * a : entity_number of second entity
-     * b : wire connector (1-4) of second entity
-     * Most entity have only 2 connector (red and green circuit)
-     * Decider Combinator, Arithmetic Combinator and Selector Combinator have 4 connectors (2 inputs, 2 outputs)
-     * 1 & 3 red
-     * 2 & 4 green
-     */
-    wires: Array<Array<number>>;
-  };
-};

+ 1 - 1
src/assets/SelectedBlockPanel.tsx

@@ -1,5 +1,5 @@
 import styles from "./SelectedBlockPanel.module.css";
-import { ACTION_PRESETS, getPreset } from "./types";
+import { ACTION_PRESETS, getPreset } from "./ClockTimeline/model";
 import ExpressionInput from "./components/ExpressionInpux";
 import { useClockStore } from "../store/useClockStore";
 import { useMemo } from "react";

+ 1 - 1
src/assets/Selector/SelectSignal.tsx

@@ -5,7 +5,7 @@ import Icon from "../icon";
 import { Popper } from "@mui/material";
 import SelectMenu from "./SelectFactorioMenu";
 import Tooltip from "../Tooltip";
-import type { Signal } from "../types";
+import type { Signal } from "../ClockTimeline/model";
 
 const signals = data.signalGroup.flatMap((r) => r.subGroup.flatMap((s) => (s.children ?? []) as Signal[]));
 

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

@@ -1,5 +1,5 @@
 import Icon from "../icon";
-import type { ActionPreset } from "../types";
+import type { ActionPreset } from "../ClockTimeline/model";
 
 interface PresetProps {
   preset: ActionPreset;

+ 2 - 4
src/blueprint/Blueprintbuilder.ts

@@ -1,13 +1,11 @@
 import {
   expandBlockInstances,
   getPreset,
-  type BlueprintSignal,
   type ClockBlock,
   type ClockRow,
-  type DeciderCombinator,
-  type DeciderCondition,
   type Signal,
-} from "../assets/types";
+} from "../assets/ClockTimeline/model";
+import { type BlueprintSignal, type DeciderCombinator, type DeciderCondition } from "./model";
 
 const BP_VERSION = 562954249109505; // version stamp reused from a real 2.0 export
 

+ 60 - 0
src/blueprint/model.ts

@@ -0,0 +1,60 @@
+type Entity = {
+  entity_number: number;
+  name: string;
+  position: {
+    x: number;
+    y: number;
+  };
+  direction: 1 | 2 | 3 | 4;
+};
+
+export type BlueprintSignal = {
+  type?: string;
+  name: string;
+  quality?: string;
+};
+type NetworkState = {
+  red: boolean;
+  green: boolean;
+};
+export type DeciderCondition = {
+  first_signal: BlueprintSignal;
+  comparator: "<" | ">" | "=" | "≥" | "≤" | "≠";
+  compare_type?: "and" | "or";
+  first_signal_networks?: NetworkState;
+} & ({ second_signal: BlueprintSignal; second_signal_networks?: NetworkState } | { constant?: number });
+type DeciderOutput = {
+  signal: BlueprintSignal;
+} & ({ copy_count_from_input: false; constant?: number } | { networks?: { red: boolean; green: boolean } });
+
+export type DeciderCombinator = Entity & {
+  name: "decider-combinator";
+  control_behavior: {
+    decider_conditions: {
+      conditions?: DeciderCondition[];
+      outputs?: DeciderOutput[];
+      else_outputs?: DeciderOutput[];
+    };
+  };
+  player_description: string;
+};
+export type Blueprint = {
+  blueprint: {
+    item: "blueprint";
+    version: 562954249109505;
+    icons: Array<{ signal: BlueprintSignal; index: 1 | 2 | 3 | 4 }>;
+    entities: [];
+    /**Each wires is represented by 4 number
+     * [a, b, c, d]
+     * a : entity_number of first entity
+     * b : wire connector (1-4) of first entity
+     * a : entity_number of second entity
+     * b : wire connector (1-4) of second entity
+     * Most entity have only 2 connector (red and green circuit)
+     * Decider Combinator, Arithmetic Combinator and Selector Combinator have 4 connectors (2 inputs, 2 outputs)
+     * 1 & 3 red
+     * 2 & 4 green
+     */
+    wires: Array<Array<number>>;
+  };
+};

+ 1 - 1
src/engine/Dashboard/types.ts

@@ -1,4 +1,4 @@
-import type { MachineSetup } from "../types";
+import type { MachineSetup } from "../model";
 
 export interface MachineData {
   id: string;

+ 121 - 114
src/engine/batch.test.ts

@@ -1,22 +1,22 @@
-import { describe, it, expect } from 'vitest';
-import { calculateOptimalBatch } from './batch';
-import type { Recipe } from '../../scripts/factorio-dump/helpers/recipes.helper';
-import type { CalculatedTimings } from './types';
+import { describe, it, expect } from "vitest";
+import { calculateOptimalBatch } from "./batch";
+import type { Recipe } from "../../scripts/factorio-dump/helpers/recipes.helper";
+import type { CalculatedTimings } from "./model";
 
-describe('Batch Calculator', () => {
-  it('calculates optimal batch for Copper Cable (1 plate -> 2 cables)', () => {
+describe("Batch Calculator", () => {
+  it("calculates optimal batch for Copper Cable (1 plate -> 2 cables)", () => {
     const mockRecipe: Partial<Recipe> = {
       energy_required: 0.5,
-      ingredients: [{ type: 'item', name: 'copper-plate', amount: 1 }],
-      results: [{ type: 'item', name: 'copper-cable', amount: 2 }]
+      ingredients: [{ type: "item", name: "copper-plate", amount: 1 }],
+      results: [{ type: "item", name: "copper-cable", amount: 2 }],
     };
-    
+
     const timings: CalculatedTimings = {
       actualCraftingSpeed: 1,
       productivityBonus: 0,
       singleCraftTicks: 30,
       craftsPerSecond: 2,
-      overloadMultiplier: 8
+      overloadMultiplier: 8,
     };
 
     // With a stack size of 16:
@@ -27,48 +27,49 @@ describe('Batch Calculator', () => {
 
     expect(batch.craftsPerCycle).toBe(16);
     expect(batch.durationTicks).toBe(16 * 30); // 480 ticks
-    expect(batch.inputs['copper-plate'].totalAmount).toBe(16); // 1 full stack
-    expect(batch.outputs['copper-cable'].totalAmount).toBe(32); // 2 full stacks
+    expect(batch.inputs["copper-plate"].totalAmount).toBe(16); // 1 full stack
+    expect(batch.outputs["copper-cable"].totalAmount).toBe(32); // 2 full stacks
   });
 
-  it('factors in productivity yields correctly', () => {
+  it("factors in productivity yields correctly", () => {
     const mockRecipe: Partial<Recipe> = {
-      ingredients: [{ type: 'item', name: 'iron-plate', amount: 2 }],
-      results: [{ type: 'item', name: 'iron-gear-wheel', amount: 1 }]
+      ingredients: [{ type: "item", name: "iron-plate", amount: 2 }],
+      results: [{ type: "item", name: "iron-gear-wheel", amount: 1 }],
     };
-    
+
     // 50% productivity bonus means 1 craft yields 1.5 items
     const timings: CalculatedTimings = {
       actualCraftingSpeed: 1,
-      productivityBonus: 0.5, 
+      productivityBonus: 0.5,
       singleCraftTicks: 30,
       craftsPerSecond: 2,
-      overloadMultiplier: 8
+      overloadMultiplier: 8,
     };
 
     const batch = calculateOptimalBatch(mockRecipe as Recipe, timings);
 
     expect(batch.craftsPerCycle).toBe(32);
-    expect(batch.inputs['iron-plate'].totalAmount).toBe(64); // 4 input stacks
-    expect(batch.outputs['iron-gear-wheel'].totalAmount).toBe(48); // 3 output stacks
+    expect(batch.inputs["iron-plate"].totalAmount).toBe(64); // 4 input stacks
+    expect(batch.outputs["iron-gear-wheel"].totalAmount).toBe(48); // 3 output stacks
   });
-  it('ignores fluid ingredients and results when calculating LCM', () => {
+  it("ignores fluid ingredients and results when calculating LCM", () => {
     // e.g. Battery recipe: 1 Iron Plate, 1 Copper Plate, 20 Sulfuric Acid (Fluid) -> 1 Battery
     const mockRecipe: Partial<Recipe> = {
       energy_required: 4,
       ingredients: [
-        { type: 'item', name: 'iron-plate', amount: 1 },
-        { type: 'item', name: 'copper-plate', amount: 1 },
-        { type: 'fluid', name: 'sulfuric-acid', amount: 20 } // FLUID
+        { type: "item", name: "iron-plate", amount: 1 },
+        { type: "item", name: "copper-plate", amount: 1 },
+        { type: "fluid", name: "sulfuric-acid", amount: 20 }, // FLUID
       ],
-      results: [
-        { type: 'item', name: 'battery', amount: 1 }
-      ]
+      results: [{ type: "item", name: "battery", amount: 1 }],
     };
-    
+
     const timings: CalculatedTimings = {
-      actualCraftingSpeed: 1, productivityBonus: 0, 
-      singleCraftTicks: 240, craftsPerSecond: 0.25, overloadMultiplier: 8
+      actualCraftingSpeed: 1,
+      productivityBonus: 0,
+      singleCraftTicks: 240,
+      craftsPerSecond: 0.25,
+      overloadMultiplier: 8,
     };
 
     const batch = calculateOptimalBatch(mockRecipe as Recipe, timings);
@@ -76,103 +77,109 @@ describe('Batch Calculator', () => {
     // If fluids were not ignored, GCD/LCM math would break trying to divide 20 fluid by 16 items.
     // It should safely calculate LCM based ONLY on the item amounts (all are 1, so LCM = 16).
     expect(batch.craftsPerCycle).toBe(16);
-    expect(batch.inputs['iron-plate']).toBeDefined();
-    expect(batch.inputs['sulfuric-acid']).toBeUndefined(); // Fluids should not be scheduled for inserters
+    expect(batch.inputs["iron-plate"]).toBeDefined();
+    expect(batch.inputs["sulfuric-acid"]).toBeUndefined(); // Fluids should not be scheduled for inserters
   });
 
-  it('calculates the output block limit correctly (bottlenecked by overload multiplier)', () => {
+  it("calculates the output block limit correctly (bottlenecked by overload multiplier)", () => {
     const mockRecipe: Partial<Recipe> = {
-      ingredients: [{ type: 'item', name: 'copper-plate', amount: 1 }],
-      results: [{ type: 'item', name: 'copper-cable', amount: 2 }]
+      ingredients: [{ type: "item", name: "copper-plate", amount: 1 }],
+      results: [{ type: "item", name: "copper-cable", amount: 2 }],
     };
-    
+
     const timings: CalculatedTimings = {
-      actualCraftingSpeed: 1, productivityBonus: 0, singleCraftTicks: 30, craftsPerSecond: 2,
-      overloadMultiplier: 4 // Very slow machine, low overload limit
+      actualCraftingSpeed: 1,
+      productivityBonus: 0,
+      singleCraftTicks: 30,
+      craftsPerSecond: 2,
+      overloadMultiplier: 4, // Very slow machine, low overload limit
     };
 
     // We pass a max stack size of 200 for the cable.
-    const batch = calculateOptimalBatch(mockRecipe as Recipe, timings, { 'copper-cable': 200 });
+    const batch = calculateOptimalBatch(mockRecipe as Recipe, timings, { "copper-cable": 200 });
 
     // Output block limit is Math.min(maxStack, overloadMultiplier * resultAmount)
     // Math.min(200, 4 * 2) = 8
-    expect(batch.outputs['copper-cable'].outputBlockLimit).toBe(8);
+    expect(batch.outputs["copper-cable"].outputBlockLimit).toBe(8);
   });
 
-  it('calculates the output block limit correctly (bottlenecked by max stack size)', () => {
+  it("calculates the output block limit correctly (bottlenecked by max stack size)", () => {
     const mockRecipe: Partial<Recipe> = {
-      ingredients: [{ type: 'item', name: 'iron-plate', amount: 1 }],
-      results: [{ type: 'item', name: 'iron-gear-wheel', amount: 1 }]
+      ingredients: [{ type: "item", name: "iron-plate", amount: 1 }],
+      results: [{ type: "item", name: "iron-gear-wheel", amount: 1 }],
     };
-    
+
     const timings: CalculatedTimings = {
-      actualCraftingSpeed: 10, productivityBonus: 0, singleCraftTicks: 3, craftsPerSecond: 20,
-      overloadMultiplier: 100 // Very fast machine, massive overload limit
+      actualCraftingSpeed: 10,
+      productivityBonus: 0,
+      singleCraftTicks: 3,
+      craftsPerSecond: 20,
+      overloadMultiplier: 100, // Very fast machine, massive overload limit
     };
 
     // Iron gear wheels stack to 100 in Factorio
-    const batch = calculateOptimalBatch(mockRecipe as Recipe, timings, { 'iron-gear-wheel': 100 });
+    const batch = calculateOptimalBatch(mockRecipe as Recipe, timings, { "iron-gear-wheel": 100 });
 
     // Output block limit is Math.min(maxStack, overloadMultiplier * resultAmount)
     // Math.min(100, 100 * 1) = 100
-    expect(batch.outputs['iron-gear-wheel'].outputBlockLimit).toBe(100);
-    expect(batch.outputs['iron-gear-wheel'].totalAmount).toBe(96);
+    expect(batch.outputs["iron-gear-wheel"].outputBlockLimit).toBe(100);
+    expect(batch.outputs["iron-gear-wheel"].totalAmount).toBe(96);
   });
-  it('handles complex multi-input recipes (Advanced Circuit) with productivity', () => {
+  it("handles complex multi-input recipes (Advanced Circuit) with productivity", () => {
     // Advanced Circuit: 2 Plastic, 4 Copper Cable, 2 Green Chips -> 1 Red Chip
     const mockRecipe: Partial<Recipe> = {
       ingredients: [
-        { type: 'item', name: 'plastic-bar', amount: 2 },
-        { type: 'item', name: 'copper-cable', amount: 4 },
-        { type: 'item', name: 'electronic-circuit', amount: 2 }
+        { type: "item", name: "plastic-bar", amount: 2 },
+        { type: "item", name: "copper-cable", amount: 4 },
+        { type: "item", name: "electronic-circuit", amount: 2 },
       ],
-      results: [{ type: 'item', name: 'advanced-circuit', amount: 1 }]
+      results: [{ type: "item", name: "advanced-circuit", amount: 1 }],
     };
-    
+
     const timings: CalculatedTimings = {
       actualCraftingSpeed: 84,
-      productivityBonus: 1.75, 
-      singleCraftTicks: 4.2857142857142865, 
+      productivityBonus: 1.75,
+      singleCraftTicks: 4.2857142857142865,
       craftsPerSecond: 14,
-      overloadMultiplier: 17
+      overloadMultiplier: 17,
     };
 
-    const batch = calculateOptimalBatch(mockRecipe as Recipe, timings, { 'advanced-circuit': 200 });
+    const batch = calculateOptimalBatch(mockRecipe as Recipe, timings, { "advanced-circuit": 200 });
 
     expect(batch.craftsPerCycle).toBe(64);
     expect(batch.durationTicks).toBe(275);
     // 64 crafts * 2 per craft = 128 (8 inserter swings of 16)
-    expect(batch.inputs['plastic-bar'].totalAmount).toBe(128); 
-    
+    expect(batch.inputs["plastic-bar"].totalAmount).toBe(128);
+
     // 64 crafts * 4 per craft = 256 (16 inserter swings of 16)
-    expect(batch.inputs['copper-cable'].totalAmount).toBe(256); 
-    
+    expect(batch.inputs["copper-cable"].totalAmount).toBe(256);
+
     // 64 crafts * 2 per craft = 128 (8 inserter swings of 16)
-    expect(batch.inputs['electronic-circuit'].totalAmount).toBe(128); 
-    
+    expect(batch.inputs["electronic-circuit"].totalAmount).toBe(128);
+
     // 64 crafts * 2.75 yield = 176 (11 inserter swings of 16)
-    expect(batch.outputs['advanced-circuit'].totalAmount).toBe(176); 
-    expect(batch.outputs['advanced-circuit'].outputBlockLimit).toBe(17);
+    expect(batch.outputs["advanced-circuit"].totalAmount).toBe(176);
+    expect(batch.outputs["advanced-circuit"].outputBlockLimit).toBe(17);
   });
-  it('handles prime-number fractions in extreme productivity scenarios (The Direct-Insertion Anchor)', () => {
+  it("handles prime-number fractions in extreme productivity scenarios (The Direct-Insertion Anchor)", () => {
     // Productivity Module 1: 5 Advanced Circuit, 5 Electronic Circuit -> 1 Prod Module
     const mockRecipe: Partial<Recipe> = {
       ingredients: [
-        { type: 'item', name: 'advanced-circuit', amount: 5 },
-        { type: 'item', name: 'electronic-circuit', amount: 5 }
+        { type: "item", name: "advanced-circuit", amount: 5 },
+        { type: "item", name: "electronic-circuit", amount: 5 },
       ],
-      results: [{ type: 'item', name: 'productivity-module', amount: 1 }]
+      results: [{ type: "item", name: "productivity-module", amount: 1 }],
     };
-    
+
     const timings: CalculatedTimings = {
       actualCraftingSpeed: 5,
       productivityBonus: 1.15, // +115% yield (Yields 2.15 items per craft)
-      singleCraftTicks: 60, 
+      singleCraftTicks: 60,
       craftsPerSecond: 1,
-      overloadMultiplier: 4 
+      overloadMultiplier: 4,
     };
 
-    const batch = calculateOptimalBatch(mockRecipe as Recipe, timings, { 'productivity-module': 50 });
+    const batch = calculateOptimalBatch(mockRecipe as Recipe, timings, { "productivity-module": 50 });
 
     // --- The Math Breakdown ---
     // 1. Productivity Yield:
@@ -191,37 +198,37 @@ describe('Batch Calculator', () => {
     //    LCM(16, 320) = 320 crafts per cycle.
 
     expect(batch.craftsPerCycle).toBe(320);
-    
+
     // Inputs: 320 crafts * 5 per craft = 1600.
     // 1600 / 16 = Exactly 100 perfect inserter swings per cycle!
-    expect(batch.inputs['advanced-circuit'].totalAmount).toBe(1600); 
-    expect(batch.inputs['electronic-circuit'].totalAmount).toBe(1600); 
-    
+    expect(batch.inputs["advanced-circuit"].totalAmount).toBe(1600);
+    expect(batch.inputs["electronic-circuit"].totalAmount).toBe(1600);
+
     // Outputs: 320 crafts * 2.15 yield = 688 items.
     // 688 / 16 = Exactly 43 perfect inserter swings per cycle!
-    expect(batch.outputs['productivity-module'].totalAmount).toBe(688); 
+    expect(batch.outputs["productivity-module"].totalAmount).toBe(688);
   });
-  it('safely caps batch scaling on high-volume output recipes to prevent buffer overflow', () => {
+  it("safely caps batch scaling on high-volume output recipes to prevent buffer overflow", () => {
     // Copper Cable: 1 Copper Plate -> 2 Copper Cable
     const mockRecipe: Partial<Recipe> = {
-      ingredients: [{ type: 'item', name: 'copper-plate', amount: 1 }],
-      results: [{ type: 'item', name: 'copper-cable', amount: 2 }]
+      ingredients: [{ type: "item", name: "copper-plate", amount: 1 }],
+      results: [{ type: "item", name: "copper-cable", amount: 2 }],
     };
-    
+
     const timings: CalculatedTimings = {
       actualCraftingSpeed: 10,
       productivityBonus: 2.5, // +250% (Total yield = 2 * 3.5 = 7 cables per craft)
-      singleCraftTicks: 6, 
+      singleCraftTicks: 6,
       craftsPerSecond: 10,
-      overloadMultiplier: 50 // Machine can hold 50 * 1 plate = 50 plates buffer
+      overloadMultiplier: 50, // Machine can hold 50 * 1 plate = 50 plates buffer
     };
 
     // We restrict the cable output buffer to max stack (200)
-    const batch = calculateOptimalBatch(mockRecipe as Recipe, timings, { 'copper-cable': 200 });
+    const batch = calculateOptimalBatch(mockRecipe as Recipe, timings, { "copper-cable": 200 });
 
     // --- The Math Breakdown ---
     // Output Yield = 7 cables per craft.
-    // Base LCM: 
+    // Base LCM:
     // Inputs (1): 16 / gcd(1, 16) = 16 crafts.
     // Outputs (yield 7): Denominator (16) / gcd(7, 16) = 16 crafts.
     // baseOptimalN = 16 crafts.
@@ -233,33 +240,33 @@ describe('Batch Calculator', () => {
     //
     // WAIT! maxSafeCrafts (14) is LESS than baseOptimalN (16).
     // The engine CANNOT safely scale down below the base LCM, or else we lose perfect 16-stack swings.
-    // It should fallback to baseOptimalN (16) and rely on the Timeline generator's 
+    // It should fallback to baseOptimalN (16) and rely on the Timeline generator's
     // drip-feed logic to keep the machine from jamming.
 
     expect(batch.craftsPerCycle).toBe(16); // Remains at the absolute mathematical minimum
-    expect(batch.inputs['copper-plate'].totalAmount).toBe(16);
-    expect(batch.outputs['copper-cable'].totalAmount).toBe(112); // 16 * 7
+    expect(batch.inputs["copper-plate"].totalAmount).toBe(16);
+    expect(batch.outputs["copper-cable"].totalAmount).toBe(112); // 16 * 7
   });
-  it('handles Legendary Max-Out (+100% Prod) with a highly restrictive overload limit', () => {
+  it("handles Legendary Max-Out (+100% Prod) with a highly restrictive overload limit", () => {
     // Chemical Science Pack: 2 Engine, 3 Adv Circuit, 1 Sulfur -> 2 Chemical Science Packs
     const mockRecipe: Partial<Recipe> = {
       ingredients: [
-        { type: 'item', name: 'engine-unit', amount: 2 },
-        { type: 'item', name: 'advanced-circuit', amount: 3 },
-        { type: 'item', name: 'sulfur', amount: 1 }
+        { type: "item", name: "engine-unit", amount: 2 },
+        { type: "item", name: "advanced-circuit", amount: 3 },
+        { type: "item", name: "sulfur", amount: 1 },
       ],
-      results: [{ type: 'item', name: 'chemical-science-pack', amount: 2 }]
+      results: [{ type: "item", name: "chemical-science-pack", amount: 2 }],
     };
-    
+
     const timings: CalculatedTimings = {
       actualCraftingSpeed: 10,
       productivityBonus: 1.0, // +100% (4 Legendary Prod 3 Modules)
-      singleCraftTicks: 14.4, 
+      singleCraftTicks: 14.4,
       craftsPerSecond: 4.16,
-      overloadMultiplier: 3 // Extremely restrictive! Machine will stall almost instantly.
+      overloadMultiplier: 3, // Extremely restrictive! Machine will stall almost instantly.
     };
 
-    const batch = calculateOptimalBatch(mockRecipe as Recipe, timings, { 'chemical-science-pack': 200 });
+    const batch = calculateOptimalBatch(mockRecipe as Recipe, timings, { "chemical-science-pack": 200 });
 
     // --- The Math Breakdown ---
     // 1. Productivity: Yield is 2 * 2.0 = 4 items per craft. (prod fraction = 2/1)
@@ -276,28 +283,28 @@ describe('Batch Calculator', () => {
     //
     // 4. Base LCM = LCM(16, 4) = 16 crafts.
     //
-    // 5. Buffer Limits (maxSafeCrafts): 
+    // 5. Buffer Limits (maxSafeCrafts):
     //    Output limit = min(200, 3 (overload) * 2 (amount)) = 6 items max in output buffer!
-    //    Yield per craft is 4. 
+    //    Yield per craft is 4.
     //    Max safe crafts = Math.floor(6 / 4) = 1 craft!
     //
-    
+
     expect(batch.craftsPerCycle).toBe(16);
-    
+
     // Inputs:
     // Engine: 16 * 2 = 32 (Exactly 2 swings of 16)
-    expect(batch.inputs['engine-unit'].totalAmount).toBe(32);
-    
+    expect(batch.inputs["engine-unit"].totalAmount).toBe(32);
+
     // Adv Circuit: 16 * 3 = 48 (Exactly 3 swings of 16)
-    expect(batch.inputs['advanced-circuit'].totalAmount).toBe(48);
-    
+    expect(batch.inputs["advanced-circuit"].totalAmount).toBe(48);
+
     // Sulfur: 16 * 1 = 16 (Exactly 1 swing of 16)
-    expect(batch.inputs['sulfur'].totalAmount).toBe(16);
-    
+    expect(batch.inputs["sulfur"].totalAmount).toBe(16);
+
     // Output: 16 * 4 yield = 64 items (Exactly 4 swings of 16)
-    expect(batch.outputs['chemical-science-pack'].totalAmount).toBe(64); 
-    
+    expect(batch.outputs["chemical-science-pack"].totalAmount).toBe(64);
+
     // Validates that the engine properly identified the output bottleneck
-    expect(batch.outputs['chemical-science-pack'].outputBlockLimit).toBe(6); 
+    expect(batch.outputs["chemical-science-pack"].outputBlockLimit).toBe(6);
   });
-});
+});

+ 13 - 15
src/engine/batch.ts

@@ -1,8 +1,7 @@
 import type { Recipe } from "../../scripts/factorio-dump/helpers/recipes.helper";
-import type { CalculatedTimings, BatchPlan } from "./types";
+import type { CalculatedTimings, BatchPlan } from "./model";
 import { gcd, lcm, floatToFraction } from "./math";
 
-
 function getMinimumCraftsForOutput(amount: number, prodRatio: { num: number; den: number }, stackSize: number): number {
   const numerator = amount * prodRatio.num;
   const denominator = stackSize * prodRatio.den;
@@ -11,7 +10,7 @@ function getMinimumCraftsForOutput(amount: number, prodRatio: { num: number; den
 /**
  * Calculates the optimal number of crafts (N) to group into one clock cycle.
  * Ensures that all inputs and outputs move in exact multiples of the inserter stack size.
- * 
+ *
  * @param recipe The recipe to process.
  * @param timings Calculated timings from computeMachineStats.
  * @param itemStackSizes A map of the maximum inventory stack size for specific items.
@@ -26,32 +25,30 @@ export function calculateOptimalBatch(
   const baselineStack = 16; // Standard fully researched stack size
   // Process Solid Ingredients
   const solidIngredients = (recipe.ingredients || []).filter((ing) => ing.type === "item");
-  const hasIngredients = solidIngredients.length>0
+  const hasIngredients = solidIngredients.length > 0;
   for (const ing of solidIngredients) {
     const requiredN = baselineStack / gcd(ing.amount, baselineStack);
     optimalN = lcm(optimalN, requiredN);
   }
-  let maxSafeCrafts = hasIngredients? timings.overloadMultiplier : Infinity;
-  
+  let maxSafeCrafts = hasIngredients ? timings.overloadMultiplier : Infinity;
+
   // Process Solid Products
   const solidResults = (recipe.results || []).filter((res) => res.type === "item");
   for (const res of solidResults) {
-    const amount = res.amount ?? res.amount_min ?? 1
-    const requiredN = getMinimumCraftsForOutput(amount,prodMultiplier,baselineStack)
+    const amount = res.amount ?? res.amount_min ?? 1;
+    const requiredN = getMinimumCraftsForOutput(amount, prodMultiplier, baselineStack);
     optimalN = lcm(optimalN, requiredN);
 
     const maxStack = itemStackSizes[res.name] ?? 50;
-    const outputBlockQuantity = hasIngredients
-      ? Math.min(maxStack, timings.overloadMultiplier * amount) 
-      : maxStack;
-      
+    const outputBlockQuantity = hasIngredients ? Math.min(maxStack, timings.overloadMultiplier * amount) : maxStack;
+
     const yieldPerCraft = (amount * prodMultiplier.num) / prodMultiplier.den;
     maxSafeCrafts = Math.min(maxSafeCrafts, Math.floor(outputBlockQuantity / yieldPerCraft));
   }
 
   if (maxSafeCrafts !== Infinity && maxSafeCrafts > optimalN) {
     const scaleFactor = Math.floor(maxSafeCrafts / optimalN);
-    optimalN *= scaleFactor; 
+    optimalN *= scaleFactor;
   }
   // Compile final totals
   const inputs: BatchPlan["inputs"] = {};
@@ -66,7 +63,8 @@ export function calculateOptimalBatch(
 
     const maxItemStack = itemStackSizes[res.name] ?? 50;
     // Factorio Output Block Logic: machine stalls if buffer exceeds min(StackSize, OverloadMultiplier * ResultAmount)
-    const outputBlockQuantity = recipe.ingredients && recipe.ingredients.length > 0
+    const outputBlockQuantity =
+      recipe.ingredients && recipe.ingredients.length > 0
         ? Math.min(maxItemStack, timings.overloadMultiplier * amount)
         : maxItemStack;
 
@@ -85,4 +83,4 @@ export function calculateOptimalBatch(
     inputs,
     outputs,
   };
-}
+}

+ 2 - 2
src/engine/index.ts

@@ -1,5 +1,5 @@
-export * from "./types";
+export * from "./model";
 export { gcd, lcm, floatToFraction } from "./math";
 export { computeMachineStats, getQualityMultiplier, getBeaconOverlapPenalty, getTransmissionStrength } from "./stats";
 export { calculateOptimalBatch } from "./batch";
-export { generateAdvancedClock } from "./timeline";
+export { generateAdvancedClock } from "./timeline";

+ 0 - 0
src/engine/types.ts → src/engine/model.ts


+ 2 - 16
src/engine/simulator.test.ts

@@ -9,7 +9,7 @@ import {
   Belt,
   FilterableInserterSimulator,
 } from "./simulator";
-import type { MachineSetup } from "./types";
+import type { MachineSetup } from "./model";
 import type { Recipe } from "../../scripts/factorio-dump/helpers/recipes.helper";
 import type { Machine } from "../../scripts/factorio-dump/process-data.models";
 
@@ -653,7 +653,7 @@ describe("Factorio Strict Phase Orchestrator", () => {
     } as Recipe;
 
     const assemblerSetup: MachineSetup = {
-      machine: { name: "assembling-machine-2", crafting_speed: 1 } as Machine,
+      machine: { name: "assembling-machine-2", crafting_speed: 10 } as Machine,
       machineModules: [],
       beacons: [],
       machineQualityLevel: 0,
@@ -705,7 +705,6 @@ describe("Factorio Strict Phase Orchestrator", () => {
     const outCirc1 = new InserterSimulator(16, circ1, sinkGreenChips, "electronic-circuit");
     const outCirc2 = new InserterSimulator(16, circ2, sinkGreenChips, "electronic-circuit");
 
-    // Bind to Pub/Sub Rows
     [inCop1, inCop2, inCop3, inIron1, inIron2].forEach((ins) => orchestrator.bindInserterToRow(ins, "row_inputs"));
     [mid1, mid3].forEach((ins) => orchestrator.bindInserterToRow(ins, "row_mid_outer"));
     orchestrator.bindInserterToRow(mid2a, "row_mid_inner");
@@ -717,34 +716,21 @@ describe("Factorio Strict Phase Orchestrator", () => {
     [cop1, cop2, cop3, circ1, circ2].forEach((m) => orchestrator.registerMachine(m));
 
     // --- 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, 5000);
-
-    // Reset counts for the true measurement
     allInserters.forEach((ins) => (ins.swingCount = 0));
     const startTarget = orchestrator.currentTick;
 
     // --- PHASE 2: MEASUREMENT WINDOW ---
-    // Measure exactly 3 cycles (1440 ticks).
     orchestrator.tickUntil(() => false, startTarget + 1440);
 
     // --- VALIDATION ---
-    // Inputs: exactly 3 swings (1 per cycle)
     expect(inCop1.swingCount).toBe(3);
     expect(inCop2.swingCount).toBe(3);
     expect(inIron1.swingCount).toBe(3);
-
-    // Mid Outer (cop1, cop3): exactly 6 swings (2 per cycle)
     expect(mid1.swingCount).toBe(6);
     expect(mid3.swingCount).toBe(6);
-
-    // Mid Inner (cop2): exactly 3 swings EACH (1 per cycle).
-    // They did not fight because they were staggered by the clock!
     expect(mid2a.swingCount).toBe(3);
     expect(mid2b.swingCount).toBe(3);
-
-    // Outputs: exactly 3 swings (1 per cycle)
     expect(outCirc1.swingCount).toBe(3);
     expect(outCirc2.swingCount).toBe(3);
 

+ 1 - 1
src/engine/simulator.ts

@@ -1,5 +1,5 @@
 import { computeMachineStats } from "./stats";
-import type { MachineSetup, CalculatedTimings } from "./types";
+import type { MachineSetup, CalculatedTimings } from "./model";
 import type { Recipe } from "../../scripts/factorio-dump/helpers/recipes.helper";
 
 export enum ContainerType {

+ 3 - 10
src/engine/stats.test.ts

@@ -1,15 +1,8 @@
 import { describe, it, expect } from "vitest";
-import {
-  getQualityMultiplier,
-  getBeaconOverlapPenalty,
-  computeMachineStats,
-} from "./stats";
-import type { MachineSetup } from "./types";
+import { getQualityMultiplier, getBeaconOverlapPenalty, computeMachineStats } from "./stats";
+import type { MachineSetup } from "./model";
 import type { Recipe } from "../../scripts/factorio-dump/helpers/recipes.helper";
-import type {
-  Beacon,
-  Machine,
-} from "../../scripts/factorio-dump/process-data.models";
+import type { Beacon, Machine } from "../../scripts/factorio-dump/process-data.models";
 
 describe("getQualityMultiplier", () => {
   it("returns correct quality multipliers", () => {

+ 3 - 3
src/engine/stats.ts

@@ -1,6 +1,6 @@
 import type { Recipe } from "../../scripts/factorio-dump/helpers/recipes.helper";
 import type { Beacon } from "../../scripts/factorio-dump/process-data.models";
-import type { MachineSetup, CalculatedTimings } from "./types";
+import type { MachineSetup, CalculatedTimings } from "./model";
 
 /**
  * Returns the Factorio 2.0 stat multiplier for a given quality level.
@@ -16,7 +16,7 @@ export function getQualityMultiplier(level: number): number {
  * @param overlapCount Total number of beacons of this type affecting the machine.
  */
 export function getBeaconOverlapPenalty(beacon: Beacon, overlapCount: number): number {
-  if (beacon.profile && (beacon.profile.length >= overlapCount)) {
+  if (beacon.profile && beacon.profile.length >= overlapCount) {
     return beacon.profile[overlapCount - 1];
   }
   // Default formula if profile array is exceeded or missing
@@ -88,4 +88,4 @@ export function computeMachineStats(setup: MachineSetup, recipe: Recipe): Calcul
     craftsPerSecond: 1 / craftTimeSeconds,
     overloadMultiplier,
   };
-}
+}

+ 74 - 50
src/engine/timeline.test.ts

@@ -1,46 +1,52 @@
-import { describe, it, expect } from 'vitest';
-import { generateAdvancedClock } from './timeline';
-import type { BatchPlan, ClockConfig } from './types';
+import { describe, it, expect } from "vitest";
+import { generateAdvancedClock } from "./timeline";
+import type { BatchPlan, ClockConfig } from "./model";
 
 function assertBlocksWithinBounds(timeline: ReturnType<typeof generateAdvancedClock>) {
-  timeline.blocks.forEach(block => {
+  timeline.blocks.forEach((block) => {
     expect(block.start).toBeGreaterThanOrEqual(0);
     expect(block.start).toBeLessThan(timeline.duration);
   });
 }
 
-describe('Timeline Generator', () => {
-  it('schedules simple 1:1 recipes perfectly (Input anticipates, Output extracts after craft)', () => {
+describe("Timeline Generator", () => {
+  it("schedules simple 1:1 recipes perfectly (Input anticipates, Output extracts after craft)", () => {
     // A slow machine doing a 1-to-1 craft. 16 crafts = 960 ticks.
     const mockBatch: BatchPlan = {
       craftsPerCycle: 16,
       durationTicks: 960, // 16 crafts * 60 ticks
-      timings: { actualCraftingSpeed: 1, productivityBonus: 0, singleCraftTicks: 60, craftsPerSecond: 1, overloadMultiplier: 2 },
-      inputs: { 'iron-plate': { totalAmount: 16, baseAmount: 1 } },
-      outputs: { 'iron-gear-wheel': { totalAmount: 16, baseAmount: 1, yieldPerCraft: 1, outputBlockLimit: 2 } }
+      timings: {
+        actualCraftingSpeed: 1,
+        productivityBonus: 0,
+        singleCraftTicks: 60,
+        craftsPerSecond: 1,
+        overloadMultiplier: 2,
+      },
+      inputs: { "iron-plate": { totalAmount: 16, baseAmount: 1 } },
+      outputs: { "iron-gear-wheel": { totalAmount: 16, baseAmount: 1, yieldPerCraft: 1, outputBlockLimit: 2 } },
     };
 
     const mockConfig: ClockConfig = {
-      inputs: { 'iron-plate': { presetId: 'chest', stackSize: 16, swingTicks: 8, inserterId: 'in' } },
-      outputs: { 'iron-gear-wheel': { presetId: 'chest', stackSize: 16, swingTicks: 8, inserterId: 'out' } },
-      machineCount: 1
+      inputs: { "iron-plate": { presetId: "chest", stackSize: 16, swingTicks: 8, inserterId: "in" } },
+      outputs: { "iron-gear-wheel": { presetId: "chest", stackSize: 16, swingTicks: 8, inserterId: "out" } },
+      machineCount: 1,
     };
 
     const timeline = generateAdvancedClock(mockBatch, mockConfig);
 
     expect(timeline.duration).toBe(960);
-    
+
     // Should generate exactly 1 input block and 1 output block (1 swing of 16 each)
     expect(timeline.blocks.length).toBe(2);
-    const inBlock = timeline.blocks.find(b => b.rowId === 'row-in');
-    const outBlock = timeline.blocks.find(b => b.rowId === 'row-out');
+    const inBlock = timeline.blocks.find((b) => b.rowId === "row-in");
+    const outBlock = timeline.blocks.find((b) => b.rowId === "row-out");
 
     expect(inBlock).toBeDefined();
     expect(outBlock).toBeDefined();
 
-    // Input math: currentArrivalTick = 0. startTick = 0 - 8(swing) = -8. 
+    // Input math: currentArrivalTick = 0. startTick = 0 - 8(swing) = -8.
     // Wrapped to cycle: 960 - 8 = 952.
-    // This is perfect: the inserter starts swinging 8 ticks BEFORE the cycle technically starts, 
+    // This is perfect: the inserter starts swinging 8 ticks BEFORE the cycle technically starts,
     // so the items hit the machine exactly at Tick 0.
     expect(inBlock!.start).toBe(952);
     expect(inBlock!.repeat).toBe(1);
@@ -53,32 +59,38 @@ describe('Timeline Generator', () => {
 
     assertBlocksWithinBounds(timeline);
   });
-  it('schedules mixed belts on the same row without overlapping', () => {
+  it("schedules mixed belts on the same row without overlapping", () => {
     const mockBatch: BatchPlan = {
       craftsPerCycle: 16,
       durationTicks: 400,
-      timings: { actualCraftingSpeed: 1, productivityBonus: 0, singleCraftTicks: 25, craftsPerSecond: 2.4, overloadMultiplier: 100 },
+      timings: {
+        actualCraftingSpeed: 1,
+        productivityBonus: 0,
+        singleCraftTicks: 25,
+        craftsPerSecond: 2.4,
+        overloadMultiplier: 100,
+      },
       inputs: {
-        'iron-plate': { totalAmount: 16, baseAmount: 1 },
-        'copper-plate': { totalAmount: 16, baseAmount: 1 }
+        "iron-plate": { totalAmount: 16, baseAmount: 1 },
+        "copper-plate": { totalAmount: 16, baseAmount: 1 },
       },
-      outputs: {}
+      outputs: {},
     };
 
     // Both inputs assigned to "in-1" (Mixed Belt)
     const mockConfig: ClockConfig = {
       inputs: {
-        'iron-plate': { presetId: 'belt', stackSize: 16, swingTicks: 12, inserterId: 'in-1' },
-        'copper-plate': { presetId: 'belt', stackSize: 16, swingTicks: 12, inserterId: 'in-1' }
+        "iron-plate": { presetId: "belt", stackSize: 16, swingTicks: 12, inserterId: "in-1" },
+        "copper-plate": { presetId: "belt", stackSize: 16, swingTicks: 12, inserterId: "in-1" },
       },
-      outputs: {}
+      outputs: {},
     };
 
     const timeline = generateAdvancedClock(mockBatch, mockConfig);
 
     // Should only create 1 input row since they share 'in-1'
     expect(timeline.rows.length).toBe(1);
-    expect(timeline.rows[0].id).toBe('row-in-1');
+    expect(timeline.rows[0].id).toBe("row-in-1");
     expect(timeline.rows[0].signals).toHaveLength(2); // Row has both iron and copper signals
 
     // Should create 2 blocks (one for iron, one for copper)
@@ -87,65 +99,77 @@ describe('Timeline Generator', () => {
     // Verify they do not overlap
     const block1 = timeline.blocks[0];
     const block2 = timeline.blocks[1];
-    
+
     const block1End = block1.start + block1.duration;
     // Block 2 must start AFTER Block 1 finishes its swing
     expect(block2.start).toBeGreaterThanOrEqual(block1End);
     assertBlocksWithinBounds(timeline);
   });
-  it('groups swings into massive bursts when the overload limit is huge', () => {
+  it("groups swings into massive bursts when the overload limit is huge", () => {
     // Fast machine, massive buffer (100). Batch cycle is 4 full swings of 16 (64 total).
     const mockBatch: BatchPlan = {
       craftsPerCycle: 64,
       durationTicks: 640, // 64 crafts * 10 ticks
-      timings: { actualCraftingSpeed: 6, productivityBonus: 0, singleCraftTicks: 10, craftsPerSecond: 6, overloadMultiplier: 100 },
-      inputs: { 'iron-plate': { totalAmount: 64, baseAmount: 1 } },
-      outputs: { 'iron-gear-wheel': { totalAmount: 64, baseAmount: 1, yieldPerCraft: 1, outputBlockLimit: 100 } }
+      timings: {
+        actualCraftingSpeed: 6,
+        productivityBonus: 0,
+        singleCraftTicks: 10,
+        craftsPerSecond: 6,
+        overloadMultiplier: 100,
+      },
+      inputs: { "iron-plate": { totalAmount: 64, baseAmount: 1 } },
+      outputs: { "iron-gear-wheel": { totalAmount: 64, baseAmount: 1, yieldPerCraft: 1, outputBlockLimit: 100 } },
     };
 
     const mockConfig: ClockConfig = {
-      inputs: { 'iron-plate': { presetId: 'chest', stackSize: 16, swingTicks: 8, inserterId: 'in' } },
-      outputs: { 'iron-gear-wheel': { presetId: 'chest', stackSize: 16, swingTicks: 8, inserterId: 'out' } },
-      machineCount: 1
+      inputs: { "iron-plate": { presetId: "chest", stackSize: 16, swingTicks: 8, inserterId: "in" } },
+      outputs: { "iron-gear-wheel": { presetId: "chest", stackSize: 16, swingTicks: 8, inserterId: "out" } },
+      machineCount: 1,
     };
 
     const timeline = generateAdvancedClock(mockBatch, mockConfig);
 
-    const inBlock = timeline.blocks.find(b => b.rowId === 'row-in');
-    
+    const inBlock = timeline.blocks.find((b) => b.rowId === "row-in");
+
     // Because the overload multiplier is 100, the machine can hold all 64 items at once.
     // The generator should condense all 4 swings into a single Block with repeat = 4!
-    expect(timeline.blocks.filter(b => b.rowId === 'row-in').length).toBe(1);
+    expect(timeline.blocks.filter((b) => b.rowId === "row-in").length).toBe(1);
     expect(inBlock!.repeat).toBe(4);
-    
+
     assertBlocksWithinBounds(timeline);
   });
-  it('schedules mixed belts safely without overlapping swings', () => {
+  it("schedules mixed belts safely without overlapping swings", () => {
     const mockBatch: BatchPlan = {
       craftsPerCycle: 16,
       durationTicks: 400,
-      timings: { actualCraftingSpeed: 1, productivityBonus: 0, singleCraftTicks: 25, craftsPerSecond: 2.4, overloadMultiplier: 100 },
+      timings: {
+        actualCraftingSpeed: 1,
+        productivityBonus: 0,
+        singleCraftTicks: 25,
+        craftsPerSecond: 2.4,
+        overloadMultiplier: 100,
+      },
       inputs: {
-        'iron-plate': { totalAmount: 16, baseAmount: 1 },
-        'copper-plate': { totalAmount: 16, baseAmount: 1 }
+        "iron-plate": { totalAmount: 16, baseAmount: 1 },
+        "copper-plate": { totalAmount: 16, baseAmount: 1 },
       },
-      outputs: {}
+      outputs: {},
     };
 
     // Both inputs assigned to "in-1" (Mixed Belt)
     const mockConfig: ClockConfig = {
       inputs: {
-        'iron-plate': { presetId: 'belt', stackSize: 16, swingTicks: 12, inserterId: 'in-1' },
-        'copper-plate': { presetId: 'belt', stackSize: 16, swingTicks: 12, inserterId: 'in-1' }
+        "iron-plate": { presetId: "belt", stackSize: 16, swingTicks: 12, inserterId: "in-1" },
+        "copper-plate": { presetId: "belt", stackSize: 16, swingTicks: 12, inserterId: "in-1" },
       },
-      outputs: {}
+      outputs: {},
     };
 
     const timeline = generateAdvancedClock(mockBatch, mockConfig);
 
     // Should only create 1 input row since they share 'in-1'
     expect(timeline.rows.length).toBe(1);
-    expect(timeline.rows[0].id).toBe('row-in-1');
+    expect(timeline.rows[0].id).toBe("row-in-1");
     expect(timeline.rows[0].signals).toHaveLength(2); // Row has both iron and copper signals
 
     // Should create 2 blocks (one for iron, one for copper)
@@ -154,11 +178,11 @@ describe('Timeline Generator', () => {
     // Verify they do not overlap
     const block1 = timeline.blocks[0];
     const block2 = timeline.blocks[1];
-    
-    const block1End = block1.start + (block1.duration * block1.repeat);
+
+    const block1End = block1.start + block1.duration * block1.repeat;
     // Block 2 must start AFTER Block 1 finishes its swing, proving the busy-wait tracker works!
     expect(block2.start).toBeGreaterThanOrEqual(block1End);
 
     assertBlocksWithinBounds(timeline);
   });
-});
+});

+ 8 - 8
src/engine/timeline.ts

@@ -1,9 +1,9 @@
 import { defaultClockSignal } from "../store/useClockStore";
-import type { BatchPlan, ClockConfig } from "./types";
+import type { BatchPlan, ClockConfig } from "./model";
 
 /**
  * Translates an optimized batch plan into a precise sequence of inserter swings (ClockBlocks).
- * It simulates the machine's virtual inventory to group swings into bursts safely, 
+ * It simulates the machine's virtual inventory to group swings into bursts safely,
  * and uses a busy-wait tracker to schedule multiple ingredients safely onto mixed belts.
  */
 export function generateAdvancedClock(batch: BatchPlan, config: ClockConfig) {
@@ -55,20 +55,20 @@ export function generateAdvancedClock(batch: BatchPlan, config: ClockConfig) {
     while (swingsLeft > 0) {
       const burst = Math.min(swingsLeft, maxBurst);
       let startTick = Math.round(currentArrivalTick - cfg.swingTicks);
-      
+
       // Ensure positive tick space before tracking busy state
       startTick = ((startTick % batch.durationTicks) + batch.durationTicks) % batch.durationTicks;
 
       // Mixed Belt Scheduling: Wait if the inserter is busy
       const busyUntil = inserterBusyUntil[inserterId] || 0;
       if (startTick < busyUntil) {
-        startTick = busyUntil; 
+        startTick = busyUntil;
       }
 
       const duration = cfg.swingTicks; // FIXED: No more +1
-      
+
       // Track chronological busy time (can exceed durationTicks chronologically)
-      inserterBusyUntil[inserterId] = startTick + (burst * duration); 
+      inserterBusyUntil[inserterId] = startTick + burst * duration;
 
       const safeStartTick = startTick % batch.durationTicks;
 
@@ -76,7 +76,7 @@ export function generateAdvancedClock(batch: BatchPlan, config: ClockConfig) {
         id: `block-in-${itemId}-${blockIndex++}`,
         rowId,
         presetId: cfg.presetId,
-        start: safeStartTick, 
+        start: safeStartTick,
         duration,
         count: cfg.stackSize,
         repeat: burst,
@@ -137,4 +137,4 @@ export function generateAdvancedClock(batch: BatchPlan, config: ClockConfig) {
     rows: Object.values(rowMap),
     blocks,
   };
-}
+}

+ 14 - 10
src/hooks/useTimelineDrag.ts

@@ -1,9 +1,10 @@
 import { useRef, useCallback } from "react";
 import { useClockStore } from "../store/useClockStore";
-import type { ClockBlock } from "../assets/types";
+import type { ClockBlock } from "../assets/ClockTimeline/model";
 
 export function useTimelineDrag() {
-  const { duration, blocks, moveBlocks, updateBlock, selectBlocks, selectedBlockIds, setAlignmentTick } = useClockStore();
+  const { duration, blocks, moveBlocks, updateBlock, selectBlocks, selectedBlockIds, setAlignmentTick } =
+    useClockStore();
 
   const dragState = useRef<{
     blockId: string;
@@ -16,21 +17,24 @@ export function useTimelineDrag() {
     origDuration: number;
   } | null>(null);
 
-  const pxToTick = useCallback((laneWidth: number, px: number) => 
-    Math.round((px / laneWidth) * duration), 
-  [duration]);
+  const pxToTick = useCallback((laneWidth: number, px: number) => Math.round((px / laneWidth) * duration), [duration]);
 
-  const onPointerDownBlock = (e: React.PointerEvent, block: ClockBlock, mode: "move" | "resize", laneElement: HTMLElement | null) => {
+  const onPointerDownBlock = (
+    e: React.PointerEvent,
+    block: ClockBlock,
+    mode: "move" | "resize",
+    laneElement: HTMLElement | null,
+  ) => {
     e.stopPropagation();
     if (!laneElement) return;
     (e.target as HTMLElement).setPointerCapture(e.pointerId);
 
     const wasSelected = selectedBlockIds.has(block.id);
     const groupIds = mode === "move" && wasSelected ? selectedBlockIds : new Set([block.id]);
-    
+
     // Convert object to map for quick lookup
     const blockValues = Object.values(blocks);
-    
+
     dragState.current = {
       blockId: block.id,
       mode,
@@ -46,7 +50,7 @@ export function useTimelineDrag() {
   const onPointerMove = (e: React.PointerEvent) => {
     const drag = dragState.current;
     if (!drag) return;
-    
+
     const deltaTicks = pxToTick(drag.laneWidth, e.clientX - drag.startX);
 
     if (drag.mode === "resize") {
@@ -100,4 +104,4 @@ export function useTimelineDrag() {
   };
 
   return { onPointerDownBlock, onPointerMove, onPointerUp, pxToTick };
-}
+}

+ 1 - 1
src/store/useClockStore.ts

@@ -1,5 +1,5 @@
 import { create } from "zustand";
-import type { ClockBlock, ClockRow, Signal } from "../assets/types";
+import type { ClockBlock, ClockRow, Signal } from "../assets/ClockTimeline/model";
 
 export const defaultClockSignal: Signal = {
   type: "virtual-signal",