import React, { useState, useEffect } from 'react';
import { useEngine } from './hooks/useEngine';
import { Ladder } from './components/Ladder';
import { Toolbar } from './components/Toolbar';
import { ConfigPanel } from './components/ConfigPanel';
import { ProgramManager } from './components/ProgramManager';
import type { RungElement, Program } from '../engine/types';
const App: React.FC = () => {
const { state, engine, program, loadProgram, start, stop, newProgram } = useEngine();
// DEMO: Load a program with branching on mount
useEffect(() => {
if (!program) {
const demoProgram: Program = {
name: 'Branch Demo',
cycleTime: 100,
rungs: [
// Rung 1: Simple series (comparison)
{
id: 'rung-demo-1',
enabled: true,
series: [
{ type: 'contact' as const, contactType: 'NO' as const, address: 'I:0/0' },
{ type: 'coil' as const, coilType: 'OTE' as const, address: 'Q:0/0' },
],
},
// Rung 2: OR branch (3 paths) — any path energized powers the coil
{
id: 'rung-demo-2',
enabled: true,
series: [
{ type: 'contact' as const, contactType: 'NO' as const, address: 'I:0/1' },
{
type: 'branch' as const,
id: 'branch-or-1',
logic: 'OR',
paths: [
// Path 1: single contact → coil
[
{ type: 'contact' as const, contactType: 'NO' as const, address: 'I:0/2' },
{ type: 'coil' as const, coilType: 'OTE' as const, address: 'Q:0/1' },
],
// Path 2: two contacts in series → coil
[
{ type: 'contact' as const, contactType: 'NC' as const, address: 'I:0/3' },
{ type: 'contact' as const, contactType: 'NO' as const, address: 'I:0/4' },
{ type: 'coil' as const, coilType: 'OTE' as const, address: 'Q:0/2' },
],
// Path 3: single NO contact → coil
[
{ type: 'contact' as const, contactType: 'NO' as const, address: 'I:0/5' },
{ type: 'coil' as const, coilType: 'OTE' as const, address: 'Q:0/3' },
],
],
},
],
},
// Rung 3: AND branch (2 paths) — both must be energized
{
id: 'rung-demo-3',
enabled: true,
series: [
{
type: 'branch' as const,
id: 'branch-and-1',
logic: 'AND',
paths: [
[
{ type: 'contact' as const, contactType: 'NO' as const, address: 'I:0/6' },
{ type: 'contact' as const, contactType: 'NO' as const, address: 'I:0/7' },
],
[
{ type: 'contact' as const, contactType: 'NC' as const, address: 'I:0/8' },
],
],
},
{ type: 'coil' as const, coilType: 'OTE' as const, address: 'Q:0/4' },
],
},
// Rung 4: Branch with timer/counter (taller elements)
{
id: 'rung-demo-4',
enabled: true,
series: [
{ type: 'contact' as const, contactType: 'NO' as const, address: 'I:0/9' },
{
type: 'branch' as const,
id: 'branch-or-2',
logic: 'OR',
paths: [
// Path 1: contact → timer
[
{ type: 'contact' as const, contactType: 'NO' as const, address: 'I:1/0' },
{ type: 'timer' as const, timerType: 'TON', instanceId: 'T4:0', preset: 500, accumulated: 0 },
],
// Path 2: contact → counter
[
{ type: 'contact' as const, contactType: 'NC' as const, address: 'I:1/1' },
{ type: 'counter' as const, counterType: 'CTU', instanceId: 'C4:0', preset: 10, current: 0 },
],
],
},
{ type: 'coil' as const, coilType: 'OTE' as const, address: 'Q:0/5' },
],
},
],
};
loadProgram(demoProgram);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const [selectedElement, setSelectedElement] = useState<{ rungIdx: number; elIdx: number; element: RungElement } | null>(null);
const handleElementClick = (rungIdx: number, elIdx: number, element: RungElement) => {
setSelectedElement({ rungIdx, elIdx, element });
};
const handleAddRung = () => {
const newRung = {
id: `rung-${Date.now()}`,
enabled: true,
series: []
};
if (!program) {
const freshProgram: any = {
name: 'Untitled Program',
rungs: [newRung],
cycleTime: 100,
};
loadProgram(freshProgram);
} else {
loadProgram({ ...program, rungs: [...program.rungs, newRung] });
}
};
const handleStart = () => {
if (!program) {
const newRung = {
id: `rung-${Date.now()}`,
enabled: true,
series: []
};
const freshProgram: any = {
name: 'Untitled Program',
rungs: [newRung],
cycleTime: 100,
};
loadProgram(freshProgram);
start();
} else {
start();
}
};
const handleDropElement = (tool: string, rungIdx: number) => {
if (!program) return;
let newElement: any;
if (tool.startsWith('contact-')) {
newElement = { type: 'contact', contactType: tool.endsWith('no') ? 'NO' : 'NC', address: 'I:0.0' };
} else if (tool.startsWith('coil-')) {
const coilSuffix = tool.replace('coil-', '');
newElement = { type: 'coil', coilType: coilSuffix.toUpperCase(), address: 'Q:0.0' };
} else if (tool.startsWith('timer-')) {
const timerSuffix = tool.replace('timer-', '').toUpperCase();
newElement = { type: 'timer', timerType: timerSuffix, instanceId: `timer-${Date.now()}`, preset: 1000, accumulated: 0 };
} else if (tool.startsWith('counter-')) {
const counterSuffix = tool.replace('counter-', '').toUpperCase();
newElement = { type: 'counter', counterType: counterSuffix, instanceId: `counter-${Date.now()}`, preset: 10, current: 0 };
} else if (tool.startsWith('gate-')) {
const gateSuffix = tool.replace('gate-', '').toUpperCase();
newElement = { type: 'gate', gateType: gateSuffix, inputs: [], outputAddress: 'Q:0.1' };
} else if (tool.startsWith('math-')) {
const mathSuffix = tool.replace('math-', '').toUpperCase();
newElement = { type: 'math', operator: mathSuffix, inputA: 'I:0.0', inputB: 'I:0.1', outputAddress: 'Q:0.2' };
} else if (tool.startsWith('branch-')) {
const branchLogic = tool.replace('branch-', '').toUpperCase();
newElement = { type: 'branch', id: `branch-${Date.now()}`, logic: branchLogic === 'AND' || branchLogic === 'OR' ? branchLogic : 'AND', paths: [[]] };
} else if (tool === 'noop') {
newElement = { type: 'noop' };
} else if (tool === 'move') {
newElement = { type: 'move', source: 'I:0.0', destination: 'Q:0.0' };
} else if (tool === 'scale') {
newElement = { type: 'scale', inputAddress: 'I:0.0', destination: 'Q:0.0', inMin: 0, inMax: 100, outMin: 0, outMax: 1000 };
} else if (tool === 'compare') {
newElement = { type: 'compare', op: '==', inputA: 'I:0.0', inputB: 'I:0.1', outputAddress: 'Q:0.0' };
} else if (tool === 'oneshot-rising' || tool === 'oneshot-falling') {
const edgeType = tool.endsWith('rising') ? 'RISING' : 'FALLING';
newElement = { type: 'oneshot', address: 'I:0.0', edgeType };
}
if (newElement) {
const newRungs = program.rungs.map((rung, i) => {
if (i === rungIdx) {
const updatedSeries = [...rung.series, newElement];
// Ensure coil is last if it's the only coil
const coilIdx = updatedSeries.findIndex(el => el.type === 'coil');
if (coilIdx !== -1 && coilIdx !== updatedSeries.length - 1) {
const newSeries = [...updatedSeries];
const [coil] = newSeries.splice(coilIdx, 1);
newSeries.push(coil);
return { ...rung, series: newSeries };
}
return { ...rung, series: updatedSeries };
}
return rung;
});
loadProgram({ ...program, rungs: newRungs });
}
};
const handleMoveElement = (fromRungIdx: number, fromElIdx: number, toRungIdx: number, toElIdx: number) => {
if (!program) return;
const newRungs = program.rungs.map((rung, i) => ({ ...rung, series: [...rung.series] }));
const elementToMove = newRungs[fromRungIdx].series[fromElIdx];
// Remove from old rung
newRungs[fromRungIdx].series.splice(fromElIdx, 1);
// Add to new rung at specific position
newRungs[toRungIdx].series.splice(toElIdx, 0, elementToMove);
loadProgram({ ...program, rungs: newRungs });
};
const handleUpdateElement = (updated: RungElement) => {
if (!selectedElement || !program) return;
const newRungs = program.rungs.map((rung, i) => {
if (i === selectedElement.rungIdx) {
const newSeries = [...rung.series];
newSeries[selectedElement.elIdx] = updated;
return { ...rung, series: newSeries };
}
return rung;
});
loadProgram({ ...program, rungs: newRungs });
};
const handleDeleteElement = () => {
if (!selectedElement || !program) return;
const newRungs = program.rungs.map((rung, i) => {
if (i === selectedElement.rungIdx) {
const newSeries = rung.series.filter((_, elIdx) => elIdx !== selectedElement.elIdx);
return { ...rung, series: newSeries };
}
return rung;
});
loadProgram({ ...program, rungs: newRungs });
setSelectedElement(null);
};
const isRunning = state.running;
return (
<div className="app-container">
<header className="header">
<h1>Lego Ladder</h1>
<div className="header-controls">
<ProgramManager />
<button className="btn" onClick={handleAddRung}>+ Add Rung</button>
{isRunning ? (
<button className="btn btn-danger" onClick={stop}>■ Stop</button>
) : (
<button className="btn btn-success" onClick={handleStart}>▶ Start</button>
)}
</div>
</header>
<main className="main">
<Toolbar onToolClick={(tool) => handleDropElement(tool, selectedElement ? selectedElement.rungIdx : 0)} />
<section className="canvas-container">
{program ? (
<Ladder
program={program}
engineState={state}
onElementClick={handleElementClick}
onDropElement={handleDropElement}
onMoveElement={handleMoveElement}
/>
) : (
<div className="no-program">
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '48px', marginBottom: '16px', opacity: 0.3 }}>⚡</div>
<div>No program loaded</div>
<div style={{ fontSize: '12px', marginTop: '8px', color: 'var(--text-muted)' }}>
Click "▶ Start" to create a new program or open an existing one
</div>
</div>
</div>
)}
</section>
<aside className="config-panel" style={{ display: selectedElement ? 'flex' : 'none' }}>
{selectedElement && (
<ConfigPanel
element={selectedElement.element}
onUpdate={handleUpdateElement}
onDelete={handleDeleteElement}
onClose={() => setSelectedElement(null)}
/>
)}
</aside>
</main>
<footer className="footer">
<span className={`status ${isRunning ? 'running' : ''}`}>
Status: {isRunning ? 'Running' : 'Stopped'}
</span>
<span className="cycle">Cycle: {state.cycle}</span>
</footer>
</div>
);
};
export default App;