feat(specctra-core): Add basic via SES export (constant size and only two layers for now)

This commit is contained in:
Mikolaj Wielgus 2025-11-24 00:05:19 +01:00
parent 8b0adec8fe
commit 00e3bb87bf
5 changed files with 122 additions and 72 deletions

View File

@ -688,7 +688,6 @@ pub struct Via {
#[anon] #[anon]
pub y: f64, pub y: f64,
pub net: String, pub net: String,
pub r#type: String,
} }
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)] #[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]

View File

@ -21,20 +21,23 @@ pub struct ConnectedComponents {
impl ConnectedComponents { impl ConnectedComponents {
pub fn new(board: &Board<impl AccessMesadata>) -> Self { pub fn new(board: &Board<impl AccessMesadata>) -> Self {
let mut unionfind = UnionFind::new(board.layout().drawing().geometry().dot_index_bound()); let mut dot_unionfind =
UnionFind::new(board.layout().drawing().geometry().dot_index_bound());
for node in board.layout().drawing().primitive_nodes() { for node in board.layout().drawing().primitive_nodes() {
Self::unionize_primitive_endpoint_dots(board, &mut unionfind, node); Self::unionize_primitive_endpoint_dots(board, &mut dot_unionfind, node);
} }
// Pins can have padstacks that span multiple layers. To account for // Pins can have padstacks that span multiple layers. To account for
// that, we have another loop to go over all the pins and connect all // that, we have another loop to go over all the pins and connect all
// their primitives. // their primitives.
for pinname in board.pinnames() { for pinname in board.pinnames() {
Self::unionize_pin(board, &mut unionfind, pinname); Self::unionize_pin(board, &mut dot_unionfind, pinname);
} }
Self { unionfind } Self {
unionfind: dot_unionfind,
}
} }
pub fn new_with_principal_layer( pub fn new_with_principal_layer(
@ -70,33 +73,33 @@ impl ConnectedComponents {
fn unionize_primitive_endpoint_dots( fn unionize_primitive_endpoint_dots(
board: &Board<impl AccessMesadata>, board: &Board<impl AccessMesadata>,
unionfind: &mut UnionFind<usize>, dot_unionfind: &mut UnionFind<usize>,
primitive: PrimitiveIndex, primitive: PrimitiveIndex,
) { ) {
match primitive { match primitive {
PrimitiveIndex::FixedSeg(seg) => { PrimitiveIndex::FixedSeg(seg) => {
let joints = board.layout().drawing().primitive(seg).joints(); let joints = board.layout().drawing().primitive(seg).joints();
unionfind.union(joints.0.index(), joints.1.index()); dot_unionfind.union(joints.0.index(), joints.1.index());
Self::unionize_fixed_dot_via(board, unionfind, joints.0); Self::unionize_fixed_dot_via(board, dot_unionfind, joints.0);
Self::unionize_fixed_dot_via(board, unionfind, joints.1); Self::unionize_fixed_dot_via(board, dot_unionfind, joints.1);
} }
PrimitiveIndex::LoneLooseSeg(seg) => { PrimitiveIndex::LoneLooseSeg(seg) => {
let joints = board.layout().drawing().primitive(seg).joints(); let joints = board.layout().drawing().primitive(seg).joints();
unionfind.union(joints.0.index(), joints.1.index()); dot_unionfind.union(joints.0.index(), joints.1.index());
} }
PrimitiveIndex::SeqLooseSeg(seg) => { PrimitiveIndex::SeqLooseSeg(seg) => {
let joints = board.layout().drawing().primitive(seg).joints(); let joints = board.layout().drawing().primitive(seg).joints();
unionfind.union(joints.0.index(), joints.1.index()); dot_unionfind.union(joints.0.index(), joints.1.index());
} }
PrimitiveIndex::FixedBend(bend) => { PrimitiveIndex::FixedBend(bend) => {
let joints = board.layout().drawing().primitive(bend).joints(); let joints = board.layout().drawing().primitive(bend).joints();
unionfind.union(joints.0.index(), joints.1.index()); dot_unionfind.union(joints.0.index(), joints.1.index());
Self::unionize_fixed_dot_via(board, unionfind, joints.0); Self::unionize_fixed_dot_via(board, dot_unionfind, joints.0);
Self::unionize_fixed_dot_via(board, unionfind, joints.1); Self::unionize_fixed_dot_via(board, dot_unionfind, joints.1);
} }
PrimitiveIndex::LooseBend(bend) => { PrimitiveIndex::LooseBend(bend) => {
let joints = board.layout().drawing().primitive(bend).joints(); let joints = board.layout().drawing().primitive(bend).joints();
unionfind.union(joints.0.index(), joints.1.index()); dot_unionfind.union(joints.0.index(), joints.1.index());
} }
_ => (), _ => (),
} }
@ -104,7 +107,7 @@ impl ConnectedComponents {
fn unionize_pin( fn unionize_pin(
board: &Board<impl AccessMesadata>, board: &Board<impl AccessMesadata>,
unionfind: &mut UnionFind<usize>, dot_unionfind: &mut UnionFind<usize>,
pinname: &str, pinname: &str,
) { ) {
let mut iter = board.pinname_nodes(pinname); let mut iter = board.pinname_nodes(pinname);
@ -120,53 +123,53 @@ impl ConnectedComponents {
for node in board.pinname_nodes(pinname) { for node in board.pinname_nodes(pinname) {
if let GenericNode::Primitive(primitive) = node { if let GenericNode::Primitive(primitive) = node {
Self::unionize_to_common(board, unionfind, primitive, first_fixed_dot); Self::unionize_to_common(board, dot_unionfind, primitive, first_fixed_dot);
} }
} }
} }
fn unionize_to_common( fn unionize_to_common(
board: &Board<impl AccessMesadata>, board: &Board<impl AccessMesadata>,
unionfind: &mut UnionFind<usize>, dot_unionfind: &mut UnionFind<usize>,
primitive: PrimitiveIndex, primitive: PrimitiveIndex,
common: FixedDotIndex, common: FixedDotIndex,
) { ) {
match primitive { match primitive {
PrimitiveIndex::FixedDot(dot) => { PrimitiveIndex::FixedDot(dot) => {
unionfind.union(common.index(), dot.index()); dot_unionfind.union(common.index(), dot.index());
Self::unionize_fixed_dot_via(board, unionfind, dot); Self::unionize_fixed_dot_via(board, dot_unionfind, dot);
} }
PrimitiveIndex::LooseDot(dot) => { PrimitiveIndex::LooseDot(dot) => {
unionfind.union(common.index(), dot.index()); dot_unionfind.union(common.index(), dot.index());
} }
PrimitiveIndex::FixedSeg(seg) => { PrimitiveIndex::FixedSeg(seg) => {
let joints = board.layout().drawing().primitive(seg).joints(); let joints = board.layout().drawing().primitive(seg).joints();
unionfind.union(common.index(), joints.0.index()); dot_unionfind.union(common.index(), joints.0.index());
Self::unionize_fixed_dot_via(board, unionfind, joints.0); Self::unionize_fixed_dot_via(board, dot_unionfind, joints.0);
unionfind.union(common.index(), joints.1.index()); dot_unionfind.union(common.index(), joints.1.index());
Self::unionize_fixed_dot_via(board, unionfind, joints.1); Self::unionize_fixed_dot_via(board, dot_unionfind, joints.1);
} }
PrimitiveIndex::LoneLooseSeg(seg) => { PrimitiveIndex::LoneLooseSeg(seg) => {
let joints = board.layout().drawing().primitive(seg).joints(); let joints = board.layout().drawing().primitive(seg).joints();
unionfind.union(common.index(), joints.0.index()); dot_unionfind.union(common.index(), joints.0.index());
unionfind.union(common.index(), joints.1.index()); dot_unionfind.union(common.index(), joints.1.index());
} }
PrimitiveIndex::SeqLooseSeg(seg) => { PrimitiveIndex::SeqLooseSeg(seg) => {
let joints = board.layout().drawing().primitive(seg).joints(); let joints = board.layout().drawing().primitive(seg).joints();
unionfind.union(common.index(), joints.0.index()); dot_unionfind.union(common.index(), joints.0.index());
unionfind.union(common.index(), joints.1.index()); dot_unionfind.union(common.index(), joints.1.index());
} }
PrimitiveIndex::FixedBend(bend) => { PrimitiveIndex::FixedBend(bend) => {
let joints = board.layout().drawing().primitive(bend).joints(); let joints = board.layout().drawing().primitive(bend).joints();
unionfind.union(common.index(), joints.0.index()); dot_unionfind.union(common.index(), joints.0.index());
Self::unionize_fixed_dot_via(board, unionfind, joints.0); Self::unionize_fixed_dot_via(board, dot_unionfind, joints.0);
unionfind.union(common.index(), joints.1.index()); dot_unionfind.union(common.index(), joints.1.index());
Self::unionize_fixed_dot_via(board, unionfind, joints.1); Self::unionize_fixed_dot_via(board, dot_unionfind, joints.1);
} }
PrimitiveIndex::LooseBend(bend) => { PrimitiveIndex::LooseBend(bend) => {
let joints = board.layout().drawing().primitive(bend).joints(); let joints = board.layout().drawing().primitive(bend).joints();
unionfind.union(common.index(), joints.0.index()); dot_unionfind.union(common.index(), joints.0.index());
unionfind.union(common.index(), joints.1.index()); dot_unionfind.union(common.index(), joints.1.index());
} }
_ => (), _ => (),
} }
@ -174,12 +177,12 @@ impl ConnectedComponents {
fn unionize_fixed_dot_via( fn unionize_fixed_dot_via(
board: &Board<impl AccessMesadata>, board: &Board<impl AccessMesadata>,
unionfind: &mut UnionFind<usize>, dot_unionfind: &mut UnionFind<usize>,
dot: FixedDotIndex, dot: FixedDotIndex,
) { ) {
if let Some(via) = board.layout().fixed_dot_via(dot) { if let Some(via) = board.layout().fixed_dot_via(dot) {
for via_dot in board.layout().via(via).dots() { for via_dot in board.layout().via_ref(via).dots() {
unionfind.union(dot.index(), via_dot.index()); dot_unionfind.union(dot.index(), via_dot.index());
} }
} }
} }

View File

@ -36,7 +36,7 @@ use crate::{
graph::{GenericIndex, GetIndex, MakeRef}, graph::{GenericIndex, GetIndex, MakeRef},
layout::{ layout::{
poly::{add_poly_with_nodes_intern, MakePolygon, PolyWeight}, poly::{add_poly_with_nodes_intern, MakePolygon, PolyWeight},
via::{Via, ViaWeight}, via::{ViaRef, ViaWeight},
}, },
math::RotationSense, math::RotationSense,
}; };
@ -476,8 +476,8 @@ impl<R: AccessRules> Layout<R> {
self.drawing.rules_mut() self.drawing.rules_mut()
} }
pub fn via(&self, index: GenericIndex<ViaWeight>) -> Via<'_, R> { pub fn via_ref(&self, index: GenericIndex<ViaWeight>) -> ViaRef<'_, R> {
Via::new(index, self.drawing()) ViaRef::new(index, self.drawing())
} }
} }

View File

@ -24,12 +24,12 @@ use crate::{
}; };
#[derive(Debug)] #[derive(Debug)]
pub struct Via<'a, R> { pub struct ViaRef<'a, R> {
pub index: GenericIndex<ViaWeight>, pub index: GenericIndex<ViaWeight>,
drawing: &'a Drawing<CompoundWeight, CompoundEntryLabel, R>, drawing: &'a Drawing<CompoundWeight, CompoundEntryLabel, R>,
} }
impl<'a, R> Via<'a, R> { impl<'a, R> ViaRef<'a, R> {
pub fn new( pub fn new(
index: GenericIndex<ViaWeight>, index: GenericIndex<ViaWeight>,
drawing: &'a Drawing<CompoundWeight, CompoundEntryLabel, R>, drawing: &'a Drawing<CompoundWeight, CompoundEntryLabel, R>,
@ -45,13 +45,13 @@ impl<'a, R> Via<'a, R> {
} }
} }
impl<R: AccessRules> GetMaybeNet for Via<'_, R> { impl<R: AccessRules> GetMaybeNet for ViaRef<'_, R> {
fn maybe_net(&self) -> Option<usize> { fn maybe_net(&self) -> Option<usize> {
self.drawing.compound_weight(self.index.into()).maybe_net() self.drawing.compound_weight(self.index.into()).maybe_net()
} }
} }
impl<R: AccessRules> MakePrimitiveShape for Via<'_, R> { impl<R: AccessRules> MakePrimitiveShape for ViaRef<'_, R> {
fn shape(&self) -> PrimitiveShape { fn shape(&self) -> PrimitiveShape {
if let CompoundWeight::Via(weight) = self.drawing.compound_weight(self.index.into()) { if let CompoundWeight::Via(weight) = self.drawing.compound_weight(self.index.into()) {
weight.shape() weight.shape()

View File

@ -6,7 +6,7 @@
//! Design DSN file, creating the [`Board`] object from the file, as well as //! Design DSN file, creating the [`Board`] object from the file, as well as
//! exporting the session file //! exporting the session file
use std::collections::{btree_map::Entry as BTreeMapEntry, BTreeMap}; use std::collections::{btree_map::Entry as BTreeMapEntry, BTreeMap, BTreeSet};
use geo::{Euclidean, Length, Line, Point, Rotate}; use geo::{Euclidean, Length, Line, Point, Rotate};
use itertools::Itertools; use itertools::Itertools;
@ -16,13 +16,14 @@ use crate::{
board::{edit::BoardEdit, AccessMesadata, Board}, board::{edit::BoardEdit, AccessMesadata, Board},
drawing::{ drawing::{
dot::{FixedDotIndex, FixedDotWeight, GeneralDotWeight}, dot::{FixedDotIndex, FixedDotWeight, GeneralDotWeight},
graph::{GetMaybeNet, MakePrimitiveRef}, graph::{GetMaybeNet, MakePrimitiveRef, PrimitiveIndex},
primitive::MakePrimitiveShape, primitive::MakePrimitiveShape,
seg::{FixedSegWeight, GeneralSegWeight}, seg::{FixedSegWeight, GeneralSegWeight},
Drawing, Drawing,
}, },
geometry::{primitive::PrimitiveShape, GetLayer, GetWidth}, geometry::{primitive::PrimitiveShape, shape::AccessShape, GetLayer, GetWidth},
layout::{poly::SolidPolyWeight, Layout}, graph::GenericIndex,
layout::{poly::SolidPolyWeight, via::ViaWeight, Layout},
math::{self, Circle}, math::{self, Circle},
specctra::{ specctra::{
mesadata::SpecctraMesadata, mesadata::SpecctraMesadata,
@ -77,10 +78,32 @@ impl SpecctraDesign {
let drawing = board.layout().drawing(); let drawing = board.layout().drawing();
let mut net_outs = BTreeMap::<usize, structure::NetOut>::new(); let mut net_outs = BTreeMap::<usize, structure::NetOut>::new();
// Since we iterate over primitives, keep track of added vias to prevent
// creation of duplicates.
let mut visited_vias: BTreeSet<GenericIndex<ViaWeight>> = BTreeSet::new();
for index in drawing.primitive_nodes() { for index in drawing.primitive_nodes() {
let primitive = index.primitive_ref(drawing); let primitive = index.primitive_ref(drawing);
if let Some(net) = primitive.maybe_net() { if let Some(net) = primitive.maybe_net() {
let net_out = match net_outs.entry(net) {
BTreeMapEntry::Occupied(occ) => occ.into_mut(),
BTreeMapEntry::Vacant(vac) => vac.insert(structure::NetOut {
name: mesadata
.net_netname(net)
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("tried to reference invalid net ID {}", net),
)
})?
.to_owned(),
wire: Vec::new(),
via: Vec::new(),
}),
};
let coords = match primitive.shape() { let coords = match primitive.shape() {
PrimitiveShape::Seg(seg) => { PrimitiveShape::Seg(seg) => {
vec![ vec![
@ -106,12 +129,37 @@ impl SpecctraDesign {
.collect() .collect()
} }
// Intentionally skipped for now. PrimitiveShape::Dot(dot_shape) => {
// Topola stores trace segments and dots joining them let PrimitiveIndex::FixedDot(dot) = index else {
// as separate objects, but the Specctra formats and KiCad continue;
// appear to consider them implicit. };
// TODO: Vias
PrimitiveShape::Dot(_) => continue, if let Some(via) = board.layout().fixed_dot_via(dot) {
if !visited_vias.contains(&via) {
net_out.via.push(structure::Via {
name: "__Via".to_string(),
x: dot_shape.center().x(),
y: dot_shape.center().y(),
net: mesadata
.net_netname(net)
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"tried to reference invalid net ID {}",
net
),
)
})?
.to_owned(),
});
visited_vias.insert(via);
}
}
continue;
}
}; };
let wire = structure::WireOut { let wire = structure::WireOut {
@ -133,22 +181,6 @@ impl SpecctraDesign {
}, },
}; };
let net_out = match net_outs.entry(net) {
BTreeMapEntry::Occupied(occ) => occ.into_mut(),
BTreeMapEntry::Vacant(vac) => vac.insert(structure::NetOut {
name: mesadata
.net_netname(net)
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("tried to reference invalid net ID {}", net),
)
})?
.to_owned(),
wire: Vec::new(),
via: Vec::new(),
}),
};
net_out.wire.push(wire); net_out.wire.push(wire);
} }
} }
@ -163,7 +195,23 @@ impl SpecctraDesign {
}, },
library_out: structure::Library { library_out: structure::Library {
images: Vec::new(), images: Vec::new(),
padstacks: Vec::new(), padstacks: vec![structure::Padstack {
name: "__Via".to_string(),
// TODO: Use correct sizes and have all layers.
shapes: vec![
structure::Shape::Circle(structure::Circle {
layer: "F.Cu".to_string(),
diameter: 500.0,
offset: None,
}),
structure::Shape::Circle(structure::Circle {
layer: "B.Cu".to_string(),
diameter: 500.0,
offset: None,
}),
],
attach: None,
}],
}, },
network_out: structure::NetworkOut { network_out: structure::NetworkOut {
net: net_outs.into_values().collect(), net: net_outs.into_values().collect(),