2025-12-21 00:31:26 +02:00
/* nets.js - Netlist Management (v2) */
class NetManager {
constructor ( db ) {
this . db = db ;
}
2025-12-21 18:45:40 +02:00
async render ( ) {
const tbody = document . getElementById ( 'nets-body' ) ;
if ( ! tbody ) return ;
// Safety check: If no board is loaded, clear the table
if ( typeof currentBomId === 'undefined' || ! currentBomId ) {
tbody . innerHTML = '' ;
return ;
}
tbody . innerHTML = '<tr><td colspan="3" style="text-align:center; color:#888;">Loading...</td></tr>' ;
const allNets = await this . db . getNets ( ) ;
// --- FIX: Filter by Current Board ID ---
const nets = allNets . filter ( n => n . projectId === currentBomId ) ;
tbody . innerHTML = '' ;
if ( nets . length === 0 ) {
tbody . innerHTML = '<tr><td colspan="3" style="text-align:center; padding:2rem; color:#94a3b8;">No nets defined. Go to "Inspect" to create one.</td></tr>' ;
return ;
}
nets . forEach ( net => {
const tr = document . createElement ( 'tr' ) ;
tr . style . height = 'auto' ;
tr . style . minHeight = '2.2rem' ;
// Target Icon for Editing
const targetIcon = ` <span style="cursor:pointer; margin-right:0.5rem;" onclick="netManager.editNet(' ${ net . id } ')" title="Edit Net on Board">🎯</span> ` ;
let nodesHtml = '' ;
net . nodes . forEach ( ( n , idx ) => {
nodesHtml += ` <span class="net-chip" onclick="netManager.editNode(' ${ net . id } ', ${ idx } )" title="Edit Node"> ${ n . label } </span> ` ;
} ) ;
tr . innerHTML = `
< td style = "display:flex; align-items:center; vertical-align:top; height:auto;" >
$ { targetIcon }
< input type = "text" value = "${net.name}" onchange = "netManager.rename('${net.id}', this.value)" style = "border:none; background:transparent; font-weight:bold; flex:1; min-width:0;" >
< / t d >
< td style = "white-space:normal; height:auto; overflow:visible;" >
< div style = "display:flex; flex-wrap:wrap; gap:4px; padding:2px 0;" > $ { nodesHtml } < / d i v >
< / t d >
< td style = "text-align:right; vertical-align:top; height:auto;" >
< button class = "danger sm-btn" onclick = "netManager.delete('${net.id}')" title = "Delete Net" > 🗑 ️ < / b u t t o n >
< / t d >
` ;
tbody . appendChild ( tr ) ;
} ) ;
}
// Edit Net in Inspector
async editNet ( id ) {
const net = await this . db . _tx ( 'nets' , 'readonly' , s => s . get ( id ) ) ;
if ( net ) {
switchView ( 'inspect' ) ;
// We use the global inspector instance
if ( window . inspector ) window . inspector . loadNet ( net ) ;
}
}
2025-12-21 00:31:26 +02:00
// Edit individual node
async editNode ( netId , nodeIdx ) {
const net = await this . db . _tx ( 'nets' , 'readonly' , s => s . get ( netId ) ) ;
if ( ! net || ! net . nodes [ nodeIdx ] ) return ;
const node = net . nodes [ nodeIdx ] ;
2025-12-21 18:45:40 +02:00
const res = await requestInput ( "Edit Node" , "Node Name" , node . label , {
2025-12-26 14:55:08 +02:00
extraBtn : { label : 'Delete' , value : '__DELETE__' , class : 'danger' } ,
helpHtml : PIN _HELP _HTML
2025-12-21 18:45:40 +02:00
} ) ;
if ( res === '__DELETE__' ) {
net . nodes . splice ( nodeIdx , 1 ) ;
await this . db . addNet ( net ) ;
this . render ( ) ;
} else if ( res ) {
net . nodes [ nodeIdx ] . label = res ;
2025-12-21 00:31:26 +02:00
await this . db . addNet ( net ) ;
this . render ( ) ;
}
}
async rename ( id , newName ) {
if ( ! newName . trim ( ) ) return ;
const net = await this . db . _tx ( 'nets' , 'readonly' , s => s . get ( id ) ) ;
if ( net ) {
net . name = newName . trim ( ) ;
await this . db . addNet ( net ) ;
}
}
async delete ( id ) {
if ( await confirmAction ( "Delete this net?" , "Delete" ) ) {
await this . db . deleteNet ( id ) ;
this . render ( ) ;
}
}
async exportKiCad ( ) {
// 1. Determine Filename
let filename = "board_netlist" ;
if ( typeof currentBomId !== 'undefined' && typeof bomList !== 'undefined' ) {
const meta = bomList . find ( b => b . id === currentBomId ) ;
if ( meta && meta . name ) {
filename = meta . name . replace ( /[^a-z0-9_\-\.]/gi , '_' ) ;
}
}
const nets = await this . db . getNets ( ) ;
const components = ( typeof bomData !== 'undefined' ) ? bomData : [ ] ;
// --- CONFIGURATION: Component Type Mapping ---
// We only map types that are structurally unambiguous (2-pin passives, test points).
// Complex types (Q, U, J) are commented out to prevent incorrect symbol assignment.
const COMPONENT _LIBRARY _MAP = {
2026-04-24 17:28:59 +03:00
'R' : { lib : "Device" , part : "R" , desc : "Resistor" } ,
'C' : { lib : "Device" , part : "C" , desc : "Unpolarized capacitor" } ,
'L' : { lib : "Device" , part : "L" , desc : "Inductor" } ,
'D' : { lib : "Device" , part : "D" , desc : "Diode" } ,
2025-12-21 00:31:26 +02:00
'TP' : { lib : "Connector" , part : "TestPoint" , desc : "Test Point" } ,
2025-12-21 18:45:40 +02:00
2025-12-21 00:31:26 +02:00
// --- AMBIGUOUS TYPES (Disabled by default) ---
2025-12-21 18:45:40 +02:00
// 'Q': { lib: "Device", part: "Q_NPN_BEC", desc: "Transistor NPN" }, // Risk: Could be PNP, MOSFET, IGBT
// 'J': { lib: "Connector", part: "Conn_01x02_Male", desc: "Connector" }, // Risk: Pin count unknown
2025-12-21 00:31:26 +02:00
// 'CN': { lib: "Connector", part: "Conn_01x02_Male", desc: "Connector" }, // Risk: Pin count unknown
} ;
let out = "(export (version D)\n" ;
// 2. Export Components
2026-04-24 17:28:59 +03:00
out += " (components\n" ;
2025-12-21 00:31:26 +02:00
components . forEach ( c => {
const val = c . value ? c . value : "~" ;
const footprint = c . desc ? c . desc . replace ( /"/g , '' ) : "" ;
const tstamp = c . id ? c . id . substring ( 0 , 8 ) : Math . floor ( Math . random ( ) * 10000000 ) . toString ( 16 ) ;
2025-12-21 18:45:40 +02:00
2025-12-21 00:31:26 +02:00
// Detect Type from Prefix
const prefix = ( c . label . match ( /^[A-Z]+/ ) || [ "" ] ) [ 0 ] . toUpperCase ( ) ;
2025-12-21 18:45:40 +02:00
2025-12-21 00:31:26 +02:00
// Lookup Library definition
// Future TODO: Add logic here to check c.desc for keywords like "NPN", "MOSFET", etc.
const def = COMPONENT _LIBRARY _MAP [ prefix ] ;
2025-12-21 18:45:40 +02:00
out += ` (comp (ref " ${ c . label } ") \n ` ;
2026-04-24 17:28:59 +03:00
out += ` (value " ${ val } ") \n ` ;
2025-12-21 18:45:40 +02:00
if ( footprint ) out += ` (footprint " ${ footprint } ") \n ` ;
2025-12-21 00:31:26 +02:00
// Inject Library Source if we have a safe definition
if ( def ) {
2026-04-24 17:28:59 +03:00
out += ` (libsource (lib " ${ def . lib } ") (part " ${ def . part } ") (description " ${ def . desc } ")) \n ` ;
out += ` (property (name "Sheetname") (value "")) (property (name "Sheetfile") (value " ${ filename } .kicad_sch")) \n ` ;
2025-12-21 00:31:26 +02:00
}
2025-12-21 18:45:40 +02:00
2026-04-24 17:28:59 +03:00
out += ` (tstamp " ${ tstamp } ") \n ` ;
2025-12-21 18:45:40 +02:00
out += ` ) \n ` ;
2025-12-21 00:31:26 +02:00
} ) ;
2026-04-24 17:28:59 +03:00
out += " )\n" ;
2025-12-21 00:31:26 +02:00
// 3. Export Nets
2026-04-24 17:28:59 +03:00
out += " (nets\n" ;
2025-12-21 00:31:26 +02:00
nets . forEach ( ( net , i ) => {
2025-12-21 18:45:40 +02:00
out += ` (net (code ${ i + 1 } ) (name " ${ net . name } ") \n ` ;
2025-12-21 00:31:26 +02:00
net . nodes . forEach ( node => {
const parts = node . label . split ( '.' ) ;
if ( parts . length === 2 ) {
// Format: R1.2 (Ref R1, Pin 2)
2026-04-24 17:28:59 +03:00
out += ` (node (ref " ${ parts [ 0 ] } ") (pin " ${ parts [ 1 ] } ")) \n ` ;
2025-12-21 00:31:26 +02:00
} else {
// Fallback: TestPoints or direct names often use Pin 1
2026-04-24 17:28:59 +03:00
out += ` (node (ref " ${ node . label } ") (pin "1")) \n ` ;
2025-12-21 00:31:26 +02:00
}
} ) ;
2025-12-21 18:45:40 +02:00
out += " )\n" ;
2025-12-21 00:31:26 +02:00
} ) ;
2026-04-24 17:28:59 +03:00
out += " )\n)\n" ;
2025-12-21 00:31:26 +02:00
// 4. Download
const blob = new Blob ( [ out ] , { type : 'text/plain' } ) ;
const a = document . createElement ( 'a' ) ;
a . href = URL . createObjectURL ( blob ) ;
a . download = ` ${ filename } .net ` ;
document . body . appendChild ( a ) ; a . click ( ) ; document . body . removeChild ( a ) ;
}
2026-04-24 17:28:59 +03:00
async exportSpice ( ) {
let filename = "board_netlist" ;
if ( typeof currentBomId !== 'undefined' && typeof bomList !== 'undefined' ) {
const meta = bomList . find ( b => b . id === currentBomId ) ;
if ( meta && meta . name ) {
filename = meta . name . replace ( /[^a-z0-9_\-\.]/gi , '_' ) ;
}
}
const nets = await this . db . getNets ( ) ;
const components = ( typeof bomData !== 'undefined' ) ? bomData : [ ] ;
let out = "* SPICE Netlist generated by PCB ReTrace\n" ;
components . forEach ( c => {
const val = c . value ? c . value . replace ( /\s+/g , '_' ) : "1k" ;
const ref = c . label ;
const pinToNet = { } ;
let maxPin = 0 ;
nets . forEach ( net => {
net . nodes . forEach ( node => {
const parts = node . label . split ( '.' ) ;
if ( parts [ 0 ] === ref ) {
const pinNum = parseInt ( parts [ 1 ] ) || 1 ;
pinToNet [ pinNum ] = net . name ;
if ( pinNum > maxPin ) maxPin = pinNum ;
} else if ( node . label === ref ) {
pinToNet [ 1 ] = net . name ;
if ( 1 > maxPin ) maxPin = 1 ;
}
} ) ;
} ) ;
let netString = "" ;
const pinCount = maxPin > 0 ? maxPin : 2 ;
for ( let i = 1 ; i <= pinCount ; i ++ ) {
netString += ( pinToNet [ i ] || ` NC_ ${ ref } _ ${ i } ` ) + " " ;
}
out += ` ${ ref } ${ netString } ${ val } \n ` ;
} ) ;
out += ".end\n" ;
const blob = new Blob ( [ out ] , { type : 'text/plain' } ) ;
const a = document . createElement ( 'a' ) ;
a . href = URL . createObjectURL ( blob ) ;
a . download = ` ${ filename } .cir ` ;
document . body . appendChild ( a ) ; a . click ( ) ; document . body . removeChild ( a ) ;
}
2025-12-21 00:31:26 +02:00
}