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]
pub y: f64,
pub net: String,
pub r#type: String,
}
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]

View File

@ -21,20 +21,23 @@ pub struct ConnectedComponents {
impl ConnectedComponents {
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() {
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
// that, we have another loop to go over all the pins and connect all
// their primitives.
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(
@ -70,33 +73,33 @@ impl ConnectedComponents {
fn unionize_primitive_endpoint_dots(
board: &Board<impl AccessMesadata>,
unionfind: &mut UnionFind<usize>,
dot_unionfind: &mut UnionFind<usize>,
primitive: PrimitiveIndex,
) {
match primitive {
PrimitiveIndex::FixedSeg(seg) => {
let joints = board.layout().drawing().primitive(seg).joints();
unionfind.union(joints.0.index(), joints.1.index());
Self::unionize_fixed_dot_via(board, unionfind, joints.0);
Self::unionize_fixed_dot_via(board, unionfind, joints.1);
dot_unionfind.union(joints.0.index(), joints.1.index());
Self::unionize_fixed_dot_via(board, dot_unionfind, joints.0);
Self::unionize_fixed_dot_via(board, dot_unionfind, joints.1);
}
PrimitiveIndex::LoneLooseSeg(seg) => {
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) => {
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) => {
let joints = board.layout().drawing().primitive(bend).joints();
unionfind.union(joints.0.index(), joints.1.index());
Self::unionize_fixed_dot_via(board, unionfind, joints.0);
Self::unionize_fixed_dot_via(board, unionfind, joints.1);
dot_unionfind.union(joints.0.index(), joints.1.index());
Self::unionize_fixed_dot_via(board, dot_unionfind, joints.0);
Self::unionize_fixed_dot_via(board, dot_unionfind, joints.1);
}
PrimitiveIndex::LooseBend(bend) => {
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(
board: &Board<impl AccessMesadata>,
unionfind: &mut UnionFind<usize>,
dot_unionfind: &mut UnionFind<usize>,
pinname: &str,
) {
let mut iter = board.pinname_nodes(pinname);
@ -120,53 +123,53 @@ impl ConnectedComponents {
for node in board.pinname_nodes(pinname) {
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(
board: &Board<impl AccessMesadata>,
unionfind: &mut UnionFind<usize>,
dot_unionfind: &mut UnionFind<usize>,
primitive: PrimitiveIndex,
common: FixedDotIndex,
) {
match primitive {
PrimitiveIndex::FixedDot(dot) => {
unionfind.union(common.index(), dot.index());
Self::unionize_fixed_dot_via(board, unionfind, dot);
dot_unionfind.union(common.index(), dot.index());
Self::unionize_fixed_dot_via(board, dot_unionfind, dot);
}
PrimitiveIndex::LooseDot(dot) => {
unionfind.union(common.index(), dot.index());
dot_unionfind.union(common.index(), dot.index());
}
PrimitiveIndex::FixedSeg(seg) => {
let joints = board.layout().drawing().primitive(seg).joints();
unionfind.union(common.index(), joints.0.index());
Self::unionize_fixed_dot_via(board, unionfind, joints.0);
unionfind.union(common.index(), joints.1.index());
Self::unionize_fixed_dot_via(board, unionfind, joints.1);
dot_unionfind.union(common.index(), joints.0.index());
Self::unionize_fixed_dot_via(board, dot_unionfind, joints.0);
dot_unionfind.union(common.index(), joints.1.index());
Self::unionize_fixed_dot_via(board, dot_unionfind, joints.1);
}
PrimitiveIndex::LoneLooseSeg(seg) => {
let joints = board.layout().drawing().primitive(seg).joints();
unionfind.union(common.index(), joints.0.index());
unionfind.union(common.index(), joints.1.index());
dot_unionfind.union(common.index(), joints.0.index());
dot_unionfind.union(common.index(), joints.1.index());
}
PrimitiveIndex::SeqLooseSeg(seg) => {
let joints = board.layout().drawing().primitive(seg).joints();
unionfind.union(common.index(), joints.0.index());
unionfind.union(common.index(), joints.1.index());
dot_unionfind.union(common.index(), joints.0.index());
dot_unionfind.union(common.index(), joints.1.index());
}
PrimitiveIndex::FixedBend(bend) => {
let joints = board.layout().drawing().primitive(bend).joints();
unionfind.union(common.index(), joints.0.index());
Self::unionize_fixed_dot_via(board, unionfind, joints.0);
unionfind.union(common.index(), joints.1.index());
Self::unionize_fixed_dot_via(board, unionfind, joints.1);
dot_unionfind.union(common.index(), joints.0.index());
Self::unionize_fixed_dot_via(board, dot_unionfind, joints.0);
dot_unionfind.union(common.index(), joints.1.index());
Self::unionize_fixed_dot_via(board, dot_unionfind, joints.1);
}
PrimitiveIndex::LooseBend(bend) => {
let joints = board.layout().drawing().primitive(bend).joints();
unionfind.union(common.index(), joints.0.index());
unionfind.union(common.index(), joints.1.index());
dot_unionfind.union(common.index(), joints.0.index());
dot_unionfind.union(common.index(), joints.1.index());
}
_ => (),
}
@ -174,12 +177,12 @@ impl ConnectedComponents {
fn unionize_fixed_dot_via(
board: &Board<impl AccessMesadata>,
unionfind: &mut UnionFind<usize>,
dot_unionfind: &mut UnionFind<usize>,
dot: FixedDotIndex,
) {
if let Some(via) = board.layout().fixed_dot_via(dot) {
for via_dot in board.layout().via(via).dots() {
unionfind.union(dot.index(), via_dot.index());
for via_dot in board.layout().via_ref(via).dots() {
dot_unionfind.union(dot.index(), via_dot.index());
}
}
}

View File

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

View File

@ -24,12 +24,12 @@ use crate::{
};
#[derive(Debug)]
pub struct Via<'a, R> {
pub struct ViaRef<'a, R> {
pub index: GenericIndex<ViaWeight>,
drawing: &'a Drawing<CompoundWeight, CompoundEntryLabel, R>,
}
impl<'a, R> Via<'a, R> {
impl<'a, R> ViaRef<'a, R> {
pub fn new(
index: GenericIndex<ViaWeight>,
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> {
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 {
if let CompoundWeight::Via(weight) = self.drawing.compound_weight(self.index.into()) {
weight.shape()

View File

@ -6,7 +6,7 @@
//! Design DSN file, creating the [`Board`] object from the file, as well as
//! 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 itertools::Itertools;
@ -16,13 +16,14 @@ use crate::{
board::{edit::BoardEdit, AccessMesadata, Board},
drawing::{
dot::{FixedDotIndex, FixedDotWeight, GeneralDotWeight},
graph::{GetMaybeNet, MakePrimitiveRef},
graph::{GetMaybeNet, MakePrimitiveRef, PrimitiveIndex},
primitive::MakePrimitiveShape,
seg::{FixedSegWeight, GeneralSegWeight},
Drawing,
},
geometry::{primitive::PrimitiveShape, GetLayer, GetWidth},
layout::{poly::SolidPolyWeight, Layout},
geometry::{primitive::PrimitiveShape, shape::AccessShape, GetLayer, GetWidth},
graph::GenericIndex,
layout::{poly::SolidPolyWeight, via::ViaWeight, Layout},
math::{self, Circle},
specctra::{
mesadata::SpecctraMesadata,
@ -77,10 +78,32 @@ impl SpecctraDesign {
let drawing = board.layout().drawing();
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() {
let primitive = index.primitive_ref(drawing);
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() {
PrimitiveShape::Seg(seg) => {
vec![
@ -106,12 +129,37 @@ impl SpecctraDesign {
.collect()
}
// Intentionally skipped for now.
// Topola stores trace segments and dots joining them
// as separate objects, but the Specctra formats and KiCad
// appear to consider them implicit.
// TODO: Vias
PrimitiveShape::Dot(_) => continue,
PrimitiveShape::Dot(dot_shape) => {
let PrimitiveIndex::FixedDot(dot) = index else {
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 {
@ -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);
}
}
@ -163,7 +195,23 @@ impl SpecctraDesign {
},
library_out: structure::Library {
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 {
net: net_outs.into_values().collect(),