diff --git a/Cargo.toml b/Cargo.toml index cefe9cdd..09a846fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,10 @@ [workspace] members = [ - "examples", + "crates/kicad-parser", "crates/opencascade", "crates/opencascade-sys", "crates/viewer", + "examples", ] resolver = "2" diff --git a/crates/kicad-parser/Cargo.toml b/crates/kicad-parser/Cargo.toml new file mode 100644 index 00000000..3ce4d468 --- /dev/null +++ b/crates/kicad-parser/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "kicad-parser" +version = "0.1.0" +edition = "2021" + +[dependencies] +thiserror = "1" +sexp = "1.1.4" diff --git a/crates/kicad-parser/sample-files/sample.kicad_pcb b/crates/kicad-parser/sample-files/sample.kicad_pcb new file mode 100644 index 00000000..10f00f3e --- /dev/null +++ b/crates/kicad-parser/sample-files/sample.kicad_pcb @@ -0,0 +1,93 @@ +(kicad_pcb (version 20221018) (generator pcbnew) + + (general + (thickness 0.89) + ) + + (paper "A4") + (layers + (0 "F.Cu" signal) + (31 "B.Cu" signal) + (32 "B.Adhes" user "B.Adhesive") + (33 "F.Adhes" user "F.Adhesive") + (34 "B.Paste" user) + (35 "F.Paste" user) + (36 "B.SilkS" user "B.Silkscreen") + (37 "F.SilkS" user "F.Silkscreen") + (38 "B.Mask" user) + (39 "F.Mask" user) + (40 "Dwgs.User" user "User.Drawings") + (41 "Cmts.User" user "User.Comments") + (42 "Eco1.User" user "User.Eco1") + (43 "Eco2.User" user "User.Eco2") + (44 "Edge.Cuts" user) + (45 "Margin" user) + (46 "B.CrtYd" user "B.Courtyard") + (47 "F.CrtYd" user "F.Courtyard") + (48 "B.Fab" user) + (49 "F.Fab" user) + (50 "User.1" user) + (51 "User.2" user) + (52 "User.3" user) + (53 "User.4" user) + (54 "User.5" user) + (55 "User.6" user) + (56 "User.7" user) + (57 "User.8" user) + (58 "User.9" user) + ) + + (setup + (pad_to_mask_clearance 0) + (pcbplotparams + (layerselection 0x00010fc_ffffffff) + (plot_on_all_layers_selection 0x0000000_00000000) + (disableapertmacros false) + (usegerberextensions false) + (usegerberattributes true) + (usegerberadvancedattributes true) + (creategerberjobfile true) + (dashed_line_dash_ratio 12.000000) + (dashed_line_gap_ratio 3.000000) + (svgprecision 4) + (plotframeref false) + (viasonmask false) + (mode 1) + (useauxorigin false) + (hpglpennumber 1) + (hpglpenspeed 20) + (hpglpendiameter 15.000000) + (dxfpolygonmode true) + (dxfimperialunits true) + (dxfusepcbnewfont true) + (psnegative false) + (psa4output false) + (plotreference true) + (plotvalue true) + (plotinvisibletext false) + (sketchpadsonfab false) + (subtractmaskfromsilk false) + (outputformat 1) + (mirror false) + (drillshape 1) + (scaleselection 1) + (outputdirectory "") + ) + ) + + (net 0 "") + + (gr_line (start 59 35) (end 59 43) + (stroke (width 0.05) (type default)) (layer "Edge.Cuts") (tstamp 1a567b5b-971c-4afb-a619-c401fcf8a44e)) + (gr_line (start 59 43) (end 43 43) + (stroke (width 0.05) (type default)) (layer "Edge.Cuts") (tstamp 5056c2d9-f717-42bb-8113-318d910b504d)) + (gr_line (start 33 33) (end 33 25) + (stroke (width 0.05) (type default)) (layer "Edge.Cuts") (tstamp 68d1f2f3-85fe-4625-b285-bc3431b96d8f)) + (gr_arc (start 49 25) (mid 56.071068 27.928932) (end 59 35) + (stroke (width 0.05) (type default)) (layer "Edge.Cuts") (tstamp 697dd226-35eb-455d-a79a-10d11dd9dbd1)) + (gr_arc (start 43 43) (mid 35.928932 40.071068) (end 33 33) + (stroke (width 0.05) (type default)) (layer "Edge.Cuts") (tstamp 8cea329e-1099-4a38-8292-55c4bd39d46b)) + (gr_line (start 33 25) (end 49 25) + (stroke (width 0.05) (type default)) (layer "Edge.Cuts") (tstamp aee5a981-e1d9-405d-a748-40680c7f708c)) + +) diff --git a/crates/kicad-parser/src/board.rs b/crates/kicad-parser/src/board.rs new file mode 100644 index 00000000..14509dcc --- /dev/null +++ b/crates/kicad-parser/src/board.rs @@ -0,0 +1,271 @@ +use crate::{extract_number, Error}; +use sexp::{Atom, Sexp}; +use std::path::Path; + +use crate::graphics::{GraphicArc, GraphicCircle, GraphicLine, GraphicRect}; + +#[derive(Debug, Clone, Default)] +pub struct KicadBoard { + graphic_lines: Vec, + graphic_arcs: Vec, + graphic_circles: Vec, + graphic_rects: Vec, + footprints: Vec, +} + +impl KicadBoard { + pub fn from_file>(file: P) -> Result { + let kicad_board_str = std::fs::read_to_string(&file)?; + let sexp = sexp::parse(&kicad_board_str)?; + + let Sexp::List(list) = sexp else { + return Err(Error::TopLevelObjectNotList); + }; + + let Sexp::Atom(Atom::S(head)) = &list[0] else { + return Err(Error::FirstElementInListNotString); + }; + + match head.as_str() { + "kicad_pcb" => { + let board_fields = &list[1..]; + Ok(Self::handle_board_fields(board_fields)?) + }, + _ => Err(Error::NotKicadPcbFile), + } + } + + pub fn footprints(&self) -> impl Iterator { + self.footprints.iter() + } + + pub fn lines(&self) -> impl Iterator { + self.graphic_lines.iter() + } + + pub fn arcs(&self) -> impl Iterator { + self.graphic_arcs.iter() + } + + pub fn circles(&self) -> impl Iterator { + self.graphic_circles.iter() + } + + pub fn rects(&self) -> impl Iterator { + self.graphic_rects.iter() + } + + fn handle_board_fields(fields: &[Sexp]) -> Result { + let mut board = Self::default(); + + for field in fields { + let Sexp::List(list) = field else { + continue; + }; + + let Sexp::Atom(Atom::S(head)) = &list[0] else { + continue; + }; + + let rest = &list[1..]; + + match head.as_str() { + "version" => {}, + "generator" => {}, + "general" => {}, + "paper" => {}, + "layers" => {}, + "footprint" => { + let footprint = Footprint::from_list(rest)?; + board.footprints.push(footprint); + }, + "gr_arc" => { + let arc = GraphicArc::from_list(rest)?; + board.graphic_arcs.push(arc); + }, + "gr_line" => { + let line = GraphicLine::from_list(rest)?; + board.graphic_lines.push(line); + }, + "gr_circle" => { + let line = GraphicCircle::from_list(rest)?; + board.graphic_circles.push(line); + }, + "gr_rect" => { + let line = GraphicRect::from_list(rest)?; + board.graphic_rects.push(line); + }, + _ => {}, + } + } + + Ok(board) + } +} + +#[derive(Debug, Clone, Default)] +pub struct Footprint { + pub location: (f64, f64), + pub rotation_degrees: f64, + graphic_lines: Vec, + graphic_arcs: Vec, +} + +impl Footprint { + pub fn from_list(list: &[Sexp]) -> Result { + let mut footprint = Self::default(); + + for field in list { + let Sexp::List(list) = field else { + continue; + }; + + let Sexp::Atom(Atom::S(head)) = &list[0] else { + continue; + }; + + let rest = &list[1..]; + + match head.as_str() { + "at" => match rest { + [x, y] => { + let x = extract_number(x)?; + let y = extract_number(y)?; + footprint.location = (x, y); + }, + [x, y, rotation_degrees] => { + let x = extract_number(x)?; + let y = extract_number(y)?; + let rotation_degrees = extract_number(rotation_degrees)?; + + footprint.location = (x, y); + footprint.rotation_degrees = rotation_degrees; + }, + _ => {}, + }, + "fp_arc" => { + let arc = GraphicArc::from_list(rest)?; + footprint.graphic_arcs.push(arc); + }, + "fp_line" => { + let line = GraphicLine::from_list(rest)?; + footprint.graphic_lines.push(line); + }, + _ => {}, + } + } + + Ok(footprint) + } + + pub fn lines(&self) -> impl Iterator { + // TODO - map from footprint space to world space + self.graphic_lines.iter() + } + + pub fn arcs(&self) -> impl Iterator { + // TODO - map from footprint space to world space + self.graphic_arcs.iter() + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum BoardLayer { + FCu, + BCu, + FAdhes, + BAdhes, + FPaste, + BPaste, + FSilkS, + BSilkS, + FMask, + BFask, + DwgsUser, + CmtsUser, + Eco1User, + Eco2User, + EdgeCuts, + Margin, + BCrtYd, + FCrtYd, + BFab, + FFab, + In1Cu, + In2Cu, + In3Cu, + In4Cu, + User(String), +} + +impl From<&str> for BoardLayer { + fn from(s: &str) -> Self { + match s { + "F.Cu" => BoardLayer::FCu, + "B.Cu" => BoardLayer::BCu, + "F.Adhes" => BoardLayer::FAdhes, + "B.Adhes" => BoardLayer::BAdhes, + "F.Paste" => BoardLayer::FPaste, + "B.Paste" => BoardLayer::BPaste, + "F.SilkS" => BoardLayer::FSilkS, + "B.SilkS" => BoardLayer::BSilkS, + "F.Mask" => BoardLayer::FMask, + "B.Mask" => BoardLayer::BFask, + "Dwgs.User" => BoardLayer::DwgsUser, + "Cmts.User" => BoardLayer::CmtsUser, + "Eco1.User" => BoardLayer::Eco1User, + "Eco2.User" => BoardLayer::Eco2User, + "Edge.Cuts" => BoardLayer::EdgeCuts, + "Margin" => BoardLayer::Margin, + "B.CrtYd" => BoardLayer::BCrtYd, + "F.CrtYd" => BoardLayer::FCrtYd, + "B.Fab" => BoardLayer::BFab, + "F.Fab" => BoardLayer::FFab, + "In1.Cu" => BoardLayer::In1Cu, + "In2.Cu" => BoardLayer::In2Cu, + "In3.Cu" => BoardLayer::In3Cu, + "In4.Cu" => BoardLayer::In4Cu, + _ => BoardLayer::User(s.to_string()), + } + } +} + +impl std::str::FromStr for BoardLayer { + type Err = (); + + fn from_str(s: &str) -> std::result::Result { + Ok(Self::from(s)) + } +} + +impl<'a> From<&'a BoardLayer> for &'a str { + fn from(layer: &'a BoardLayer) -> Self { + match *layer { + BoardLayer::FCu => "F.Cu", + BoardLayer::BCu => "B.Cu", + BoardLayer::FAdhes => "F.Adhes", + BoardLayer::BAdhes => "B.Adhes", + BoardLayer::FPaste => "F.Paste", + BoardLayer::BPaste => "B.Paste", + BoardLayer::FSilkS => "F.SilkS", + BoardLayer::BSilkS => "B.SilkS", + BoardLayer::FMask => "F.Mask", + BoardLayer::BFask => "B.Mask", + BoardLayer::DwgsUser => "Dwgs.User", + BoardLayer::CmtsUser => "Cmts.User", + BoardLayer::Eco1User => "Eco1.User", + BoardLayer::Eco2User => "Eco2.User", + BoardLayer::EdgeCuts => "Edge.Cuts", + BoardLayer::Margin => "Margin", + BoardLayer::BCrtYd => "B.CrtYd", + BoardLayer::FCrtYd => "F.CrtYd", + BoardLayer::BFab => "B.Fab", + BoardLayer::FFab => "F.Fab", + BoardLayer::In1Cu => "In1.Cu", + BoardLayer::In2Cu => "In2.Cu", + BoardLayer::In3Cu => "In3.Cu", + BoardLayer::In4Cu => "In4.Cu", + BoardLayer::User(ref s) => s, + } + } +} diff --git a/crates/kicad-parser/src/graphics.rs b/crates/kicad-parser/src/graphics.rs new file mode 100644 index 00000000..a03c088a --- /dev/null +++ b/crates/kicad-parser/src/graphics.rs @@ -0,0 +1,237 @@ +use crate::{extract_coords, Error}; +use sexp::{Atom, Sexp}; + +use crate::board::BoardLayer; + +#[derive(Debug, Clone, Default, PartialEq)] +pub struct GraphicLine { + start: (f64, f64), + end: (f64, f64), + layer: String, +} + +impl GraphicLine { + pub fn from_list(list: &[Sexp]) -> Result { + let mut line = Self::default(); + + for field in list { + let Sexp::List(list) = field else { + continue; + }; + + let Sexp::Atom(Atom::S(head)) = &list[0] else { + continue; + }; + + let rest = &list[1..]; + + match head.as_str() { + "start" => { + let coords = extract_coords(&rest[0], &rest[1])?; + line.start = coords; + }, + "end" => { + let coords = extract_coords(&rest[0], &rest[1])?; + line.end = coords; + }, + "layer" => { + if let Sexp::Atom(Atom::S(layer)) = &rest[0] { + line.layer = layer.to_string(); + } + }, + _ => {}, + } + } + + Ok(line) + } + + pub fn start_point(&self) -> (f64, f64) { + self.start + } + + pub fn end_point(&self) -> (f64, f64) { + self.end + } + + pub fn layer(&self) -> BoardLayer { + BoardLayer::from(self.layer.as_str()) + } +} + +#[derive(Debug, Clone, Default, PartialEq)] +pub struct GraphicArc { + start: (f64, f64), + mid: (f64, f64), + end: (f64, f64), + layer: String, +} + +impl GraphicArc { + pub fn from_list(list: &[Sexp]) -> Result { + let mut line = Self::default(); + + for field in list { + let Sexp::List(list) = field else { + continue; + }; + + let Sexp::Atom(Atom::S(head)) = &list[0] else { + continue; + }; + + let rest = &list[1..]; + + match head.as_str() { + "start" => { + let coords = extract_coords(&rest[0], &rest[1])?; + line.start = coords; + }, + "mid" => { + let coords = extract_coords(&rest[0], &rest[1])?; + line.mid = coords; + }, + "end" => { + let coords = extract_coords(&rest[0], &rest[1])?; + line.end = coords; + }, + "layer" => { + if let Sexp::Atom(Atom::S(layer)) = &rest[0] { + line.layer = layer.to_string(); + } + }, + _ => {}, + } + } + + Ok(line) + } + + pub fn start_point(&self) -> (f64, f64) { + self.start + } + + pub fn mid_point(&self) -> (f64, f64) { + self.mid + } + + pub fn end_point(&self) -> (f64, f64) { + self.end + } + + pub fn layer(&self) -> BoardLayer { + BoardLayer::from(self.layer.as_str()) + } +} + +#[derive(Debug, Clone, Default, PartialEq)] +pub struct GraphicCircle { + center: (f64, f64), + end: (f64, f64), + layer: String, +} + +impl GraphicCircle { + pub fn from_list(list: &[Sexp]) -> Result { + let mut line = Self::default(); + + for field in list { + let Sexp::List(list) = field else { + continue; + }; + + let Sexp::Atom(Atom::S(head)) = &list[0] else { + continue; + }; + + let rest = &list[1..]; + + match head.as_str() { + "center" => { + let coords = extract_coords(&rest[0], &rest[1])?; + line.center = coords; + }, + "end" => { + let coords = extract_coords(&rest[0], &rest[1])?; + line.end = coords; + }, + "layer" => { + if let Sexp::Atom(Atom::S(layer)) = &rest[0] { + line.layer = layer.to_string(); + } + }, + _ => {}, + } + } + + Ok(line) + } + + pub fn center_point(&self) -> (f64, f64) { + self.center + } + + pub fn end_point(&self) -> (f64, f64) { + self.end + } + + pub fn layer(&self) -> BoardLayer { + BoardLayer::from(self.layer.as_str()) + } +} + +#[derive(Debug, Clone, Default, PartialEq)] +pub struct GraphicRect { + start: (f64, f64), + end: (f64, f64), + layer: String, +} + +impl GraphicRect { + pub fn from_list(list: &[Sexp]) -> Result { + let mut line = Self::default(); + + for field in list { + let Sexp::List(list) = field else { + continue; + }; + + let Sexp::Atom(Atom::S(head)) = &list[0] else { + continue; + }; + + let rest = &list[1..]; + + match head.as_str() { + "start" => { + let coords = extract_coords(&rest[0], &rest[1])?; + line.start = coords; + }, + "end" => { + let coords = extract_coords(&rest[0], &rest[1])?; + line.end = coords; + }, + "layer" => { + if let Sexp::Atom(Atom::S(layer)) = &rest[0] { + line.layer = layer.to_string(); + } + }, + _ => {}, + } + } + + Ok(line) + } + + pub fn start_point(&self) -> (f64, f64) { + self.start + } + + pub fn end_point(&self) -> (f64, f64) { + self.end + } + + pub fn layer(&self) -> BoardLayer { + BoardLayer::from(self.layer.as_str()) + } +} diff --git a/crates/kicad-parser/src/lib.rs b/crates/kicad-parser/src/lib.rs new file mode 100644 index 00000000..ea791b5f --- /dev/null +++ b/crates/kicad-parser/src/lib.rs @@ -0,0 +1,33 @@ +use sexp::{Atom, Sexp}; +use thiserror::Error; + +pub mod board; +pub mod graphics; + +#[derive(Error, Debug)] +pub enum Error { + #[error("IO Error: {0}")] + IoError(#[from] std::io::Error), + #[error("S-Expression Parse Error: {0}")] + SexpParseError(#[from] Box), + #[error("Top level object is not a list")] + TopLevelObjectNotList, + #[error("First element in the top level list should be a string")] + FirstElementInListNotString, + #[error("The file is not a kicad_pcb file")] + NotKicadPcbFile, + #[error("Tried to extract a number which is not a float or an int")] + NumberShouldBeFloatOrInt, +} + +fn extract_number(num: &Sexp) -> Result { + match num { + Sexp::Atom(Atom::F(float)) => Ok(*float), + Sexp::Atom(Atom::I(int)) => Ok(*int as f64), + _ => Err(Error::NumberShouldBeFloatOrInt), + } +} + +fn extract_coords(x: &Sexp, y: &Sexp) -> Result<(f64, f64), Error> { + Ok((extract_number(x)?, extract_number(y)?)) +} diff --git a/crates/opencascade/Cargo.toml b/crates/opencascade/Cargo.toml index 6acd46d6..7ce9cef1 100644 --- a/crates/opencascade/Cargo.toml +++ b/crates/opencascade/Cargo.toml @@ -11,6 +11,7 @@ repository = "https://github.com/bschwind/opencascade-rs" cxx = "1" opencascade-sys = { version = "0.2", path = "../opencascade-sys" } glam = { version = "0.24", features = ["bytemuck"] } +kicad-parser = { path = "../kicad-parser" } thiserror = "1" [features] diff --git a/crates/opencascade/src/kicad.rs b/crates/opencascade/src/kicad.rs new file mode 100644 index 00000000..eff7d095 --- /dev/null +++ b/crates/opencascade/src/kicad.rs @@ -0,0 +1,113 @@ +use crate::{ + angle::ToAngle, + primitives::{Edge, EdgeConnection, Face, Wire}, + workplane::Workplane, + Error, +}; +use glam::DVec2; +use kicad_parser::{ + board::{BoardLayer, KicadBoard}, + graphics::{GraphicArc, GraphicCircle, GraphicLine, GraphicRect}, +}; +use std::path::Path; + +impl From<&GraphicLine> for Edge { + fn from(line: &GraphicLine) -> Edge { + let start = DVec2::from(line.start_point()); + let end = DVec2::from(line.end_point()); + Edge::segment(start.extend(0.0), end.extend(0.0)) + } +} + +impl From<&GraphicArc> for Edge { + fn from(arc: &GraphicArc) -> Edge { + let start = DVec2::from(arc.start_point()); + let mid = DVec2::from(arc.mid_point()); + let end = DVec2::from(arc.end_point()); + Edge::arc(start.extend(0.0), mid.extend(0.0), end.extend(0.0)) + } +} + +impl From<&GraphicCircle> for Face { + fn from(circle: &GraphicCircle) -> Face { + let center = DVec2::from(circle.center_point()); + let end = DVec2::from(circle.end_point()); + + let delta = (center - end).abs(); + + let radius = (delta.x * delta.x + delta.y * delta.y).sqrt(); + Workplane::xy().translated(center.extend(0.0)).circle(center.x, center.y, radius).to_face() + } +} + +impl From<&GraphicRect> for Face { + fn from(rect: &GraphicRect) -> Face { + let start = DVec2::from(rect.start_point()); + let end = DVec2::from(rect.end_point()); + + let dimensions = (end - start).abs(); + Workplane::xy().translated(start.extend(0.0)).rect(dimensions.x, dimensions.y).to_face() + } +} + +pub struct KicadPcb { + board: KicadBoard, +} + +impl KicadPcb { + pub fn from_file>(file: P) -> Result { + Ok(Self { board: KicadBoard::from_file(file)? }) + } + + pub fn edge_cuts(&self) -> Wire { + Wire::from_unordered_edges( + self.layer_edges(&BoardLayer::EdgeCuts), + EdgeConnection::default(), + ) + } + + pub fn layer_edges<'a>(&'a self, layer: &'a BoardLayer) -> impl Iterator + '_ { + let footprint_edges = self.board.footprints().flat_map(|footprint| { + let angle = footprint.rotation_degrees.degrees(); + // TODO(bschwind) - Document why a negative angle is needed here. + let angle_vec = DVec2::from_angle(-angle.radians()); + let translate = DVec2::from(footprint.location); + + footprint + .lines() + .filter(|line| line.layer() == *layer) + .map(move |line| { + let start = line.start_point(); + let end = line.end_point(); + let start = DVec2::from(start); + let end = DVec2::from(end); + + let start = translate + angle_vec.rotate(start); + let end = translate + angle_vec.rotate(end); + + Edge::segment(start.extend(0.0), end.extend(0.0)) + }) + .chain(footprint.arcs().filter(|arc| arc.layer() == *layer).map(move |arc| { + let start = arc.start_point(); + let mid = arc.mid_point(); + let end = arc.end_point(); + let start = DVec2::from(start); + let mid = DVec2::from(mid); + let end = DVec2::from(end); + + let start = translate + angle_vec.rotate(start); + let mid = translate + angle_vec.rotate(mid); + let end = translate + angle_vec.rotate(end); + + Edge::arc(start.extend(0.0), mid.extend(0.0), end.extend(0.0)) + })) + }); + + self.board + .lines() + .filter(|line| line.layer() == *layer) + .map(Edge::from) + .chain(self.board.arcs().filter(|arc| arc.layer() == *layer).map(Edge::from)) + .chain(footprint_edges) + } +} diff --git a/crates/opencascade/src/lib.rs b/crates/opencascade/src/lib.rs index 7a04ed46..85bb84fd 100644 --- a/crates/opencascade/src/lib.rs +++ b/crates/opencascade/src/lib.rs @@ -1,6 +1,7 @@ use thiserror::Error; pub mod angle; +pub mod kicad; pub mod mesh; pub mod primitives; pub mod workplane; @@ -11,6 +12,8 @@ pub enum Error { StlWriteFailed, #[error("failed to read STEP file")] StepReadFailed, + #[error("failed to read KiCAD PCB file: {0}")] + KicadReadFailed(#[from] kicad_parser::Error), #[error("failed to write STEP file")] StepWriteFailed, #[error("failed to triangulate Shape")] diff --git a/crates/opencascade/src/primitives/wire.rs b/crates/opencascade/src/primitives/wire.rs index 63022122..9a42ac0b 100644 --- a/crates/opencascade/src/primitives/wire.rs +++ b/crates/opencascade/src/primitives/wire.rs @@ -79,14 +79,14 @@ impl Wire { Self::from_make_wire(make_wire) } - pub fn from_unordered_edges<'a>( - unordered_edges: impl IntoIterator, + pub fn from_unordered_edges>( + unordered_edges: impl IntoIterator, edge_connection: EdgeConnection, ) -> Self { let mut edges = ffi::new_Handle_TopTools_HSequenceOfShape(); for edge in unordered_edges { - let edge_shape = ffi::cast_edge_to_shape(&edge.inner); + let edge_shape = ffi::cast_edge_to_shape(&edge.as_ref().inner); ffi::TopTools_HSequenceOfShape_append(edges.pin_mut(), edge_shape); } diff --git a/crates/viewer/Cargo.toml b/crates/viewer/Cargo.toml index fb696ba8..632b4adb 100644 --- a/crates/viewer/Cargo.toml +++ b/crates/viewer/Cargo.toml @@ -9,6 +9,7 @@ bytemuck = { version = "1", features = ["derive"] } clap = { version = "4", features = ["derive"] } examples = { path = "../../examples", default-features = false } glam = { version = "0.24", features = ["bytemuck"] } +kicad-parser = { path = "../kicad-parser" } opencascade = { version = "0.2", path = "../opencascade", default-features = false } simple-game = { git = "https://github.com/bschwind/simple-game.git", rev = "651a57a9f28b0707afbb5a43fe1cbc8f6755169c", default-features = false } smaa = "0.12" diff --git a/crates/viewer/src/main.rs b/crates/viewer/src/main.rs index 61f6734f..f09565b7 100644 --- a/crates/viewer/src/main.rs +++ b/crates/viewer/src/main.rs @@ -6,7 +6,11 @@ use anyhow::Error; use camera::OrbitCamera; use clap::{Parser, ValueEnum}; use glam::{vec2, vec3, DVec3, Mat4, Quat, Vec2, Vec3}; -use opencascade::primitives::Shape; +use kicad_parser::board::BoardLayer; +use opencascade::{ + kicad::KicadPcb, + primitives::{IntoShape, Shape}, +}; use simple_game::{ graphics::{ text::{AxisAlign, StyledText, TextAlignment, TextSystem}, @@ -81,6 +85,9 @@ struct AppArgs { #[arg(long, group = "model")] step_file: Option, + #[arg(long, group = "model")] + kicad_file: Option, + #[arg(long, value_enum, group = "model")] example: Option, } @@ -132,6 +139,16 @@ impl GameApp for ViewerApp { let shape = if let Some(step_file) = args.step_file { Shape::read_step(step_file).expect("Failed to read STEP file, {step_file}") + } else if let Some(kicad_file) = args.kicad_file { + // Parse the kicad file, turn it into a face, extrude it by 1.6mm + let pcb = + KicadPcb::from_file(kicad_file).expect("Failed to read KiCAD PCB file, kicad_file"); + + // Temporary - Unions all edges together to display without connecting them. + let edges = pcb.layer_edges(&BoardLayer::EdgeCuts); + edges.map(|edge| edge.into_shape()).reduce(|acc, edge| acc.union(&edge).into()).unwrap() + + // pcb.edge_cuts().to_face().extrude(glam::dvec3(0.0, 0.0, 1.6)).into() } else if let Some(example) = args.example { example.shape() } else {