Ryan Malloy de7a29c69e Add auto-schematic generation from SPICE netlists
Parse netlists into component graphs and render circuit diagrams via
SchemDraw. Two layout strategies: loop layout for simple 2-terminal
circuits (RC, RL, voltage divider) and labeled grid for complex
circuits with active devices (BJT amplifiers, MOSFET).

Backend: netlist parser, schematic engine, POST API endpoint.
Frontend: SchematicViewer with zoom/download, stacked cell layout
showing schematic + SPICE editor + waveform simultaneously.
2026-02-13 06:07:30 -07:00

144 lines
4.3 KiB
TypeScript

import { useCallback } from 'react';
import type { Cell } from '../../../lib/types';
import type { WaveformData } from '../../../lib/types';
import { useNotebookStore } from '../../../lib/notebook-store';
import { CellToolbar } from '../toolbar/CellToolbar';
import { SpiceEditor } from '../editor/SpiceEditor';
import { WaveformViewer } from '../output/WaveformViewer';
import { SchematicViewer } from '../output/SchematicViewer';
import { SimulationLog } from '../output/SimulationLog';
import { cn } from '../../../lib/cn';
interface SpiceCellProps {
cell: Cell;
isFirst: boolean;
isLast: boolean;
}
export function SpiceCell({ cell, isFirst, isLast }: SpiceCellProps) {
const {
activeCell,
runningCells,
setActiveCell,
updateCellSource,
deleteCell,
moveCell,
runCell,
generateSchematic,
} = useNotebookStore();
const isActive = activeCell === cell.id;
const isRunning = runningCells.has(cell.id);
const handleChange = useCallback(
(value: string) => {
updateCellSource(cell.id, value);
},
[cell.id, updateCellSource],
);
const handleRun = useCallback(() => {
runCell(cell.id);
}, [cell.id, runCell]);
const handleGenerateSchematic = useCallback(() => {
generateSchematic(cell.id);
}, [cell.id, generateSchematic]);
// Extract simulation output
const simOutput = cell.outputs.find(
(o) => o.output_type === 'simulation_result' || o.output_type === 'error',
);
const outputData = simOutput?.data as {
success?: boolean;
waveform?: WaveformData | null;
log?: string;
error?: string | null;
elapsed_seconds?: number;
} | null;
// Extract schematic output
const schematicOutput = cell.outputs.find((o) => o.output_type === 'schematic');
const schematicSvg = schematicOutput?.data?.svg as string | null;
return (
<div
className={cn(
'rounded-lg border transition-colors overflow-hidden',
isActive
? 'border-blue-500/50 ring-1 ring-blue-500/20'
: 'border-slate-700/50 hover:border-slate-600',
)}
onClick={() => setActiveCell(cell.id)}
>
<CellToolbar
cellId={cell.id}
cellType="spice"
isRunning={isRunning}
isFirst={isFirst}
isLast={isLast}
onRun={handleRun}
onGenerateSchematic={handleGenerateSchematic}
onDelete={() => deleteCell(cell.id)}
onMoveUp={() => moveCell(cell.id, 'up')}
onMoveDown={() => moveCell(cell.id, 'down')}
/>
{/* Schematic — shown above the editor when generated */}
{schematicSvg && (
<div className="border-b border-slate-700/50">
<SchematicViewer svg={schematicSvg} />
</div>
)}
{/* SPICE Editor — always visible */}
<div className="min-h-[80px]">
<SpiceEditor
value={cell.source}
onChange={handleChange}
onRun={handleRun}
/>
</div>
{/* Running indicator */}
{isRunning && (
<div className="border-t border-slate-700/50 px-4 py-3">
<div className="flex items-center gap-2 text-sm text-blue-400">
<div className="w-4 h-4 border-2 border-blue-400/30 border-t-blue-400 rounded-full animate-spin" />
Processing...
</div>
</div>
)}
{/* Waveform + Simulation results — shown below the editor */}
{!isRunning && outputData && (
<div className="border-t border-slate-700/50">
{/* Error banner */}
{!outputData.success && outputData.error && (
<div className="px-4 py-3 bg-red-600/10 border-b border-red-500/20">
<div className="text-sm text-red-400 font-mono whitespace-pre-wrap">
{outputData.error}
</div>
</div>
)}
{/* Waveform plot */}
{outputData.waveform && (
<div className="p-3 bg-slate-900/50">
<WaveformViewer waveform={outputData.waveform} />
</div>
)}
{/* Simulation log */}
<SimulationLog
log={outputData.log || ''}
error={outputData.success ? null : (outputData.error ?? null)}
elapsedSeconds={outputData.elapsed_seconds || 0}
defaultOpen={!outputData.success}
/>
</div>
)}
</div>
);
}