mirror of https://codeberg.org/topola/topola.git
707 lines
17 KiB
Rust
707 lines
17 KiB
Rust
// SPDX-FileCopyrightText: 2024 Topola contributors
|
|
//
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
use super::read::{ListTokenizer, ReadDsn};
|
|
use super::write::{ListWriter, WriteSes};
|
|
use crate::error::{ParseError, ParseErrorContext};
|
|
use crate::math::PointWithRotation;
|
|
use crate::ListToken;
|
|
|
|
use core::fmt;
|
|
use geo_types::{LineString, Polygon as GeoPolygon};
|
|
use specctra_derive::{ReadDsn, WriteSes};
|
|
use std::{borrow::Cow, io};
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct Dummy {}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug)]
|
|
pub struct SesFile {
|
|
pub session: Session,
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct Session {
|
|
#[anon]
|
|
pub id: String,
|
|
pub routes: Routes,
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct Routes {
|
|
pub resolution: Resolution,
|
|
pub library_out: Library,
|
|
pub network_out: NetworkOut,
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct NetworkOut {
|
|
#[vec("net")]
|
|
pub net: Vec<NetOut>,
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct NetOut {
|
|
#[anon]
|
|
pub name: String,
|
|
#[vec("wire")]
|
|
pub wire: Vec<WireOut>,
|
|
#[vec("via")]
|
|
pub via: Vec<Via>,
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug)]
|
|
pub struct DsnFile {
|
|
pub pcb: Pcb,
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct Pcb {
|
|
#[anon]
|
|
pub name: String,
|
|
pub parser: Option<Parser>,
|
|
pub resolution: Resolution,
|
|
pub unit: Option<String>,
|
|
pub structure: Structure,
|
|
pub placement: Placement,
|
|
pub library: Library,
|
|
pub network: Network,
|
|
pub wiring: Wiring,
|
|
}
|
|
|
|
#[derive(WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct Parser {
|
|
pub string_quote: Option<char>,
|
|
pub space_in_quoted_tokens: Option<bool>,
|
|
pub host_cad: Option<String>,
|
|
pub host_version: Option<String>,
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct Resolution {
|
|
#[anon]
|
|
pub unit: String,
|
|
#[anon]
|
|
pub value: f32,
|
|
}
|
|
|
|
#[derive(WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct Structure {
|
|
#[vec("layer")]
|
|
pub layers: Vec<Layer>,
|
|
pub boundary: Boundary,
|
|
pub place_boundary: Option<Boundary>,
|
|
#[vec("plane")]
|
|
pub planes: Vec<Plane>,
|
|
#[anon]
|
|
pub keepouts: Keepouts,
|
|
pub via: ViaNames,
|
|
#[vec("grid")]
|
|
pub grids: Vec<Grid>,
|
|
// this is a vec of special structs because EasyEDA uses different syntax
|
|
// it outputs a sequence of rules containing a clearance each
|
|
// (in class rules it outputs a single rule with all clearances like KiCad)
|
|
#[vec("rule")]
|
|
pub rules: Vec<StructureRule>,
|
|
}
|
|
|
|
// custom impl to handle layers appearing late
|
|
impl<R: io::BufRead> ReadDsn<R> for Structure {
|
|
fn read_dsn(tokenizer: &mut ListTokenizer<R>) -> Result<Self, ParseErrorContext> {
|
|
let mut value = Self {
|
|
layers: tokenizer.read_named_array(&["layer"])?,
|
|
boundary: tokenizer.read_named(&["boundary"])?,
|
|
place_boundary: tokenizer.read_optional(&["place_boundary"])?,
|
|
planes: tokenizer.read_named_array(&["plane"])?,
|
|
keepouts: Keepouts::read_dsn(tokenizer)?,
|
|
via: tokenizer.read_named(&["via"])?,
|
|
grids: tokenizer.read_named_array(&["grid"])?,
|
|
rules: tokenizer.read_named_array(&["rule"])?,
|
|
};
|
|
|
|
value
|
|
.layers
|
|
.append(&mut tokenizer.read_named_array(&["layer"])?);
|
|
|
|
Ok(value)
|
|
}
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct Layer {
|
|
#[anon]
|
|
pub name: String,
|
|
pub r#type: String,
|
|
pub property: Option<Property>,
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct Property {
|
|
pub index: usize,
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub enum Boundary {
|
|
Path(Path),
|
|
Rect(Rect),
|
|
}
|
|
|
|
impl Boundary {
|
|
/// * For the `.structure.boundary`, it is expected that `.layer() == "pcb"`;
|
|
/// * Another special value which could be encountered is `"signal"`, for the boundary of the area available for routing.
|
|
pub fn layer(&self) -> &str {
|
|
match self {
|
|
Self::Path(x) => &x.layer,
|
|
Self::Rect(x) => &x.layer,
|
|
}
|
|
}
|
|
|
|
pub fn coords(&self) -> Cow<'_, [Point]> {
|
|
match self {
|
|
Self::Path(x) => Cow::Borrowed(&x.coords[..]),
|
|
Self::Rect(x) => Cow::Owned(x.coords()),
|
|
}
|
|
}
|
|
|
|
pub fn to_polygon(&self) -> GeoPolygon {
|
|
GeoPolygon::new(
|
|
LineString(
|
|
self.coords()
|
|
.iter()
|
|
.map(|i| geo_types::coord! { x: i.x, y: i.y })
|
|
.collect(),
|
|
),
|
|
Vec::new(),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct Plane {
|
|
#[anon]
|
|
pub net: String,
|
|
pub polygon: Polygon,
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct ViaNames {
|
|
#[anon_vec]
|
|
pub names: Vec<String>,
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct Grid {
|
|
#[anon]
|
|
pub kind: String,
|
|
#[anon]
|
|
pub value: f64,
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct StructureRule {
|
|
pub width: Option<f32>,
|
|
#[vec("clearance", "clear")]
|
|
pub clearances: Vec<Clearance>,
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct Placement {
|
|
#[vec("component")]
|
|
pub components: Vec<Component>,
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct Component {
|
|
#[anon]
|
|
pub name: String,
|
|
#[vec("place")]
|
|
pub places: Vec<Place>,
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
#[allow(non_snake_case)]
|
|
pub struct Place {
|
|
#[anon]
|
|
pub name: String,
|
|
#[anon]
|
|
pub x: f64,
|
|
#[anon]
|
|
pub y: f64,
|
|
#[anon]
|
|
pub side: String,
|
|
#[anon]
|
|
pub rotation: f64,
|
|
pub PN: Option<String>,
|
|
}
|
|
|
|
impl Place {
|
|
pub fn point_with_rotation(&self) -> PointWithRotation {
|
|
PointWithRotation {
|
|
pos: (self.x, self.y).into(),
|
|
rot: self.rotation,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct Library {
|
|
#[vec("image")]
|
|
pub images: Vec<Image>,
|
|
#[vec("padstack")]
|
|
pub padstacks: Vec<Padstack>,
|
|
}
|
|
|
|
impl Library {
|
|
pub fn find_padstack_by_name(&self, name: &str) -> Option<&Padstack> {
|
|
self.padstacks.iter().find(|padstack| padstack.name == name)
|
|
}
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct Image {
|
|
#[anon]
|
|
pub name: String,
|
|
#[vec("outline")]
|
|
pub outlines: Vec<Outline>,
|
|
#[vec("pin")]
|
|
pub pins: Vec<Pin>,
|
|
#[anon]
|
|
pub keepouts: Keepouts,
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct Outline {
|
|
pub path: Path,
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct Pin {
|
|
#[anon]
|
|
pub name: String,
|
|
pub rotate: Option<f64>,
|
|
#[anon]
|
|
pub id: String,
|
|
#[anon]
|
|
pub x: f64,
|
|
#[anon]
|
|
pub y: f64,
|
|
}
|
|
|
|
impl Pin {
|
|
pub fn point_with_rotation(&self) -> PointWithRotation {
|
|
PointWithRotation {
|
|
pos: (self.x, self.y).into(),
|
|
rot: self.rotate.unwrap_or(0.0),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct Keepouts(pub Vec<Keepout>);
|
|
|
|
impl<R: io::BufRead> ReadDsn<R> for Keepouts {
|
|
fn read_dsn(tokenizer: &mut ListTokenizer<R>) -> Result<Self, ParseErrorContext> {
|
|
let mut ret = Vec::new();
|
|
while let Ok(input) = tokenizer.consume_token() {
|
|
let is_keepout = input.token.is_start_of(&[
|
|
"keepout",
|
|
"place_keepout",
|
|
"via_keepout",
|
|
"wire_keepout",
|
|
"bend_keepout",
|
|
"elongate_keepout",
|
|
]);
|
|
tokenizer.return_token(input);
|
|
if !is_keepout {
|
|
break;
|
|
}
|
|
ret.push(Keepout::read_dsn(tokenizer)?);
|
|
}
|
|
Ok(Self(ret))
|
|
}
|
|
}
|
|
|
|
impl<W: io::Write> WriteSes<W> for Keepouts {
|
|
fn write_dsn(&self, writer: &mut ListWriter<W>) -> Result<(), io::Error> {
|
|
for i in &self.0[..] {
|
|
i.write_dsn(writer)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub enum KeepoutKind {
|
|
/// `keepout`
|
|
Normal,
|
|
/// `place_keepout`
|
|
Place,
|
|
/// `via_keepout`
|
|
Via,
|
|
/// `wire_keepout`
|
|
Wire,
|
|
/// `bend_keepout`
|
|
Bend,
|
|
/// `elongate_keepout`
|
|
Elongate,
|
|
}
|
|
|
|
impl core::str::FromStr for KeepoutKind {
|
|
type Err = ();
|
|
|
|
fn from_str(s: &str) -> Result<Self, ()> {
|
|
Ok(match s {
|
|
"keepout" => Self::Normal,
|
|
"place_keepout" => Self::Place,
|
|
"via_keepout" => Self::Via,
|
|
"wire_keepout" => Self::Wire,
|
|
"bend_keepout" => Self::Bend,
|
|
"elongate_keepout" => Self::Elongate,
|
|
_ => return Err(()),
|
|
})
|
|
}
|
|
}
|
|
|
|
impl AsRef<str> for KeepoutKind {
|
|
fn as_ref(&self) -> &str {
|
|
match self {
|
|
Self::Normal => "keepout",
|
|
Self::Place => "place_keepout",
|
|
Self::Via => "via_keepout",
|
|
Self::Wire => "wire_keepout",
|
|
Self::Bend => "bend_keepout",
|
|
Self::Elongate => "elongate_keepout",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for KeepoutKind {
|
|
#[inline]
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.write_str(self.as_ref())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct Keepout {
|
|
pub kind: KeepoutKind,
|
|
pub id_: String,
|
|
pub sequence_number: Option<i32>,
|
|
pub shape: Shape,
|
|
pub rule: Option<Rule>,
|
|
// TODO: `place_rule`: `(place_rule (spacing ....))`
|
|
}
|
|
|
|
impl<R: io::BufRead> ReadDsn<R> for Keepout {
|
|
fn read_dsn(tokenizer: &mut ListTokenizer<R>) -> Result<Self, ParseErrorContext> {
|
|
let input = tokenizer.consume_token()?;
|
|
let err = ParseError::ExpectedStartOfListOneOf(&[
|
|
"keepout",
|
|
"place_keepout",
|
|
"via_keepout",
|
|
"wire_keepout",
|
|
"bend_keepout",
|
|
"elongate_keepout",
|
|
]);
|
|
let kind = if let ListToken::Start { name: ref kind } = input.token {
|
|
kind.parse::<KeepoutKind>().map_err(|()| {
|
|
tokenizer.return_token(input);
|
|
tokenizer.add_context(err)
|
|
})?
|
|
} else {
|
|
tokenizer.return_token(input);
|
|
return Err(tokenizer.add_context(err));
|
|
};
|
|
|
|
let id_ = String::read_dsn(tokenizer)?;
|
|
let sequence_number = tokenizer.read_optional(&["sequence_number"])?;
|
|
let shape = Shape::read_dsn(tokenizer)?;
|
|
let rule = tokenizer.read_optional(&["rule"])?;
|
|
// TODO: handle `place_rule`
|
|
|
|
tokenizer.consume_token()?.expect_end()?;
|
|
Ok(Self {
|
|
kind,
|
|
id_,
|
|
sequence_number,
|
|
shape,
|
|
rule,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl<W: io::Write> WriteSes<W> for Keepout {
|
|
fn write_dsn(&self, writer: &mut ListWriter<W>) -> Result<(), io::Error> {
|
|
writer.write_token(ListToken::Start {
|
|
name: self.kind.as_ref().to_string(),
|
|
})?;
|
|
|
|
self.id_.write_dsn(writer)?;
|
|
writer.write_optional("sequence_number", &self.sequence_number)?;
|
|
self.shape.write_dsn(writer)?;
|
|
writer.write_optional("rule", &self.rule)?;
|
|
|
|
writer.write_token(ListToken::End)
|
|
}
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct Padstack {
|
|
#[anon]
|
|
pub name: String,
|
|
#[vec("shape")]
|
|
pub shapes: Vec<Shape>,
|
|
pub attach: Option<bool>,
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub enum Shape {
|
|
Circle(Circle),
|
|
Rect(Rect),
|
|
Path(Path),
|
|
Polygon(Polygon),
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct Circle {
|
|
#[anon]
|
|
pub layer: String,
|
|
#[anon]
|
|
pub diameter: f64,
|
|
#[anon]
|
|
pub offset: Option<Point>,
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct Network {
|
|
#[vec("net")]
|
|
pub nets: Vec<NetPinAssignments>,
|
|
#[vec("class")]
|
|
pub classes: Vec<Class>,
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
// dsn names this "net", but it's a structure unrelated to "net" in wiring or elsewhere
|
|
pub struct NetPinAssignments {
|
|
#[anon]
|
|
pub name: String,
|
|
pub pins: Option<Pins>,
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct Pins {
|
|
#[anon_vec]
|
|
pub names: Vec<String>,
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct Class {
|
|
#[anon]
|
|
pub name: String,
|
|
#[anon_vec]
|
|
pub nets: Vec<String>,
|
|
pub circuit: Circuit,
|
|
pub rule: Rule,
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct Circuit {
|
|
pub use_via: String,
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct Wiring {
|
|
#[vec("wire")]
|
|
pub wires: Vec<Wire>,
|
|
#[vec("via")]
|
|
pub vias: Vec<Via>,
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct Wire {
|
|
pub path: Path,
|
|
pub net: String,
|
|
pub r#type: String,
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct WireOut {
|
|
pub path: Path,
|
|
}
|
|
|
|
////////////////////////////////////////////
|
|
// structs that appear in multiple places //
|
|
////////////////////////////////////////////
|
|
|
|
// This type isn't meant to be deserialized as is (single points are
|
|
// more conveniently represented as fields on the enclosing struct)
|
|
// It exists to give a way to read arrays of coordinates
|
|
// (and enforce that such an array actually contains a whole number of points)
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct Point {
|
|
pub x: f64,
|
|
pub y: f64,
|
|
}
|
|
|
|
impl From<geo_types::Point> for Point {
|
|
#[inline(always)]
|
|
fn from(z: geo_types::Point) -> Self {
|
|
Point { x: z.0.x, y: z.0.y }
|
|
}
|
|
}
|
|
|
|
impl From<Point> for geo_types::Point {
|
|
#[inline(always)]
|
|
fn from(z: Point) -> Self {
|
|
geo_types::point! {
|
|
x: z.x,
|
|
y: z.y
|
|
}
|
|
}
|
|
}
|
|
|
|
// Custom impl for the case described above
|
|
impl<R: io::BufRead> ReadDsn<R> for Vec<Point> {
|
|
fn read_dsn(tokenizer: &mut ListTokenizer<R>) -> Result<Self, ParseErrorContext> {
|
|
let mut array = Vec::new();
|
|
while let Some(x) = tokenizer.read_value::<Option<Point>>()? {
|
|
array.push(x);
|
|
}
|
|
Ok(array)
|
|
}
|
|
}
|
|
|
|
impl<R: io::BufRead> ReadDsn<R> for Option<Point> {
|
|
fn read_dsn(tokenizer: &mut ListTokenizer<R>) -> Result<Self, ParseErrorContext> {
|
|
let input = tokenizer.consume_token()?;
|
|
Ok(if let ListToken::Leaf { value: ref x } = input.token {
|
|
let x = x
|
|
.parse::<f64>()
|
|
.map_err(|_| ParseError::Expected("f64").add_context(input.context))?;
|
|
let y = tokenizer.read_value::<f64>()?;
|
|
Some(Point { x, y })
|
|
} else {
|
|
tokenizer.return_token(input);
|
|
None
|
|
})
|
|
}
|
|
}
|
|
|
|
impl<W: io::Write> WriteSes<W> for Vec<Point> {
|
|
fn write_dsn(&self, writer: &mut ListWriter<W>) -> Result<(), io::Error> {
|
|
for elem in self {
|
|
writer.write_value(&elem.x)?;
|
|
writer.write_value(&elem.y)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl<W: io::Write> WriteSes<W> for Option<Point> {
|
|
fn write_dsn(&self, writer: &mut ListWriter<W>) -> Result<(), io::Error> {
|
|
if let Some(value) = self {
|
|
writer.write_value(&value.x)?;
|
|
writer.write_value(&value.y)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct Polygon {
|
|
#[anon]
|
|
pub layer: String,
|
|
#[anon]
|
|
pub width: f64,
|
|
#[anon]
|
|
pub coords: Vec<Point>,
|
|
}
|
|
|
|
impl Polygon {
|
|
pub fn to_polygon(&self) -> GeoPolygon {
|
|
GeoPolygon::new(
|
|
LineString(
|
|
self.coords
|
|
.iter()
|
|
.map(|i| geo_types::coord! { x: i.x, y: i.y })
|
|
.collect(),
|
|
),
|
|
Vec::new(),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct Path {
|
|
#[anon]
|
|
pub layer: String,
|
|
#[anon]
|
|
pub width: f64,
|
|
#[anon]
|
|
pub coords: Vec<Point>,
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct Rect {
|
|
#[anon]
|
|
pub layer: String,
|
|
#[anon]
|
|
pub x1: f64,
|
|
#[anon]
|
|
pub y1: f64,
|
|
#[anon]
|
|
pub x2: f64,
|
|
#[anon]
|
|
pub y2: f64,
|
|
}
|
|
|
|
impl Rect {
|
|
fn coords(&self) -> Vec<Point> {
|
|
vec![
|
|
Point {
|
|
x: self.x1,
|
|
y: self.y1,
|
|
},
|
|
Point {
|
|
x: self.x2,
|
|
y: self.y1,
|
|
},
|
|
Point {
|
|
x: self.x2,
|
|
y: self.y2,
|
|
},
|
|
Point {
|
|
x: self.x1,
|
|
y: self.y2,
|
|
},
|
|
]
|
|
}
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct Via {
|
|
#[anon]
|
|
pub name: String,
|
|
#[anon]
|
|
pub x: f64,
|
|
#[anon]
|
|
pub y: f64,
|
|
pub net: String,
|
|
pub r#type: String,
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct Rule {
|
|
pub width: f32,
|
|
#[vec("clearance")]
|
|
pub clearances: Vec<Clearance>,
|
|
}
|
|
|
|
#[derive(ReadDsn, WriteSes, Debug, Clone, PartialEq)]
|
|
pub struct Clearance {
|
|
#[anon]
|
|
pub value: f32,
|
|
pub r#type: Option<String>,
|
|
}
|