1use crate::compiler::component::Component;
8use crate::compiler::components::*;
9use crate::dsl::*;
10use std::fmt::Write;
11
12pub(crate) fn value_str(kind: &dyn Component) -> String {
18 if let Some(rs) = kind.as_any().downcast_ref::<ResistorSwitched>() {
20 let values = &rs.values;
21 if values.is_empty() {
22 return "R_switched".into();
23 }
24 return format!(
25 "R_switched_{}-{}",
26 format_eng(
27 *values
28 .iter()
29 .min_by(|a, b| a.partial_cmp(b).unwrap())
30 .unwrap(),
31 "Ω"
32 ),
33 format_eng(
34 *values
35 .iter()
36 .max_by(|a, b| a.partial_cmp(b).unwrap())
37 .unwrap(),
38 "Ω"
39 )
40 );
41 }
42 if let Some(cs) = kind.as_any().downcast_ref::<CapSwitched>() {
43 let values = &cs.values;
44 if values.is_empty() {
45 return "C_switched".into();
46 }
47 return format!(
48 "C_switched_{}-{}",
49 format_eng(
50 *values
51 .iter()
52 .min_by(|a, b| a.partial_cmp(b).unwrap())
53 .unwrap(),
54 "F"
55 ),
56 format_eng(
57 *values
58 .iter()
59 .max_by(|a, b| a.partial_cmp(b).unwrap())
60 .unwrap(),
61 "F"
62 )
63 );
64 }
65 if let Some(ls) = kind.as_any().downcast_ref::<InductorSwitched>() {
66 let values = &ls.values;
67 if values.is_empty() {
68 return "L_switched".into();
69 }
70 return format!(
71 "L_switched_{}-{}",
72 format_eng(
73 *values
74 .iter()
75 .min_by(|a, b| a.partial_cmp(b).unwrap())
76 .unwrap(),
77 "H"
78 ),
79 format_eng(
80 *values
81 .iter()
82 .max_by(|a, b| a.partial_cmp(b).unwrap())
83 .unwrap(),
84 "H"
85 )
86 );
87 }
88
89 if let Some(tc) = kind.as_any().downcast_ref::<Tempco>() {
91 return format!("{}_Tempco_{:.0}ppm", format_eng(tc.resistance, "Ω"), tc.ppm);
92 }
93
94 if let Some(z) = kind.as_any().downcast_ref::<Zener>() {
96 return format!("{}V_Zener", z.breakdown_voltage);
97 }
98 if let Some(dt) = kind.diode_type() {
99 if kind.as_any().downcast_ref::<DiodePair>().is_some() {
100 return format!("{dt:?}_pair");
101 }
102 return format!("{dt:?}");
103 }
104
105 if let Some(r) = kind.resistance() {
107 return format_eng(r, "Ω");
108 }
109 if let Some(c) = kind.capacitance() {
110 return format_eng(c, "F");
111 }
112 if let Some(l) = kind.inductance() {
113 return format_eng(l, "H");
114 }
115
116 if kind.is_bjt() {
118 if let Some(name) = kind.model_name() {
119 return name.to_string();
120 }
121 }
122
123 if let Some(ot) = kind.op_amp_type() {
125 return match ot {
126 OpAmpType::Generic => "OpAmp".into(),
127 OpAmpType::Tl071 => "TL071".into(),
128 OpAmpType::Tl072 => "TL072".into(),
129 OpAmpType::Tl082 => "TL082".into(),
130 OpAmpType::Jrc4558 => "JRC4558D".into(),
131 OpAmpType::Rc4558 => "RC4558".into(),
132 OpAmpType::Lm308 => "LM308N".into(),
133 OpAmpType::Lm741 => "LM741".into(),
134 OpAmpType::Ne5532 => "NE5532".into(),
135 OpAmpType::Ca3080 => "CA3080".into(),
136 OpAmpType::Op07 => "OP07".into(),
137 };
138 }
139
140 if let Some(jfet) = kind.as_any().downcast_ref::<NJfet>() {
142 return format!("N-JFET_{}", jfet.model);
143 }
144 if let Some(jfet) = kind.as_any().downcast_ref::<PJfet>() {
145 return format!("P-JFET_{}", jfet.model);
146 }
147
148 if let Some(pc) = kind.as_any().downcast_ref::<PhotocouplerComp>() {
150 return format!("Vactrol_{:?}", pc.coupler_type);
151 }
152
153 if let Some(lfo) = kind.as_any().downcast_ref::<Lfo>() {
155 return format!(
156 "LFO_{:?}_R{}_C{}",
157 lfo.waveform,
158 format_eng(lfo.timing_r, ""),
159 format_eng(lfo.timing_c, "")
160 );
161 }
162
163 if let Some(t) = kind.as_any().downcast_ref::<Triode>() {
165 return format!("Triode_{:?}", t.model);
166 }
167 if let Some(vm) = kind.as_any().downcast_ref::<VariMu>() {
168 return format!("VariMu_{}", vm.model);
169 }
170 if let Some(p) = kind.as_any().downcast_ref::<Pentode>() {
171 return format!("Pentode_{:?}", p.model);
172 }
173
174 if let Some(ef) = kind.as_any().downcast_ref::<EnvelopeFollower>() {
176 return format!(
177 "ENV_atk{}/{}_rel{}/{}_sens{}",
178 format_eng(ef.attack_r, ""),
179 format_eng(ef.attack_c, ""),
180 format_eng(ef.release_r, ""),
181 format_eng(ef.release_c, ""),
182 format_eng(ef.sensitivity_r, ""),
183 );
184 }
185
186 if let Some(m) = kind.as_any().downcast_ref::<Nmos>() {
188 return format!("N-MOS_{:?}", m.mosfet_type);
189 }
190 if let Some(m) = kind.as_any().downcast_ref::<Pmos>() {
191 return format!("P-MOS_{:?}", m.mosfet_type);
192 }
193
194 if let Some(b) = kind.as_any().downcast_ref::<Bbd>() {
196 return format!("BBD_{:?}", b.bbd_type);
197 }
198
199 if let Some(dl) = kind.as_any().downcast_ref::<DelayLineComp>() {
201 return format!(
202 "DelayLine_{}-{}_{:?}_{:?}",
203 format_eng(dl.min_delay, "s"),
204 format_eng(dl.max_delay, "s"),
205 dl.interpolation,
206 dl.medium
207 );
208 }
209
210 if let Some(tap) = kind.as_any().downcast_ref::<Tap>() {
212 return format!("Tap_{}_{:.1}x", tap.parent_id, tap.ratio);
213 }
214
215 if let Some(n) = kind.as_any().downcast_ref::<Neon>() {
217 return match n.neon_type {
218 NeonType::Ne2 => "NE-2".into(),
219 NeonType::Ne51 => "NE-51".into(),
220 NeonType::Ne83 => "NE-83".into(),
221 };
222 }
223
224 if let Some(v) = kind.as_any().downcast_ref::<Vco>() {
226 return format!("VCO_{:?}", v.vco_type);
227 }
228 if let Some(v) = kind.as_any().downcast_ref::<Vcf>() {
229 return format!("VCF_{:?}", v.vcf_type);
230 }
231 if let Some(v) = kind.as_any().downcast_ref::<Vca>() {
232 return format!("VCA_{:?}", v.vca_type);
233 }
234 if let Some(c) = kind.as_any().downcast_ref::<Comparator>() {
235 return format!("Comparator_{:?}", c.comp_type);
236 }
237 if let Some(s) = kind.as_any().downcast_ref::<AnalogSwitch>() {
238 return format!("Switch_{:?}", s.switch_type);
239 }
240
241 if let Some(m) = kind.as_any().downcast_ref::<MatchedNpn>() {
243 return format!("Matched_NPN_{:?}", m.matched_type);
244 }
245 if let Some(m) = kind.as_any().downcast_ref::<MatchedPnp>() {
246 return format!("Matched_PNP_{:?}", m.matched_type);
247 }
248
249 if let Some(cfg) = kind.transformer_config() {
251 let pri = match cfg.primary_type {
252 crate::dsl::WindingType::Standard => "",
253 crate::dsl::WindingType::CenterTap => "CT",
254 crate::dsl::WindingType::PushPull => "PP",
255 };
256 let sec = match cfg.secondary_type {
257 crate::dsl::WindingType::Standard => "",
258 crate::dsl::WindingType::CenterTap => "CT",
259 crate::dsl::WindingType::PushPull => "PP",
260 };
261 return format!(
262 "XFMR_{:.1}:1_{}{}_{}H",
263 cfg.turns_ratio,
264 pri,
265 sec,
266 format_eng(cfg.primary_inductance, "")
267 );
268 }
269
270 if let Some(sw) = kind.as_any().downcast_ref::<RotarySwitch>() {
272 return format!("Rotary_{}pos", sw.linked_ids.len());
273 }
274
275 if let Some(sw) = kind.as_any().downcast_ref::<Switch>() {
277 return format!("Switch_{}pos", sw.positions);
278 }
279
280 if kind.as_any().downcast_ref::<TriggerInputComp>().is_some() {
282 return "TriggerInput".into();
283 }
284
285 kind.type_tag().to_string()
287}
288
289pub fn format_eng(val: f64, unit: &str) -> String {
291 if val >= 1e6 {
292 format!("{:.1}M{unit}", val / 1e6)
293 } else if val >= 1e3 {
294 format!("{:.1}k{unit}", val / 1e3)
295 } else if val >= 1.0 {
296 format!("{:.1}{unit}", val)
297 } else if val >= 1e-3 {
298 format!("{:.1}m{unit}", val * 1e3)
299 } else if val >= 1e-6 {
300 format!("{:.1}u{unit}", val * 1e6)
301 } else if val >= 1e-9 {
302 format!("{:.1}n{unit}", val * 1e9)
303 } else {
304 format!("{:.1}p{unit}", val * 1e12)
305 }
306}
307
308fn build_net_map(nets: &[NetDef]) -> Vec<(usize, String, Vec<Pin>)> {
311 let mut groups: Vec<Vec<Pin>> = Vec::new();
314
315 for net in nets {
316 let mut all_pins: Vec<Pin> = vec![net.from.clone()];
317 all_pins.extend(net.to.iter().cloned());
318
319 let mut merge_indices: Vec<usize> = Vec::new();
321 for (i, group) in groups.iter().enumerate() {
322 if all_pins.iter().any(|p| group.contains(p)) {
323 merge_indices.push(i);
324 }
325 }
326
327 if merge_indices.is_empty() {
328 groups.push(all_pins);
329 } else {
330 merge_indices.sort();
332 let target = merge_indices[0];
333 for &idx in merge_indices.iter().skip(1).rev() {
334 let g = groups.remove(idx);
335 groups[target].extend(g);
336 }
337 for p in all_pins {
338 if !groups[target].contains(&p) {
339 groups[target].push(p);
340 }
341 }
342 }
343 }
344
345 groups
347 .into_iter()
348 .enumerate()
349 .map(|(i, pins)| {
350 let net_num = i + 1;
351 let name = pins
353 .iter()
354 .find_map(|p| {
355 if let Pin::Reserved(n) = p {
356 Some(n.clone())
357 } else {
358 None
359 }
360 })
361 .unwrap_or_else(|| format!("Net{net_num}"));
362 (net_num, name, pins)
363 })
364 .collect()
365}
366
367fn pin_to_string(pin: &Pin) -> String {
368 match pin {
369 Pin::Reserved(n) => n.clone(),
370 Pin::ComponentPin { component, pin } => format!("{component}.{pin}"),
371 Pin::Fork {
372 switch,
373 destinations,
374 } => {
375 let dests: Vec<_> = destinations.iter().map(pin_to_string).collect();
376 format!("fork({}, [{}])", switch, dests.join(", "))
377 }
378 Pin::SubcircuitPort { subcircuit, port } => format!("{subcircuit}.{port}"),
379 }
380}
381
382pub fn export_kicad_netlist(pedal: &PedalDef) -> String {
384 let mut out = String::with_capacity(4096);
385
386 writeln!(out, "(export (version D)").unwrap();
387 writeln!(out, " (design").unwrap();
388 writeln!(out, " (source \"{}.pedal\")", pedal.name).unwrap();
389 writeln!(out, " (tool \"PedalKernel DSL\"))").unwrap();
390
391 writeln!(out, " (components").unwrap();
393 for comp in &pedal.components {
394 if let Some(lfo) = comp.kind.as_any().downcast_ref::<Lfo>() {
395 let waveform = &lfo.waveform;
397 writeln!(
399 out,
400 " (comp (ref R_{id}) (value \"{val}\") (libsource (lib \"Device:R\")) (field (name \"Note\") \"LFO {wf:?} timing\"))",
401 id = comp.id, val = format_eng(lfo.timing_r, "Ω"), wf = waveform
402 )
403 .unwrap();
404 writeln!(
406 out,
407 " (comp (ref C_{id}) (value \"{val}\") (libsource (lib \"Device:C\")) (field (name \"Note\") \"LFO {wf:?} timing\"))",
408 id = comp.id, val = format_eng(lfo.timing_c, "F"), wf = waveform
409 )
410 .unwrap();
411 } else if let Some(ef) = comp.kind.as_any().downcast_ref::<EnvelopeFollower>() {
412 writeln!(
415 out,
416 " (comp (ref R_{id}_ATK) (value \"{val}\") (libsource (lib \"Device:R\")) (field (name \"Note\") \"Envelope attack timing\"))",
417 id = comp.id, val = format_eng(ef.attack_r, "Ω")
418 )
419 .unwrap();
420 writeln!(
422 out,
423 " (comp (ref C_{id}_ATK) (value \"{val}\") (libsource (lib \"Device:C\")) (field (name \"Note\") \"Envelope attack timing\"))",
424 id = comp.id, val = format_eng(ef.attack_c, "F")
425 )
426 .unwrap();
427 writeln!(
429 out,
430 " (comp (ref R_{id}_REL) (value \"{val}\") (libsource (lib \"Device:R\")) (field (name \"Note\") \"Envelope release timing\"))",
431 id = comp.id, val = format_eng(ef.release_r, "Ω")
432 )
433 .unwrap();
434 writeln!(
436 out,
437 " (comp (ref C_{id}_REL) (value \"{val}\") (libsource (lib \"Device:C\")) (field (name \"Note\") \"Envelope release timing\"))",
438 id = comp.id, val = format_eng(ef.release_c, "F")
439 )
440 .unwrap();
441 writeln!(
443 out,
444 " (comp (ref R_{id}_SENS) (value \"{val}\") (libsource (lib \"Device:R\")) (field (name \"Note\") \"Envelope sensitivity\"))",
445 id = comp.id, val = format_eng(ef.sensitivity_r, "Ω")
446 )
447 .unwrap();
448 writeln!(
450 out,
451 " (comp (ref D_{id}) (value \"1N4148\") (libsource (lib \"Device:D\")) (field (name \"Note\") \"Envelope rectifier\"))",
452 id = comp.id
453 )
454 .unwrap();
455 } else {
456 let (lib_ref, _prefix) = comp.kind.footprint_ref();
457 let val = value_str(&*comp.kind);
458 writeln!(
459 out,
460 " (comp (ref {id}) (value \"{val}\") (libsource (lib \"{lib_ref}\")))",
461 id = comp.id
462 )
463 .unwrap();
464 }
465 }
466 writeln!(out, " )").unwrap();
467
468 let net_map = build_net_map(&pedal.nets);
470 writeln!(out, " (nets").unwrap();
471 for (num, name, pins) in &net_map {
472 write!(out, " (net (code {num}) (name \"{name}\")").unwrap();
473 for pin in pins {
474 write!(out, " (node (ref \"{}\"))", pin_to_string(pin)).unwrap();
475 }
476 writeln!(out, ")").unwrap();
477 }
478 writeln!(out, " )").unwrap();
479
480 writeln!(out, ")").unwrap();
481 out
482}
483
484#[cfg(test)]
489mod tests {
490 use super::*;
491
492 #[test]
493 fn format_eng_values() {
494 assert_eq!(format_eng(4700.0, "Ω"), "4.7kΩ");
495 assert_eq!(format_eng(220e-9, "F"), "220.0nF");
496 assert_eq!(format_eng(0.1, "H"), "100.0mH");
497 assert_eq!(format_eng(1e6, "Ω"), "1.0MΩ");
498 }
499
500 #[test]
501 fn export_tube_screamer() {
502 let pedal = PedalDef {
503 name: "Tube Screamer".into(),
504 subtitle: None,
505 components: vec![
506 ComponentDef {
507 id: "R1".into(),
508 kind: Box::new(Resistor { value: 4700.0 }),
509 },
510 ComponentDef {
511 id: "C1".into(),
512 kind: Box::new(Capacitor {
513 config: CapConfig::new(220e-9),
514 }),
515 },
516 ComponentDef {
517 id: "D1".into(),
518 kind: Box::new(DiodePair {
519 diode_type: DiodeType::Silicon,
520 }),
521 },
522 ],
523 nets: vec![
524 NetDef {
525 from: Pin::Reserved("in".into()),
526 to: vec![Pin::ComponentPin {
527 component: "C1".into(),
528 pin: "a".into(),
529 }],
530 },
531 NetDef {
532 from: Pin::ComponentPin {
533 component: "C1".into(),
534 pin: "b".into(),
535 },
536 to: vec![
537 Pin::ComponentPin {
538 component: "R1".into(),
539 pin: "a".into(),
540 },
541 Pin::ComponentPin {
542 component: "D1".into(),
543 pin: "a".into(),
544 },
545 ],
546 },
547 ],
548 controls: vec![],
549 supplies: vec![],
550 trims: vec![],
551 monitors: vec![],
552 sidechains: vec![],
553 mirrors: hashbrown::HashMap::new(),
554 calibrate: false,
555 subcircuits: vec![],
556 ports: vec![],
557 init_hints: vec![],
558 uses: vec![],
559 };
560
561 let netlist = export_kicad_netlist(&pedal);
562 assert!(netlist.contains("(export (version D)"));
563 assert!(netlist.contains("4.7kΩ"));
564 assert!(netlist.contains("220.0nF"));
565 assert!(netlist.contains("(net (code"));
566 }
567
568 #[test]
569 fn net_merging() {
570 let nets = vec![
572 NetDef {
573 from: Pin::Reserved("in".into()),
574 to: vec![Pin::ComponentPin {
575 component: "C1".into(),
576 pin: "a".into(),
577 }],
578 },
579 NetDef {
580 from: Pin::ComponentPin {
581 component: "C1".into(),
582 pin: "a".into(),
583 },
584 to: vec![Pin::ComponentPin {
585 component: "R1".into(),
586 pin: "a".into(),
587 }],
588 },
589 ];
590 let map = build_net_map(&nets);
591 assert_eq!(map.len(), 1);
593 assert_eq!(map[0].2.len(), 3); }
595}