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.
144 lines
4.3 KiB
TypeScript
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>
|
|
);
|
|
}
|