From 894f5287936c9b223b61d72c1c9a8dd208124fc8 Mon Sep 17 00:00:00 2001 From: Brian Schwind Date: Wed, 19 Jul 2023 17:40:11 +0900 Subject: [PATCH 01/16] Add the beginning of a very simplistic kicad board file parser --- Cargo.toml | 3 +- crates/kicad-parser/Cargo.toml | 8 ++ .../sample-files/sample.kicad_pcb | 83 +++++++++++ crates/kicad-parser/src/main.rs | 133 ++++++++++++++++++ crates/occt-sys/OCCT | 2 +- 5 files changed, 227 insertions(+), 2 deletions(-) create mode 100644 crates/kicad-parser/Cargo.toml create mode 100644 crates/kicad-parser/sample-files/sample.kicad_pcb create mode 100644 crates/kicad-parser/src/main.rs 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..35e37f51 --- /dev/null +++ b/crates/kicad-parser/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "kicad-parser" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "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..0f200ecd --- /dev/null +++ b/crates/kicad-parser/sample-files/sample.kicad_pcb @@ -0,0 +1,83 @@ +(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) + ) + + (gr_line (start 174.8 55.1) (end 174.35 55.55) + (stroke (width 0.1) (type default)) (layer "B.SilkS") (tstamp 0baaefa6-77a5-4afb-bf3f-9e0db0a584be)) + (gr_line (start 157 56.35) (end 157 56.1) + (stroke (width 0.1) (type default)) (layer "B.SilkS") (tstamp 209e05e1-97c1-43a4-bbae-3c88186d7e8e)) + (gr_line (start 174.35 55.55) (end 174.35 55.2) + (stroke (width 0.1) (type default)) (layer "B.SilkS") (tstamp 40dcca91-2392-4dac-b749-9e6fce3ee42b)) + (gr_line (start 156.55 55.9) (end 157 56.35) + (stroke (width 0.1) (type default)) (layer "B.SilkS") (tstamp e8c228f2-4fe7-4f97-a224-f4aa9deeed51)) + (gr_line (start 185 71) (end 151 71) + (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp 0d0b18b8-29e1-400d-9fe0-0c553320d369)) + (gr_line (start 188.950546 53.08) (end 188.950546 47.524454) + (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp 19f9a363-e864-4747-9205-86bc18fec056)) + (gr_arc (start 188.675546 47.249454) (mid 188.87 47.33) (end 188.950546 47.524454) + (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp 27a9dd0c-17b3-494e-8eb1-5591d63b4214)) + (gr_arc (start 188 68) (mid 187.12132 70.12132) (end 185 71) + (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp 2b4447d8-cc2c-42c4-8c3c-94373f69b331)) + (gr_arc (start 148 41) (mid 148.87868 38.87868) (end 151 38) + (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp 2c508737-2daa-4bed-82b0-cefc47998f5f)) + (gr_line (start 188 46.973866) (end 188 41) + (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp 2fc0c9d8-bd5d-4598-b05c-649a87b15bd8)) + (gr_arc (start 188.950546 53.08) (mid 188.87 53.274454) (end 188.675546 53.355) + (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp 32fa687c-3f1e-4171-8891-b3caae4ab217)) + (gr_line (start 151 38) (end 185 38) + (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp ae1d430b-8a10-4b47-a76e-f4f7ab7d79a1)) + (gr_line (start 188 53.64) (end 188 68) + (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp bece4ada-f6bb-41b4-abb7-f17f91e74bb7)) + (gr_arc (start 151 71) (mid 148.87868 70.12132) (end 148 68) + (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp c7ad8427-669a-40f7-b224-92bcde4ab5a8)) + (gr_line (start 148 68) (end 148 41) + (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp cc9c2e93-e0ca-429c-89b1-1b471ece0c41)) + (gr_arc (start 185 38) (mid 187.12132 38.87868) (end 188 41) + (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp d543461e-372c-4d57-90d6-704eeadbb4df)) + (gr_line (start 188.675546 53.355588) (end 188.275 53.355588) + (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp da1c4d49-6186-46e5-8107-f3f3d10f3ac9)) + (gr_arc (start 188 53.630588) (mid 188.080546 53.436134) (end 188.275 53.355588) + (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp e6f929d5-29b3-4daf-96c5-8cca7ca35c0d)) + (gr_line (start 188.275 47.248866) (end 188.675546 47.248866) + (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp eb3fa897-53c6-40af-9ce5-21313d913df4)) + (gr_arc (start 188.275 47.248866) (mid 188.080546 47.16832) (end 188 46.973866) + (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp f279b724-e617-43d5-8412-ff57704a025f)) + +) \ No newline at end of file diff --git a/crates/kicad-parser/src/main.rs b/crates/kicad-parser/src/main.rs new file mode 100644 index 00000000..7d6db96b --- /dev/null +++ b/crates/kicad-parser/src/main.rs @@ -0,0 +1,133 @@ +use anyhow::{anyhow, Context, Result}; +use sexp::{Atom, Sexp}; +use std::path::Path; + +fn main() -> Result<()> { + let Some(input_file) = std::env::args().nth(1) else { + return Err(anyhow!("Usage: kicad-parser ")); + }; + + let board = KicadBoard::from_file(input_file)?; + + dbg!(board); + + Ok(()) +} + +#[derive(Debug, Clone, Default)] +pub struct KicadBoard { + graphic_lines: Vec, +} + +impl KicadBoard { + pub fn from_file>(file: P) -> Result { + let kicad_board_str = std::fs::read_to_string(&file) + .context(format!("Reading {:?}", file.as_ref().to_string_lossy()))?; + let sexp = sexp::parse(&kicad_board_str)?; + + let Sexp::List(list) = sexp else { + return Err(anyhow!("Top level file wasn't a list")); + }; + + let Sexp::Atom(Atom::S(head)) = &list[0] else { + return Err(anyhow!("First element in the top level list should be a string")); + }; + + match head.as_str() { + "kicad_pcb" => { + let board_fields = &list[1..]; + Ok(Self::handle_board_fields(board_fields)?) + }, + _ => Err(anyhow!("Invalid top-level file type - expected 'kicad_pcb'")), + } + } + + 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" => {}, + "gr_arc" => {}, + "gr_line" => { + let line = GraphicLine::from_list(rest)?; + board.graphic_lines.push(line); + }, + _ => {}, + } + } + + Ok(board) + } +} + +#[derive(Debug, Clone, Default)] +pub struct GraphicLine { + start: (f64, f64), + end: (f64, f64), + layer: String, +} + +impl GraphicLine { + 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) + } +} + +fn extract_coords(x: &Sexp, y: &Sexp) -> Result<(f64, f64)> { + Ok((extract_number(x)?, extract_number(y)?)) +} + +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(anyhow!("Expected a number to be a float or integer")), + } +} diff --git a/crates/occt-sys/OCCT b/crates/occt-sys/OCCT index ffce0d66..b079fb98 160000 --- a/crates/occt-sys/OCCT +++ b/crates/occt-sys/OCCT @@ -1 +1 @@ -Subproject commit ffce0d66bbaafe3a95984d0e61804c201b9995d2 +Subproject commit b079fb9877ef64d4a8158a60fa157f59b096debb From b1c827f6d7212d18d542f94a0e746c80ffebabb2 Mon Sep 17 00:00:00 2001 From: John Whittington Date: Sat, 22 Jul 2023 17:33:21 +0200 Subject: [PATCH 02/16] more graphics; to primatives for graphics --- crates/kicad-parser/Cargo.toml | 2 + .../sample-files/sample.kicad_pcb | 100 +++--- crates/kicad-parser/src/board.rs | 207 +++++++++++++ crates/kicad-parser/src/graphics.rs | 287 ++++++++++++++++++ crates/kicad-parser/src/main.rs | 135 +------- 5 files changed, 563 insertions(+), 168 deletions(-) create mode 100644 crates/kicad-parser/src/board.rs create mode 100644 crates/kicad-parser/src/graphics.rs diff --git a/crates/kicad-parser/Cargo.toml b/crates/kicad-parser/Cargo.toml index 35e37f51..a4906137 100644 --- a/crates/kicad-parser/Cargo.toml +++ b/crates/kicad-parser/Cargo.toml @@ -6,3 +6,5 @@ edition = "2021" [dependencies] anyhow = "1" sexp = "1.1.4" +opencascade = { version = "0.1", path = "../opencascade" } +glam = { version = "0.23", features = ["bytemuck"] } diff --git a/crates/kicad-parser/sample-files/sample.kicad_pcb b/crates/kicad-parser/sample-files/sample.kicad_pcb index 0f200ecd..10f00f3e 100644 --- a/crates/kicad-parser/sample-files/sample.kicad_pcb +++ b/crates/kicad-parser/sample-files/sample.kicad_pcb @@ -1,12 +1,10 @@ -(kicad_pcb - (version 20221018) - (generator pcbnew) +(kicad_pcb (version 20221018) (generator pcbnew) + (general (thickness 0.89) ) (paper "A4") - (layers (0 "F.Cu" signal) (31 "B.Cu" signal) @@ -39,45 +37,57 @@ (58 "User.9" user) ) - (gr_line (start 174.8 55.1) (end 174.35 55.55) - (stroke (width 0.1) (type default)) (layer "B.SilkS") (tstamp 0baaefa6-77a5-4afb-bf3f-9e0db0a584be)) - (gr_line (start 157 56.35) (end 157 56.1) - (stroke (width 0.1) (type default)) (layer "B.SilkS") (tstamp 209e05e1-97c1-43a4-bbae-3c88186d7e8e)) - (gr_line (start 174.35 55.55) (end 174.35 55.2) - (stroke (width 0.1) (type default)) (layer "B.SilkS") (tstamp 40dcca91-2392-4dac-b749-9e6fce3ee42b)) - (gr_line (start 156.55 55.9) (end 157 56.35) - (stroke (width 0.1) (type default)) (layer "B.SilkS") (tstamp e8c228f2-4fe7-4f97-a224-f4aa9deeed51)) - (gr_line (start 185 71) (end 151 71) - (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp 0d0b18b8-29e1-400d-9fe0-0c553320d369)) - (gr_line (start 188.950546 53.08) (end 188.950546 47.524454) - (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp 19f9a363-e864-4747-9205-86bc18fec056)) - (gr_arc (start 188.675546 47.249454) (mid 188.87 47.33) (end 188.950546 47.524454) - (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp 27a9dd0c-17b3-494e-8eb1-5591d63b4214)) - (gr_arc (start 188 68) (mid 187.12132 70.12132) (end 185 71) - (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp 2b4447d8-cc2c-42c4-8c3c-94373f69b331)) - (gr_arc (start 148 41) (mid 148.87868 38.87868) (end 151 38) - (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp 2c508737-2daa-4bed-82b0-cefc47998f5f)) - (gr_line (start 188 46.973866) (end 188 41) - (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp 2fc0c9d8-bd5d-4598-b05c-649a87b15bd8)) - (gr_arc (start 188.950546 53.08) (mid 188.87 53.274454) (end 188.675546 53.355) - (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp 32fa687c-3f1e-4171-8891-b3caae4ab217)) - (gr_line (start 151 38) (end 185 38) - (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp ae1d430b-8a10-4b47-a76e-f4f7ab7d79a1)) - (gr_line (start 188 53.64) (end 188 68) - (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp bece4ada-f6bb-41b4-abb7-f17f91e74bb7)) - (gr_arc (start 151 71) (mid 148.87868 70.12132) (end 148 68) - (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp c7ad8427-669a-40f7-b224-92bcde4ab5a8)) - (gr_line (start 148 68) (end 148 41) - (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp cc9c2e93-e0ca-429c-89b1-1b471ece0c41)) - (gr_arc (start 185 38) (mid 187.12132 38.87868) (end 188 41) - (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp d543461e-372c-4d57-90d6-704eeadbb4df)) - (gr_line (start 188.675546 53.355588) (end 188.275 53.355588) - (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp da1c4d49-6186-46e5-8107-f3f3d10f3ac9)) - (gr_arc (start 188 53.630588) (mid 188.080546 53.436134) (end 188.275 53.355588) - (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp e6f929d5-29b3-4daf-96c5-8cca7ca35c0d)) - (gr_line (start 188.275 47.248866) (end 188.675546 47.248866) - (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp eb3fa897-53c6-40af-9ce5-21313d913df4)) - (gr_arc (start 188.275 47.248866) (mid 188.080546 47.16832) (end 188 46.973866) - (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp f279b724-e617-43d5-8412-ff57704a025f)) + (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)) -) \ No newline at end of file +) diff --git a/crates/kicad-parser/src/board.rs b/crates/kicad-parser/src/board.rs new file mode 100644 index 00000000..f40289fa --- /dev/null +++ b/crates/kicad-parser/src/board.rs @@ -0,0 +1,207 @@ +use anyhow::{anyhow, Context, Result}; +use sexp::{Atom, Sexp}; +use std::path::Path; +use opencascade::primitives::{Edge, Wire, Face}; + +use crate::graphics::{GraphicLine, GraphicArc, GraphicRect, GraphicCircle}; + +#[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, + } + } +} + +#[derive(Debug, Clone, Default)] +pub struct KicadBoard { + graphic_lines: Vec, + graphic_arcs: Vec, + graphic_circles: Vec, + graphic_rects: Vec, +} + +impl KicadBoard { + pub fn from_file>(file: P) -> Result { + let kicad_board_str = std::fs::read_to_string(&file) + .context(format!("Reading {:?}", file.as_ref().to_string_lossy()))?; + let sexp = sexp::parse(&kicad_board_str)?; + + let Sexp::List(list) = sexp else { + return Err(anyhow!("Top level file wasn't a list")); + }; + + let Sexp::Atom(Atom::S(head)) = &list[0] else { + return Err(anyhow!("First element in the top level list should be a string")); + }; + + match head.as_str() { + "kicad_pcb" => { + let board_fields = &list[1..]; + Ok(Self::handle_board_fields(board_fields)?) + }, + _ => Err(anyhow!("Invalid top-level file type - expected 'kicad_pcb'")), + } + } + + 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" => {}, + "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) + } + + pub fn layer_edges(&self, layer: BoardLayer) -> Vec { + self.graphic_lines.iter() + .filter(|line| line.layer() == layer) + .map(Into::::into) + .chain(self.graphic_arcs.iter() + .filter(|arc| arc.layer() == layer) + .map(Into::::into)) + .collect() + } + + pub fn layer_wire(&self, layer: BoardLayer) -> Wire { + Wire::from_edges(&self.layer_edges(layer)) + } + + pub fn layer_face(&self, layer: BoardLayer) -> Face { + Face::from_wire(&self.layer_wire(layer)) + } + + pub fn outline(&self, _offset: f64) -> Face { + let outline = self.layer_face(BoardLayer::EdgeCuts); + // TODO apply offset around the face + outline + } +} diff --git a/crates/kicad-parser/src/graphics.rs b/crates/kicad-parser/src/graphics.rs new file mode 100644 index 00000000..317b1392 --- /dev/null +++ b/crates/kicad-parser/src/graphics.rs @@ -0,0 +1,287 @@ +use anyhow::{anyhow, Result}; +use sexp::{Atom, Sexp}; +use glam::{dvec3, DVec3}; +use opencascade::primitives::{Edge, Face}; +use opencascade::workplane::Workplane; + +use crate::board::BoardLayer; + +#[derive(Debug, Clone, Default)] +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) -> DVec3 { + dvec3(self.start.0, self.start.1, 0.0) + } + + pub fn end_point(&self) -> DVec3 { + dvec3(self.end.0, self.end.1, 0.0) + } + + pub fn layer(&self) -> BoardLayer { + BoardLayer::from(self.layer.as_str()) + } +} + +impl Into for &GraphicLine { + fn into(self) -> Edge { + Edge::segment(self.start_point(), self.end_point()) + } +} + +#[derive(Debug, Clone, Default)] +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) -> DVec3 { + dvec3(self.start.0, self.start.1, 0.0) + } + + pub fn mid_point(&self) -> DVec3 { + dvec3(self.mid.0, self.mid.1, 0.0) + } + + pub fn end_point(&self) -> DVec3 { + dvec3(self.end.0, self.end.1, 0.0) + } + + pub fn layer(&self) -> BoardLayer { + BoardLayer::from(self.layer.as_str()) + } +} + +impl Into for &GraphicArc { + fn into(self) -> Edge { + Edge::arc(self.start_point(), self.mid_point(), self.end_point()) + } +} + +#[derive(Debug, Clone, Default)] +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) -> DVec3 { + dvec3(self.center.0, self.center.1, 0.0) + } + + pub fn end_point(&self) -> DVec3 { + dvec3(self.end.0, self.end.1, 0.0) + } + + pub fn layer(&self) -> BoardLayer { + BoardLayer::from(self.layer.as_str()) + } +} + +impl Into for &GraphicCircle { + fn into(self) -> Face { + let delta_x = (self.center.0 - self.end.0).abs(); + let delta_y = (self.center.1 - self.end.1).abs(); + let radius = (delta_x * delta_x + delta_y * delta_y).sqrt(); + Workplane::xy() + .translated(self.center_point()) + .circle(self.center.0, self.center.1, radius) + .to_face() + } +} + +#[derive(Debug, Clone, Default)] +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) -> DVec3 { + dvec3(self.start.0, self.start.1, 0.0) + } + + pub fn end_point(&self) -> DVec3 { + dvec3(self.end.0, self.end.1, 0.0) + } + + pub fn layer(&self) -> BoardLayer { + BoardLayer::from(self.layer.as_str()) + } +} + +impl Into for &GraphicRect { + fn into(self) -> Face { + let height = (self.end.1 - self.start.1).abs(); + let width = (self.end.0 - self.start.0).abs(); + Workplane::xy() + .translated(self.start_point()) + .rect(height, width) + .to_face() + } +} + +fn extract_coords(x: &Sexp, y: &Sexp) -> Result<(f64, f64)> { + Ok((extract_number(x)?, extract_number(y)?)) +} + +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(anyhow!("Expected a number to be a float or integer")), + } +} diff --git a/crates/kicad-parser/src/main.rs b/crates/kicad-parser/src/main.rs index 7d6db96b..a857f45b 100644 --- a/crates/kicad-parser/src/main.rs +++ b/crates/kicad-parser/src/main.rs @@ -1,133 +1,22 @@ -use anyhow::{anyhow, Context, Result}; -use sexp::{Atom, Sexp}; -use std::path::Path; +use anyhow::{anyhow, Result}; +use glam::DVec3; + +mod board; +mod graphics; fn main() -> Result<()> { let Some(input_file) = std::env::args().nth(1) else { return Err(anyhow!("Usage: kicad-parser ")); }; - let board = KicadBoard::from_file(input_file)?; + let board = board::KicadBoard::from_file(input_file)?; + let outline = board.outline(0.5); + // let outline: Face = Into::::into(&board.graphic_rects[0]); - dbg!(board); + let solid = outline.extrude(DVec3 { x: 0.0, y: 0.0, z: 10.0 }); + let mut shape = solid.to_shape(); + shape.clean(); + shape.write_stl("outline.stl")?; Ok(()) } - -#[derive(Debug, Clone, Default)] -pub struct KicadBoard { - graphic_lines: Vec, -} - -impl KicadBoard { - pub fn from_file>(file: P) -> Result { - let kicad_board_str = std::fs::read_to_string(&file) - .context(format!("Reading {:?}", file.as_ref().to_string_lossy()))?; - let sexp = sexp::parse(&kicad_board_str)?; - - let Sexp::List(list) = sexp else { - return Err(anyhow!("Top level file wasn't a list")); - }; - - let Sexp::Atom(Atom::S(head)) = &list[0] else { - return Err(anyhow!("First element in the top level list should be a string")); - }; - - match head.as_str() { - "kicad_pcb" => { - let board_fields = &list[1..]; - Ok(Self::handle_board_fields(board_fields)?) - }, - _ => Err(anyhow!("Invalid top-level file type - expected 'kicad_pcb'")), - } - } - - 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" => {}, - "gr_arc" => {}, - "gr_line" => { - let line = GraphicLine::from_list(rest)?; - board.graphic_lines.push(line); - }, - _ => {}, - } - } - - Ok(board) - } -} - -#[derive(Debug, Clone, Default)] -pub struct GraphicLine { - start: (f64, f64), - end: (f64, f64), - layer: String, -} - -impl GraphicLine { - 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) - } -} - -fn extract_coords(x: &Sexp, y: &Sexp) -> Result<(f64, f64)> { - Ok((extract_number(x)?, extract_number(y)?)) -} - -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(anyhow!("Expected a number to be a float or integer")), - } -} From 9b11d457cbb7d6fd5d75ae2b85bbfc7253962936 Mon Sep 17 00:00:00 2001 From: John Whittington Date: Sat, 22 Jul 2023 17:44:31 +0200 Subject: [PATCH 03/16] format --- crates/kicad-parser/src/board.rs | 15 ++++++++------- crates/kicad-parser/src/graphics.rs | 13 ++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/crates/kicad-parser/src/board.rs b/crates/kicad-parser/src/board.rs index f40289fa..febf37f3 100644 --- a/crates/kicad-parser/src/board.rs +++ b/crates/kicad-parser/src/board.rs @@ -1,9 +1,9 @@ use anyhow::{anyhow, Context, Result}; +use opencascade::primitives::{Edge, Face, Wire}; use sexp::{Atom, Sexp}; use std::path::Path; -use opencascade::primitives::{Edge, Wire, Face}; -use crate::graphics::{GraphicLine, GraphicArc, GraphicRect, GraphicCircle}; +use crate::graphics::{GraphicArc, GraphicCircle, GraphicLine, GraphicRect}; #[derive(Debug, Clone, PartialEq)] pub enum BoardLayer { @@ -31,7 +31,7 @@ pub enum BoardLayer { In2Cu, In3Cu, In4Cu, - User(String) + User(String), } impl From<&str> for BoardLayer { @@ -182,12 +182,13 @@ impl KicadBoard { } pub fn layer_edges(&self, layer: BoardLayer) -> Vec { - self.graphic_lines.iter() + self.graphic_lines + .iter() .filter(|line| line.layer() == layer) .map(Into::::into) - .chain(self.graphic_arcs.iter() - .filter(|arc| arc.layer() == layer) - .map(Into::::into)) + .chain( + self.graphic_arcs.iter().filter(|arc| arc.layer() == layer).map(Into::::into), + ) .collect() } diff --git a/crates/kicad-parser/src/graphics.rs b/crates/kicad-parser/src/graphics.rs index 317b1392..da4f8d07 100644 --- a/crates/kicad-parser/src/graphics.rs +++ b/crates/kicad-parser/src/graphics.rs @@ -1,8 +1,10 @@ use anyhow::{anyhow, Result}; -use sexp::{Atom, Sexp}; use glam::{dvec3, DVec3}; -use opencascade::primitives::{Edge, Face}; -use opencascade::workplane::Workplane; +use opencascade::{ + primitives::{Edge, Face}, + workplane::Workplane, +}; +use sexp::{Atom, Sexp}; use crate::board::BoardLayer; @@ -267,10 +269,7 @@ impl Into for &GraphicRect { fn into(self) -> Face { let height = (self.end.1 - self.start.1).abs(); let width = (self.end.0 - self.start.0).abs(); - Workplane::xy() - .translated(self.start_point()) - .rect(height, width) - .to_face() + Workplane::xy().translated(self.start_point()).rect(height, width).to_face() } } From 3e853c85ffb14bab2f101d4be076fa4a7aa46a8f Mon Sep 17 00:00:00 2001 From: John Whittington Date: Sat, 22 Jul 2023 21:12:27 +0200 Subject: [PATCH 04/16] Add PartialEq to various graphics primitives --- crates/kicad-parser/src/graphics.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/kicad-parser/src/graphics.rs b/crates/kicad-parser/src/graphics.rs index da4f8d07..9f2efc10 100644 --- a/crates/kicad-parser/src/graphics.rs +++ b/crates/kicad-parser/src/graphics.rs @@ -8,7 +8,7 @@ use sexp::{Atom, Sexp}; use crate::board::BoardLayer; -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, PartialEq)] pub struct GraphicLine { start: (f64, f64), end: (f64, f64), @@ -70,7 +70,7 @@ impl Into for &GraphicLine { } } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, PartialEq)] pub struct GraphicArc { start: (f64, f64), mid: (f64, f64), @@ -141,7 +141,7 @@ impl Into for &GraphicArc { } } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, PartialEq)] pub struct GraphicCircle { center: (f64, f64), end: (f64, f64), @@ -209,7 +209,7 @@ impl Into for &GraphicCircle { } } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, PartialEq)] pub struct GraphicRect { start: (f64, f64), end: (f64, f64), From 4ff57a948d3961760c7000e863e8508e52b2ef92 Mon Sep 17 00:00:00 2001 From: Brian Schwind Date: Fri, 1 Sep 2023 16:14:15 +0900 Subject: [PATCH 05/16] Update opencascade dependency for kicad-parser --- crates/kicad-parser/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/kicad-parser/Cargo.toml b/crates/kicad-parser/Cargo.toml index a4906137..54d4c60a 100644 --- a/crates/kicad-parser/Cargo.toml +++ b/crates/kicad-parser/Cargo.toml @@ -6,5 +6,5 @@ edition = "2021" [dependencies] anyhow = "1" sexp = "1.1.4" -opencascade = { version = "0.1", path = "../opencascade" } +opencascade = { version = "0.2", path = "../opencascade" } glam = { version = "0.23", features = ["bytemuck"] } From 821db776d11bb1765c6180d57cf7a4592f1eada0 Mon Sep 17 00:00:00 2001 From: Brian Schwind Date: Fri, 1 Sep 2023 16:14:27 +0900 Subject: [PATCH 06/16] Fix a few clippy lints --- crates/kicad-parser/src/board.rs | 5 ++--- crates/kicad-parser/src/main.rs | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/kicad-parser/src/board.rs b/crates/kicad-parser/src/board.rs index febf37f3..d198f6fb 100644 --- a/crates/kicad-parser/src/board.rs +++ b/crates/kicad-parser/src/board.rs @@ -101,7 +101,7 @@ impl<'a> From<&'a BoardLayer> for &'a str { BoardLayer::In2Cu => "In2.Cu", BoardLayer::In3Cu => "In3.Cu", BoardLayer::In4Cu => "In4.Cu", - BoardLayer::User(ref s) => &s, + BoardLayer::User(ref s) => s, } } } @@ -201,8 +201,7 @@ impl KicadBoard { } pub fn outline(&self, _offset: f64) -> Face { - let outline = self.layer_face(BoardLayer::EdgeCuts); // TODO apply offset around the face - outline + self.layer_face(BoardLayer::EdgeCuts) } } diff --git a/crates/kicad-parser/src/main.rs b/crates/kicad-parser/src/main.rs index a857f45b..12a94cdf 100644 --- a/crates/kicad-parser/src/main.rs +++ b/crates/kicad-parser/src/main.rs @@ -1,3 +1,4 @@ +use opencascade::primitives::Shape; use anyhow::{anyhow, Result}; use glam::DVec3; @@ -14,7 +15,7 @@ fn main() -> Result<()> { // let outline: Face = Into::::into(&board.graphic_rects[0]); let solid = outline.extrude(DVec3 { x: 0.0, y: 0.0, z: 10.0 }); - let mut shape = solid.to_shape(); + let mut shape: Shape = solid.into(); shape.clean(); shape.write_stl("outline.stl")?; From 68a64e48924a7c9626927a92bbcdbf4a0d732e9c Mon Sep 17 00:00:00 2001 From: Brian Schwind Date: Sat, 2 Sep 2023 18:21:14 +0900 Subject: [PATCH 07/16] Fix formatting --- crates/kicad-parser/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/kicad-parser/src/main.rs b/crates/kicad-parser/src/main.rs index 12a94cdf..b8583eb8 100644 --- a/crates/kicad-parser/src/main.rs +++ b/crates/kicad-parser/src/main.rs @@ -1,6 +1,6 @@ -use opencascade::primitives::Shape; use anyhow::{anyhow, Result}; use glam::DVec3; +use opencascade::primitives::Shape; mod board; mod graphics; From 0c65c398fee1f6dee165383e85fab1e274eac28c Mon Sep 17 00:00:00 2001 From: Brian Schwind Date: Sat, 2 Sep 2023 18:23:48 +0900 Subject: [PATCH 08/16] Use the new Wire::from_unordered_edges() function for creating a wire out of a BoardLayer --- crates/kicad-parser/src/board.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/kicad-parser/src/board.rs b/crates/kicad-parser/src/board.rs index d198f6fb..93ed4367 100644 --- a/crates/kicad-parser/src/board.rs +++ b/crates/kicad-parser/src/board.rs @@ -1,5 +1,5 @@ use anyhow::{anyhow, Context, Result}; -use opencascade::primitives::{Edge, Face, Wire}; +use opencascade::primitives::{Edge, EdgeConnection, Face, Wire}; use sexp::{Atom, Sexp}; use std::path::Path; @@ -193,7 +193,7 @@ impl KicadBoard { } pub fn layer_wire(&self, layer: BoardLayer) -> Wire { - Wire::from_edges(&self.layer_edges(layer)) + Wire::from_unordered_edges(&self.layer_edges(layer), EdgeConnection::default()) } pub fn layer_face(&self, layer: BoardLayer) -> Face { From 45c68a479c19b0bb47a028ad5fef61cdc7c9626b Mon Sep 17 00:00:00 2001 From: Brian Schwind Date: Mon, 25 Dec 2023 16:56:41 +0900 Subject: [PATCH 09/16] Make kicad_parser pure of any opencascade types, integrate into the viewer app --- crates/kicad-parser/Cargo.toml | 4 +- crates/kicad-parser/src/board.rs | 56 ++++++-------- crates/kicad-parser/src/graphics.rs | 89 ++++++----------------- crates/kicad-parser/src/main.rs | 23 ------ crates/opencascade/Cargo.toml | 1 + crates/opencascade/src/kicad.rs | 85 ++++++++++++++++++++++ crates/opencascade/src/lib.rs | 3 + crates/opencascade/src/primitives/wire.rs | 6 +- crates/viewer/src/main.rs | 13 +++- 9 files changed, 149 insertions(+), 131 deletions(-) delete mode 100644 crates/kicad-parser/src/main.rs create mode 100644 crates/opencascade/src/kicad.rs diff --git a/crates/kicad-parser/Cargo.toml b/crates/kicad-parser/Cargo.toml index 54d4c60a..3ce4d468 100644 --- a/crates/kicad-parser/Cargo.toml +++ b/crates/kicad-parser/Cargo.toml @@ -4,7 +4,5 @@ version = "0.1.0" edition = "2021" [dependencies] -anyhow = "1" +thiserror = "1" sexp = "1.1.4" -opencascade = { version = "0.2", path = "../opencascade" } -glam = { version = "0.23", features = ["bytemuck"] } diff --git a/crates/kicad-parser/src/board.rs b/crates/kicad-parser/src/board.rs index 93ed4367..cfc360f4 100644 --- a/crates/kicad-parser/src/board.rs +++ b/crates/kicad-parser/src/board.rs @@ -1,5 +1,4 @@ -use anyhow::{anyhow, Context, Result}; -use opencascade::primitives::{Edge, EdgeConnection, Face, Wire}; +use crate::Error; use sexp::{Atom, Sexp}; use std::path::Path; @@ -115,17 +114,16 @@ pub struct KicadBoard { } impl KicadBoard { - pub fn from_file>(file: P) -> Result { - let kicad_board_str = std::fs::read_to_string(&file) - .context(format!("Reading {:?}", file.as_ref().to_string_lossy()))?; + 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(anyhow!("Top level file wasn't a list")); + return Err(Error::TopLevelObjectNotList); }; let Sexp::Atom(Atom::S(head)) = &list[0] else { - return Err(anyhow!("First element in the top level list should be a string")); + return Err(Error::FirstElementInListNotString); }; match head.as_str() { @@ -133,11 +131,27 @@ impl KicadBoard { let board_fields = &list[1..]; Ok(Self::handle_board_fields(board_fields)?) }, - _ => Err(anyhow!("Invalid top-level file type - expected 'kicad_pcb'")), + _ => Err(Error::NotKicadPcbFile), } } - fn handle_board_fields(fields: &[Sexp]) -> Result { + 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 { @@ -180,28 +194,4 @@ impl KicadBoard { Ok(board) } - - pub fn layer_edges(&self, layer: BoardLayer) -> Vec { - self.graphic_lines - .iter() - .filter(|line| line.layer() == layer) - .map(Into::::into) - .chain( - self.graphic_arcs.iter().filter(|arc| arc.layer() == layer).map(Into::::into), - ) - .collect() - } - - pub fn layer_wire(&self, layer: BoardLayer) -> Wire { - Wire::from_unordered_edges(&self.layer_edges(layer), EdgeConnection::default()) - } - - pub fn layer_face(&self, layer: BoardLayer) -> Face { - Face::from_wire(&self.layer_wire(layer)) - } - - pub fn outline(&self, _offset: f64) -> Face { - // TODO apply offset around the face - self.layer_face(BoardLayer::EdgeCuts) - } } diff --git a/crates/kicad-parser/src/graphics.rs b/crates/kicad-parser/src/graphics.rs index 9f2efc10..d0691f85 100644 --- a/crates/kicad-parser/src/graphics.rs +++ b/crates/kicad-parser/src/graphics.rs @@ -1,9 +1,4 @@ -use anyhow::{anyhow, Result}; -use glam::{dvec3, DVec3}; -use opencascade::{ - primitives::{Edge, Face}, - workplane::Workplane, -}; +use crate::Error; use sexp::{Atom, Sexp}; use crate::board::BoardLayer; @@ -16,7 +11,7 @@ pub struct GraphicLine { } impl GraphicLine { - pub fn from_list(list: &[Sexp]) -> Result { + pub fn from_list(list: &[Sexp]) -> Result { let mut line = Self::default(); for field in list { @@ -51,12 +46,12 @@ impl GraphicLine { Ok(line) } - pub fn start_point(&self) -> DVec3 { - dvec3(self.start.0, self.start.1, 0.0) + pub fn start_point(&self) -> (f64, f64) { + self.start } - pub fn end_point(&self) -> DVec3 { - dvec3(self.end.0, self.end.1, 0.0) + pub fn end_point(&self) -> (f64, f64) { + self.end } pub fn layer(&self) -> BoardLayer { @@ -64,12 +59,6 @@ impl GraphicLine { } } -impl Into for &GraphicLine { - fn into(self) -> Edge { - Edge::segment(self.start_point(), self.end_point()) - } -} - #[derive(Debug, Clone, Default, PartialEq)] pub struct GraphicArc { start: (f64, f64), @@ -79,7 +68,7 @@ pub struct GraphicArc { } impl GraphicArc { - pub fn from_list(list: &[Sexp]) -> Result { + pub fn from_list(list: &[Sexp]) -> Result { let mut line = Self::default(); for field in list { @@ -118,16 +107,16 @@ impl GraphicArc { Ok(line) } - pub fn start_point(&self) -> DVec3 { - dvec3(self.start.0, self.start.1, 0.0) + pub fn start_point(&self) -> (f64, f64) { + self.start } - pub fn mid_point(&self) -> DVec3 { - dvec3(self.mid.0, self.mid.1, 0.0) + pub fn mid_point(&self) -> (f64, f64) { + self.mid } - pub fn end_point(&self) -> DVec3 { - dvec3(self.end.0, self.end.1, 0.0) + pub fn end_point(&self) -> (f64, f64) { + self.end } pub fn layer(&self) -> BoardLayer { @@ -135,12 +124,6 @@ impl GraphicArc { } } -impl Into for &GraphicArc { - fn into(self) -> Edge { - Edge::arc(self.start_point(), self.mid_point(), self.end_point()) - } -} - #[derive(Debug, Clone, Default, PartialEq)] pub struct GraphicCircle { center: (f64, f64), @@ -149,7 +132,7 @@ pub struct GraphicCircle { } impl GraphicCircle { - pub fn from_list(list: &[Sexp]) -> Result { + pub fn from_list(list: &[Sexp]) -> Result { let mut line = Self::default(); for field in list { @@ -184,12 +167,12 @@ impl GraphicCircle { Ok(line) } - pub fn center_point(&self) -> DVec3 { - dvec3(self.center.0, self.center.1, 0.0) + pub fn center_point(&self) -> (f64, f64) { + self.center } - pub fn end_point(&self) -> DVec3 { - dvec3(self.end.0, self.end.1, 0.0) + pub fn end_point(&self) -> (f64, f64) { + self.end } pub fn layer(&self) -> BoardLayer { @@ -197,18 +180,6 @@ impl GraphicCircle { } } -impl Into for &GraphicCircle { - fn into(self) -> Face { - let delta_x = (self.center.0 - self.end.0).abs(); - let delta_y = (self.center.1 - self.end.1).abs(); - let radius = (delta_x * delta_x + delta_y * delta_y).sqrt(); - Workplane::xy() - .translated(self.center_point()) - .circle(self.center.0, self.center.1, radius) - .to_face() - } -} - #[derive(Debug, Clone, Default, PartialEq)] pub struct GraphicRect { start: (f64, f64), @@ -217,7 +188,7 @@ pub struct GraphicRect { } impl GraphicRect { - pub fn from_list(list: &[Sexp]) -> Result { + pub fn from_list(list: &[Sexp]) -> Result { let mut line = Self::default(); for field in list { @@ -252,35 +223,19 @@ impl GraphicRect { Ok(line) } - pub fn start_point(&self) -> DVec3 { - dvec3(self.start.0, self.start.1, 0.0) - } - - pub fn end_point(&self) -> DVec3 { - dvec3(self.end.0, self.end.1, 0.0) - } - pub fn layer(&self) -> BoardLayer { BoardLayer::from(self.layer.as_str()) } } -impl Into for &GraphicRect { - fn into(self) -> Face { - let height = (self.end.1 - self.start.1).abs(); - let width = (self.end.0 - self.start.0).abs(); - Workplane::xy().translated(self.start_point()).rect(height, width).to_face() - } -} - -fn extract_coords(x: &Sexp, y: &Sexp) -> Result<(f64, f64)> { +fn extract_coords(x: &Sexp, y: &Sexp) -> Result<(f64, f64), Error> { Ok((extract_number(x)?, extract_number(y)?)) } -fn extract_number(num: &Sexp) -> Result { +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(anyhow!("Expected a number to be a float or integer")), + _ => Err(Error::NumberShouldBeFloatOrInt), } } diff --git a/crates/kicad-parser/src/main.rs b/crates/kicad-parser/src/main.rs deleted file mode 100644 index b8583eb8..00000000 --- a/crates/kicad-parser/src/main.rs +++ /dev/null @@ -1,23 +0,0 @@ -use anyhow::{anyhow, Result}; -use glam::DVec3; -use opencascade::primitives::Shape; - -mod board; -mod graphics; - -fn main() -> Result<()> { - let Some(input_file) = std::env::args().nth(1) else { - return Err(anyhow!("Usage: kicad-parser ")); - }; - - let board = board::KicadBoard::from_file(input_file)?; - let outline = board.outline(0.5); - // let outline: Face = Into::::into(&board.graphic_rects[0]); - - let solid = outline.extrude(DVec3 { x: 0.0, y: 0.0, z: 10.0 }); - let mut shape: Shape = solid.into(); - shape.clean(); - shape.write_stl("outline.stl")?; - - Ok(()) -} 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..7d50a87a --- /dev/null +++ b/crates/opencascade/src/kicad.rs @@ -0,0 +1,85 @@ +use crate::{ + primitives::{Edge, EdgeConnection, Wire}, + Error, +}; +use glam::dvec3; +use kicad_parser::{ + board::{BoardLayer, KicadBoard}, + graphics::{GraphicArc, GraphicLine}, +}; +use std::path::Path; + +impl From<&GraphicLine> for Edge { + fn from(line: &GraphicLine) -> Edge { + let start = line.start_point(); + let end = line.end_point(); + Edge::segment(dvec3(start.0, start.1, 0.0), dvec3(end.0, end.1, 0.0)) + } +} + +impl From<&GraphicArc> for Edge { + fn from(arc: &GraphicArc) -> Edge { + let start = arc.start_point(); + let mid = arc.mid_point(); + let end = arc.end_point(); + Edge::arc(dvec3(start.0, start.1, 0.0), dvec3(mid.0, mid.1, 0.0), dvec3(end.0, end.1, 0.0)) + } +} + +// impl From<&GraphicCircle> for Face { +// fn from(circle: &GraphicCircle) -> Face { +// let delta_x = (circle.center.0 - circle.end.0).abs(); +// let delta_y = (circle.center.1 - circle.end.1).abs(); +// let radius = (delta_x * delta_x + delta_y * delta_y).sqrt(); +// Workplane::xy() +// .translated(circle.center_point()) +// .circle(circle.center.0, circle.center.1, radius) +// .to_face() +// } +// } + +// impl From<&GraphicRect> for Face { +// fn from(rect: &GraphicRect) -> Face { +// let height = (rect.end.1 - rect.start.1).abs(); +// let width = (rect.end.0 - rect.start.0).abs(); +// Workplane::xy().translated(rect.start_point()).rect(height, width).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(), + ) + } + + fn layer_edges<'a>(&'a self, layer: &'a BoardLayer) -> impl Iterator + '_ { + self.board + .lines() + .filter(|line| line.layer() == *layer) + .map(Edge::from) + .chain(self.board.arcs().filter(|arc| arc.layer() == *layer).map(Edge::from)) + } + + // pub fn layer_wire(&self, layer: BoardLayer) -> Wire { + // Wire::from_unordered_edges(&self.layer_edges(layer), EdgeConnection::default()) + // } + + // pub fn layer_face(&self, layer: BoardLayer) -> Face { + // Face::from_wire(&self.layer_wire(layer)) + // } + + // pub fn outline(&self, _offset: f64) -> Face { + // // TODO apply offset around the face + // self.layer_face(BoardLayer::EdgeCuts) + // } +} 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/src/main.rs b/crates/viewer/src/main.rs index 61f6734f..bd3b5d7d 100644 --- a/crates/viewer/src/main.rs +++ b/crates/viewer/src/main.rs @@ -5,8 +5,8 @@ use crate::{ use anyhow::Error; use camera::OrbitCamera; use clap::{Parser, ValueEnum}; -use glam::{vec2, vec3, DVec3, Mat4, Quat, Vec2, Vec3}; -use opencascade::primitives::Shape; +use glam::{dvec3, vec2, vec3, DVec3, Mat4, Quat, Vec2, Vec3}; +use opencascade::{kicad::KicadPcb, primitives::Shape}; use simple_game::{ graphics::{ text::{AxisAlign, StyledText, TextAlignment, TextSystem}, @@ -81,6 +81,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 +135,12 @@ 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"); + + pcb.edge_cuts().to_face().extrude(dvec3(0.0, 0.0, 1.6)).into() } else if let Some(example) = args.example { example.shape() } else { From a8a56ac0bc0226f6c690fb17633ffc1aee4fbd32 Mon Sep 17 00:00:00 2001 From: Brian Schwind Date: Mon, 25 Dec 2023 17:00:12 +0900 Subject: [PATCH 10/16] Add missing kicad_parser lib.rs file --- crates/kicad-parser/src/lib.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 crates/kicad-parser/src/lib.rs diff --git a/crates/kicad-parser/src/lib.rs b/crates/kicad-parser/src/lib.rs new file mode 100644 index 00000000..0989b8d0 --- /dev/null +++ b/crates/kicad-parser/src/lib.rs @@ -0,0 +1,20 @@ +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("IO 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, +} From 0dbee574a1382324502bb5c03b4b5de30db7b920 Mon Sep 17 00:00:00 2001 From: Brian Schwind Date: Tue, 26 Dec 2023 13:21:04 +0900 Subject: [PATCH 11/16] Support edge cuts defined in part footprints --- crates/kicad-parser/src/board.rs | 258 ++++++++++++++++++---------- crates/kicad-parser/src/graphics.rs | 14 +- crates/kicad-parser/src/lib.rs | 13 ++ crates/opencascade/src/kicad.rs | 41 ++++- crates/viewer/Cargo.toml | 1 + crates/viewer/src/main.rs | 14 +- 6 files changed, 231 insertions(+), 110 deletions(-) diff --git a/crates/kicad-parser/src/board.rs b/crates/kicad-parser/src/board.rs index cfc360f4..14509dcc 100644 --- a/crates/kicad-parser/src/board.rs +++ b/crates/kicad-parser/src/board.rs @@ -1,9 +1,174 @@ -use crate::Error; +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, @@ -104,94 +269,3 @@ impl<'a> From<&'a BoardLayer> for &'a str { } } } - -#[derive(Debug, Clone, Default)] -pub struct KicadBoard { - graphic_lines: Vec, - graphic_arcs: Vec, - graphic_circles: Vec, - graphic_rects: 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 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" => {}, - "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) - } -} diff --git a/crates/kicad-parser/src/graphics.rs b/crates/kicad-parser/src/graphics.rs index d0691f85..8a7cd4e4 100644 --- a/crates/kicad-parser/src/graphics.rs +++ b/crates/kicad-parser/src/graphics.rs @@ -1,4 +1,4 @@ -use crate::Error; +use crate::{extract_coords, Error}; use sexp::{Atom, Sexp}; use crate::board::BoardLayer; @@ -227,15 +227,3 @@ impl GraphicRect { BoardLayer::from(self.layer.as_str()) } } - -fn extract_coords(x: &Sexp, y: &Sexp) -> Result<(f64, f64), Error> { - Ok((extract_number(x)?, extract_number(y)?)) -} - -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), - } -} diff --git a/crates/kicad-parser/src/lib.rs b/crates/kicad-parser/src/lib.rs index 0989b8d0..45ae6cc5 100644 --- a/crates/kicad-parser/src/lib.rs +++ b/crates/kicad-parser/src/lib.rs @@ -1,3 +1,4 @@ +use sexp::{Atom, Sexp}; use thiserror::Error; pub mod board; @@ -18,3 +19,15 @@ pub enum Error { #[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/src/kicad.rs b/crates/opencascade/src/kicad.rs index 7d50a87a..8b419438 100644 --- a/crates/opencascade/src/kicad.rs +++ b/crates/opencascade/src/kicad.rs @@ -1,8 +1,9 @@ use crate::{ + angle::ToAngle, primitives::{Edge, EdgeConnection, Wire}, Error, }; -use glam::dvec3; +use glam::{dvec3, DVec2}; use kicad_parser::{ board::{BoardLayer, KicadBoard}, graphics::{GraphicArc, GraphicLine}, @@ -62,12 +63,48 @@ impl KicadPcb { ) } - fn layer_edges<'a>(&'a self, layer: &'a BoardLayer) -> impl Iterator + '_ { + 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(); + 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) } // pub fn layer_wire(&self, layer: BoardLayer) -> Wire { 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 bd3b5d7d..f09565b7 100644 --- a/crates/viewer/src/main.rs +++ b/crates/viewer/src/main.rs @@ -5,8 +5,12 @@ use crate::{ use anyhow::Error; use camera::OrbitCamera; use clap::{Parser, ValueEnum}; -use glam::{dvec3, vec2, vec3, DVec3, Mat4, Quat, Vec2, Vec3}; -use opencascade::{kicad::KicadPcb, primitives::Shape}; +use glam::{vec2, vec3, DVec3, Mat4, Quat, Vec2, Vec3}; +use kicad_parser::board::BoardLayer; +use opencascade::{ + kicad::KicadPcb, + primitives::{IntoShape, Shape}, +}; use simple_game::{ graphics::{ text::{AxisAlign, StyledText, TextAlignment, TextSystem}, @@ -140,7 +144,11 @@ impl GameApp for ViewerApp { let pcb = KicadPcb::from_file(kicad_file).expect("Failed to read KiCAD PCB file, kicad_file"); - pcb.edge_cuts().to_face().extrude(dvec3(0.0, 0.0, 1.6)).into() + // 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 { From 5974ebd0ef56aa429fd3a86f55f56458f4e6a6fa Mon Sep 17 00:00:00 2001 From: Brian Schwind Date: Tue, 26 Dec 2023 22:53:38 +0900 Subject: [PATCH 12/16] Revert OCCT updates --- crates/occt-sys/OCCT | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/occt-sys/OCCT b/crates/occt-sys/OCCT index b079fb98..ffce0d66 160000 --- a/crates/occt-sys/OCCT +++ b/crates/occt-sys/OCCT @@ -1 +1 @@ -Subproject commit b079fb9877ef64d4a8158a60fa157f59b096debb +Subproject commit ffce0d66bbaafe3a95984d0e61804c201b9995d2 From 843d097ec954506d60556d0dfbefb702265b3170 Mon Sep 17 00:00:00 2001 From: Brian Schwind Date: Tue, 26 Dec 2023 22:53:55 +0900 Subject: [PATCH 13/16] Fix an error message --- crates/kicad-parser/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/kicad-parser/src/lib.rs b/crates/kicad-parser/src/lib.rs index 45ae6cc5..ea791b5f 100644 --- a/crates/kicad-parser/src/lib.rs +++ b/crates/kicad-parser/src/lib.rs @@ -8,7 +8,7 @@ pub mod graphics; pub enum Error { #[error("IO Error: {0}")] IoError(#[from] std::io::Error), - #[error("IO Error: {0}")] + #[error("S-Expression Parse Error: {0}")] SexpParseError(#[from] Box), #[error("Top level object is not a list")] TopLevelObjectNotList, From a3f47fe787aa97fa42a772c86a76980050488cba Mon Sep 17 00:00:00 2001 From: Brian Schwind Date: Tue, 26 Dec 2023 23:03:16 +0900 Subject: [PATCH 14/16] Uncomment the rest of the From impls --- crates/kicad-parser/src/graphics.rs | 8 ++++ crates/opencascade/src/kicad.rs | 58 ++++++++++++----------------- 2 files changed, 32 insertions(+), 34 deletions(-) diff --git a/crates/kicad-parser/src/graphics.rs b/crates/kicad-parser/src/graphics.rs index 8a7cd4e4..a03c088a 100644 --- a/crates/kicad-parser/src/graphics.rs +++ b/crates/kicad-parser/src/graphics.rs @@ -223,6 +223,14 @@ impl GraphicRect { 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/opencascade/src/kicad.rs b/crates/opencascade/src/kicad.rs index 8b419438..e4dffed6 100644 --- a/crates/opencascade/src/kicad.rs +++ b/crates/opencascade/src/kicad.rs @@ -1,12 +1,13 @@ use crate::{ angle::ToAngle, - primitives::{Edge, EdgeConnection, Wire}, + primitives::{Edge, EdgeConnection, Face, Wire}, + workplane::Workplane, Error, }; use glam::{dvec3, DVec2}; use kicad_parser::{ board::{BoardLayer, KicadBoard}, - graphics::{GraphicArc, GraphicLine}, + graphics::{GraphicArc, GraphicCircle, GraphicLine, GraphicRect}, }; use std::path::Path; @@ -27,25 +28,27 @@ impl From<&GraphicArc> for Edge { } } -// impl From<&GraphicCircle> for Face { -// fn from(circle: &GraphicCircle) -> Face { -// let delta_x = (circle.center.0 - circle.end.0).abs(); -// let delta_y = (circle.center.1 - circle.end.1).abs(); -// let radius = (delta_x * delta_x + delta_y * delta_y).sqrt(); -// Workplane::xy() -// .translated(circle.center_point()) -// .circle(circle.center.0, circle.center.1, radius) -// .to_face() -// } -// } - -// impl From<&GraphicRect> for Face { -// fn from(rect: &GraphicRect) -> Face { -// let height = (rect.end.1 - rect.start.1).abs(); -// let width = (rect.end.0 - rect.start.0).abs(); -// Workplane::xy().translated(rect.start_point()).rect(height, width).to_face() -// } -// } +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, @@ -106,17 +109,4 @@ impl KicadPcb { .chain(self.board.arcs().filter(|arc| arc.layer() == *layer).map(Edge::from)) .chain(footprint_edges) } - - // pub fn layer_wire(&self, layer: BoardLayer) -> Wire { - // Wire::from_unordered_edges(&self.layer_edges(layer), EdgeConnection::default()) - // } - - // pub fn layer_face(&self, layer: BoardLayer) -> Face { - // Face::from_wire(&self.layer_wire(layer)) - // } - - // pub fn outline(&self, _offset: f64) -> Face { - // // TODO apply offset around the face - // self.layer_face(BoardLayer::EdgeCuts) - // } } From 90e2b3ac91c8e9140b66e3253b20658dfcd9b449 Mon Sep 17 00:00:00 2001 From: Brian Schwind Date: Tue, 26 Dec 2023 23:19:42 +0900 Subject: [PATCH 15/16] Be more consistent with DVec construction --- crates/opencascade/src/kicad.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/opencascade/src/kicad.rs b/crates/opencascade/src/kicad.rs index e4dffed6..5e067f33 100644 --- a/crates/opencascade/src/kicad.rs +++ b/crates/opencascade/src/kicad.rs @@ -4,7 +4,7 @@ use crate::{ workplane::Workplane, Error, }; -use glam::{dvec3, DVec2}; +use glam::DVec2; use kicad_parser::{ board::{BoardLayer, KicadBoard}, graphics::{GraphicArc, GraphicCircle, GraphicLine, GraphicRect}, @@ -13,18 +13,18 @@ use std::path::Path; impl From<&GraphicLine> for Edge { fn from(line: &GraphicLine) -> Edge { - let start = line.start_point(); - let end = line.end_point(); - Edge::segment(dvec3(start.0, start.1, 0.0), dvec3(end.0, end.1, 0.0)) + 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 = arc.start_point(); - let mid = arc.mid_point(); - let end = arc.end_point(); - Edge::arc(dvec3(start.0, start.1, 0.0), dvec3(mid.0, mid.1, 0.0), dvec3(end.0, end.1, 0.0)) + 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)) } } From 71911795800d7a595beb60a7aecb28087ef1ca84 Mon Sep 17 00:00:00 2001 From: Brian Schwind Date: Tue, 26 Dec 2023 23:21:56 +0900 Subject: [PATCH 16/16] Add a small comment --- crates/opencascade/src/kicad.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/opencascade/src/kicad.rs b/crates/opencascade/src/kicad.rs index 5e067f33..eff7d095 100644 --- a/crates/opencascade/src/kicad.rs +++ b/crates/opencascade/src/kicad.rs @@ -69,6 +69,7 @@ impl KicadPcb { 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);