From f71c0327d17ba8c9ade1d67dff14bae726cb74b3 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Thu, 24 Aug 2023 01:56:20 -0400 Subject: [PATCH 1/3] Add macro implementations for arrays --- Cargo.lock | 5 +- altium-macros/src/lib.rs | 188 ++++++++++++++++++++++++++++++++++----- altium/Cargo.toml | 1 + altium/src/__private.rs | 28 ++---- altium/src/logging.rs | 12 ++- 5 files changed, 190 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9489a41..3dbf19d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,6 +27,7 @@ dependencies = [ "flate2", "image", "lazy_static", + "log", "num_enum", "quick-xml", "regex", @@ -262,9 +263,9 @@ checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] name = "log" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "memchr" diff --git a/altium-macros/src/lib.rs b/altium-macros/src/lib.rs index d6aa8d4..0dc80cc 100644 --- a/altium-macros/src/lib.rs +++ b/altium-macros/src/lib.rs @@ -4,8 +4,8 @@ use std::collections::BTreeMap; use convert_case::{Case, Casing}; use proc_macro::TokenStream; -use proc_macro2::{Ident, Literal, TokenStream as TokenStream2, TokenTree}; -use quote::quote; +use proc_macro2::{Ident, Literal, Span, TokenStream as TokenStream2, TokenTree}; +use quote::{quote, ToTokens}; use syn::{parse2, Attribute, Data, DeriveInput, Meta, Type}; /// Derive `FromRecord` for a type. See that trait for better information. @@ -39,61 +39,127 @@ fn inner(tokens: TokenStream2) -> syn::Result { panic!("only usable on structs"); }; - let mut attr_map = parse_attrs(parsed.attrs).expect("attribute with `id = n` required"); - let TokenTree::Literal(id) = attr_map.remove("id").expect("record ID required") else { + // Parse outer attributes + let mut struct_attr_map = parse_attrs(parsed.attrs).expect("attribute with `id = n` required"); + let TokenTree::Literal(id) = struct_attr_map.remove("id").expect("record ID required") else { panic!("record id should be a literal"); }; - let use_box = match attr_map.remove("use_box") { + let use_box = match struct_attr_map.remove("use_box") { Some(TokenTree::Ident(val)) if val == "true" => true, Some(TokenTree::Ident(val)) if val == "false" => true, Some(v) => panic!("Expected ident but got {v:?}"), None => false, }; - error_if_map_not_empty(&attr_map); + error_if_map_not_empty(&struct_attr_map); + + let mut match_stmts: Vec = Vec::new(); + let mut outer_flags: Vec = Vec::new(); - let mut match_stmts = Vec::new(); for field in data.fields { - // Convert to pascal case let Type::Path(path) = field.ty else { panic!("invalid type") }; let field_name = field.ident.unwrap(); - let mut field_map = parse_attrs(field.attrs).unwrap_or_default(); + let mut field_attr_map = parse_attrs(field.attrs).unwrap_or_default(); + + if let Some(arr_val) = field_attr_map.remove("array") { + let arr_val_str = arr_val.to_string(); + if arr_val_str == "true" { + let count_ident = field_attr_map + .remove("count") + .expect("missing 'count' attribute"); + + process_array(&name, &field_name, count_ident, &mut match_stmts); + error_if_map_not_empty(&field_attr_map); + continue; + } else if arr_val_str != "false" { + panic!("array must be `true` or `false` but got {arr_val_str}"); + } + } // We match a single literal, like `OwnerPartId` - let match_pat = match field_map.remove("rename") { + // Perform renaming if attribute requests it + let match_pat = match field_attr_map.remove("rename") { Some(TokenTree::Literal(v)) => v, Some(v) => panic!("expected literal, got {v:?}"), None => create_key_name(&field_name), }; - error_if_map_not_empty(&field_map); + + // If we haven't consumed all attributes, yell + error_if_map_not_empty(&field_attr_map); let match_lit = match_pat.to_string(); - let ret_stmt = if path.path.segments.first().unwrap().ident == "Option" { + let field_name_str = field_name.to_string(); + let update_stmt = if path.path.segments.first().unwrap().ident == "Option" { // Optional return quote! { ret.#field_name = Some(parsed); } } else { quote! { ret.#field_name = parsed; } }; + // Altium does this weird thing where it will create a `%UTF8%` key and + // a key without that. + let path_str = path.to_token_stream().to_string(); + let add_utf8_match = path_str.contains("String") || path_str.contains("str"); + + let (utf8_pat, utf8_def_flag, utf8_check_flag) = if add_utf8_match { + let match_pat_utf8 = create_key_name_utf8(&match_pat); + let match_lit_utf8 = match_pat.to_string(); + let flag_ident = Ident::new( + &format!("{field_name_str}_found_utf8_field"), + Span::call_site(), + ); + + let pat = quote! { + #match_pat_utf8 => { + let parsed = val.parse_as_utf8() + // Add context of what we were trying to parse for errors + .context(concat!( + "while matching `", #match_lit_utf8, "` (`", #field_name_str , + "`) for `", stringify!(#name), "` (via proc macro)" + ))?; + + #flag_ident = true; + #update_stmt + }, + }; + let def_flag = quote! { let mut #flag_ident: bool = false; }; + let check_flag = quote! { + if #flag_ident { + ::log::debug!("skipping {} after finding utf8", #field_name_str); + continue; + } + }; + + (pat, def_flag, check_flag) + } else { + ( + TokenStream2::new(), + TokenStream2::new(), + TokenStream2::new(), + ) + }; + let quoted = quote! { + #utf8_pat + #match_pat => { + #utf8_check_flag + let parsed = val.parse_as_utf8() - // Add context of what we were trying to parse + // Add context of what we were trying to parse for errors .context(concat!( - "while matching `", - #match_lit, - "` for `", - stringify!(#name), - "` (via proc macro)" + "while matching `", #match_lit, "` (`", #field_name_str ,"`) for `", + stringify!(#name), "` (via proc macro)" ))?; - #ret_stmt + #update_stmt }, }; + outer_flags.push(utf8_def_flag); match_stmts.push(quoted); } @@ -111,10 +177,14 @@ fn inner(tokens: TokenStream2) -> syn::Result { records: I, ) -> Result { let mut ret = Self::default(); + + // Boolean flags used to track what we have found throughout the loop + #(#outer_flags)* + for (key, val) in records { match key { #(#match_stmts)* - _ => crate::__private::macro_unsupported_key(stringify!(#name), key, val) + _ => crate::logging::macro_unsupported_key(stringify!(#name), key, val) } } @@ -188,6 +258,68 @@ fn error_if_map_not_empty(map: &BTreeMap) { assert!(map.is_empty(), "unexpected pairs {map:?}"); } +/// Setup handling of `X1 = 1234, Y1 = 909` +fn process_array( + name: &Ident, + field_name: &Ident, + count_ident_tt: TokenTree, + match_stmts: &mut Vec, +) { + let TokenTree::Literal(match_pat) = count_ident_tt else { + panic!("expected a literal for `count`"); + }; + + let field_name_str = field_name.to_string(); + let match_pat_str = match_pat.to_string(); + let count_match = quote! { + // Set the length of our array once given + #match_pat => { + let count = val.parse_as_utf8().context(concat!( + "while matching `", #match_pat_str, "` (`", #field_name_str ,"`) for `", + stringify!(#name), "` (via proc macro array)" + ))?; + + ret.#field_name = vec![crate::common::Location::default(); count]; + }, + + // Set an X value if given + xstr if crate::common::is_number_pattern(xstr, b'X') => { + let idx: usize = xstr.strip_prefix(b"X").unwrap() + .parse_as_utf8() + .or_context(|| format!( + "while extracting `{}` (`{}`) for `{}` (via proc macro array)", + String::from_utf8_lossy(xstr), #field_name_str, stringify!(#name) + ))?; + + let x = val.parse_as_utf8().or_context(|| format!( + "while extracting `{}` (`{}`) for `{}` (via proc macro array)", + String::from_utf8_lossy(xstr), #field_name_str, stringify!(#name) + ))?; + + ret.#field_name[idx - 1].x = x; + }, + + // Set a Y value if given + ystr if crate::common::is_number_pattern(ystr, b'Y') => { + let idx: usize = ystr.strip_prefix(b"Y").unwrap() + .parse_as_utf8() + .or_context(|| format!( + "while extracting `{}` (`{}`) for `{}` (via proc macro array)", + String::from_utf8_lossy(ystr), #field_name_str, stringify!(#name) + ))?; + + let y = val.parse_as_utf8().or_context(|| format!( + "while extracting `{}` (`{}`) for `{}` (via proc macro array)", + String::from_utf8_lossy(ystr), #field_name_str, stringify!(#name) + ))?; + + ret.#field_name[idx - 1].y = y; + }, + }; + + match_stmts.push(count_match); +} + /// From a field in our struct, create the name we should match by fn create_key_name(id: &Ident) -> Literal { // Replace these strings, just inconsistencies @@ -203,11 +335,16 @@ fn create_key_name(id: &Ident) -> Literal { ("Frac", "_Frac"), ]; + // Skip replacements if we have a full match + const REPLACE_SKIP: &[&str] = &["CornerXRadius", "CornerYRadius"]; + // First, convert to Pascal case let mut id_str = id.to_string().to_case(Case::Pascal); - for (from, to) in REPLACE { - id_str = id_str.replace(from, to); + if !REPLACE_SKIP.contains(&id_str.as_str()) { + for (from, to) in REPLACE { + id_str = id_str.replace(from, to); + } } Literal::byte_string(id_str.as_bytes()) @@ -223,3 +360,10 @@ fn create_key_name(id: &Ident) -> Literal { // None if field_name == "is_not_accessible" => Literal::byte_string(b"IsNotAccesible"), // None => Literal::byte_string(field_name.to_string().to_case(Case::Pascal).as_bytes()), } + +/// Convert `name` -> `%UTF8%name` for Altium's weird UTF8 pattern +fn create_key_name_utf8(lit: &Literal) -> Literal { + let s = lit.to_string(); + let inner = s.strip_prefix("b\"").unwrap().strip_suffix('"').unwrap(); + Literal::byte_string(format!("%UTF8%{inner}").as_bytes()) +} diff --git a/altium/Cargo.toml b/altium/Cargo.toml index ddaa248..0468e82 100644 --- a/altium/Cargo.toml +++ b/altium/Cargo.toml @@ -14,6 +14,7 @@ flate2 = "1.0.26" # image = "0.24.6" image = { version = "0.24.6", default-features = false, features = ["png", "bmp", "jpeg"] } lazy_static = "1.4.0" +log = "0.4.20" num_enum = "0.6.1" quick-xml = "0.30.0" regex = "1.9.1" diff --git a/altium/src/__private.rs b/altium/src/__private.rs index 6e5ce45..01783f8 100644 --- a/altium/src/__private.rs +++ b/altium/src/__private.rs @@ -1,19 +1,9 @@ -//! Things only used for testing - -use std::sync::atomic::Ordering; - -use crate::{common::buf2lstring, logging::UNSUPPORTED_KEYS}; - -pub fn num_unsupported_keys() -> u32 { - UNSUPPORTED_KEYS.load(Ordering::Relaxed) -} - -/// Called from our proc macro to log a key -pub fn macro_unsupported_key(name: &str, key: &[u8], val: &[u8]) { - UNSUPPORTED_KEYS.fetch_add(1, Ordering::Relaxed); - eprintln!( - "unsupported key for `{name}`: {}={} (via `FromRecord` derive)", - buf2lstring(key), - buf2lstring(val) - ); -} +//! Things only used for testing + +use std::sync::atomic::Ordering; + +use crate::{common::buf2lstring, logging::UNSUPPORTED_KEYS}; + +pub fn num_unsupported_keys() -> u32 { + UNSUPPORTED_KEYS.load(Ordering::Relaxed) +} diff --git a/altium/src/logging.rs b/altium/src/logging.rs index 30fc35d..a7a0f96 100644 --- a/altium/src/logging.rs +++ b/altium/src/logging.rs @@ -8,5 +8,15 @@ pub static UNSUPPORTED_KEYS: AtomicU32 = AtomicU32::new(0); /// Log the unsupported key pub fn log_unsupported_key(key: &[u8], val: &[u8]) { UNSUPPORTED_KEYS.fetch_add(1, Ordering::Relaxed); - eprintln!("unsupported key {}={}", buf2lstring(key), buf2lstring(val)); + log::warn!("unsupported key {}={}", buf2lstring(key), buf2lstring(val)); +} + +/// Called from our proc macro to log a key +pub fn macro_unsupported_key(name: &str, key: &[u8], val: &[u8]) { + UNSUPPORTED_KEYS.fetch_add(1, Ordering::Relaxed); + log::warn!( + "unsupported key for `{name}`: {}={} (via `FromRecord` derive)", + buf2lstring(key), + buf2lstring(val) + ); } From 70699de0876e9aea5a4c8253b479827d35e0645e Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Thu, 24 Aug 2023 01:59:07 -0400 Subject: [PATCH 2/3] Fix static warnings --- altium/src/sch/schlib.rs | 18 +++++++++--------- altium/src/sch/storage.rs | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/altium/src/sch/schlib.rs b/altium/src/sch/schlib.rs index 06ff610..4a9fc23 100644 --- a/altium/src/sch/schlib.rs +++ b/altium/src/sch/schlib.rs @@ -219,32 +219,32 @@ pub(crate) struct SchLibMeta { /// Parse implementation impl SchLibMeta { - const STREAMNAME: &str = "FileHeader"; + const STREAMNAME: &'static str = "FileHeader"; /// Magic header found in all streams - const HEADER: &[u8] = + const HEADER: &'static [u8] = b"HEADER=Protel for Windows - Schematic Library Editor Binary File Version 5.0"; - const HEADER_KEY: &[u8] = b"HEADER"; + const HEADER_KEY: &'static [u8] = b"HEADER"; // /// Every header starts with this // const PFX: &[u8] = &[0x7a, 0x04, 0x00, 0x00, b'|']; // Seems like each stream starts with 4 random bytes followed by a `|`? const PFX_LEN: usize = 5; - const SFX: &[u8] = &[0x00]; + const SFX: &'static [u8] = &[0x00]; /* font-related items */ /// `FontName1=Times New Roman` - const FONT_NAME_PFX: &[u8] = b"FontName"; + const FONT_NAME_PFX: &'static [u8] = b"FontName"; /// `Size1=9` - const FONT_SIZE_PFX: &[u8] = b"Size"; + const FONT_SIZE_PFX: &'static [u8] = b"Size"; /* part-related items */ /// `Libref0=Part Name` - const COMP_LIBREF_PFX: &[u8] = b"LibRef"; + const COMP_LIBREF_PFX: &'static [u8] = b"LibRef"; /// `CompDescr0=Long description of thing Name` - const COMP_DESC_PFX: &[u8] = b"CompDescr"; + const COMP_DESC_PFX: &'static [u8] = b"CompDescr"; /// `PartCount0=2` number of subcomponents (seems to default to 2?) - const COMP_PARTCOUNT_PFX: &[u8] = b"PartCount"; + const COMP_PARTCOUNT_PFX: &'static [u8] = b"PartCount"; /// Validate a `FileHeader` and extract its information /// diff --git a/altium/src/sch/storage.rs b/altium/src/sch/storage.rs index 8fc95e3..ffab15e 100644 --- a/altium/src/sch/storage.rs +++ b/altium/src/sch/storage.rs @@ -48,7 +48,7 @@ pub enum CompressedData { } impl Storage { - const STREAMNAME: &str = "Storage"; + const STREAMNAME: &'static str = "Storage"; /// Get the data from a key (path) name if available /// From 02d1a06a4ace02933eb91df623b41d301f7fe212 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Thu, 24 Aug 2023 02:03:20 -0400 Subject: [PATCH 3/3] Improve drawing traits, adjust parsing - Restructure `Canvas` and `Draw` - Enable parsing of Altium arrays - Restructure logging --- altium/src/common.rs | 58 +++++- altium/src/draw/canvas.rs | 58 ++++++ altium/src/draw/mod.rs | 115 +---------- altium/src/draw/svg.rs | 238 ++++++++++++++++++++++ altium/src/lib.rs | 3 +- altium/src/prj/parse.rs | 12 +- altium/src/prj/prjcfg.rs | 4 +- altium/src/sch/component.rs | 18 +- altium/src/sch/pin.rs | 22 +- altium/src/sch/record.rs | 91 ++++++++- altium/src/sch/record/draw.rs | 276 ++++++++++++++++---------- altium/src/sch/schlib.rs | 13 +- altium/src/sch/schlib/section_keys.rs | 29 ++- 13 files changed, 664 insertions(+), 273 deletions(-) create mode 100644 altium/src/draw/canvas.rs create mode 100644 altium/src/draw/svg.rs diff --git a/altium/src/common.rs b/altium/src/common.rs index 4ec9093..211043d 100644 --- a/altium/src/common.rs +++ b/altium/src/common.rs @@ -1,5 +1,7 @@ use std::{fmt, str}; +use uuid::Uuid; + use crate::error::{ErrorKind, TruncBuf}; use crate::parse::{FromUtf8, ParseUtf8}; @@ -15,10 +17,12 @@ pub struct Location { } impl Location { + #[must_use] pub fn new(x: i32, y: i32) -> Self { Self { x, y } } + #[must_use] pub fn add_x(self, x: i32) -> Self { Self { x: self.x + x, @@ -26,6 +30,7 @@ impl Location { } } + #[must_use] pub fn add_y(self, y: i32) -> Self { Self { x: self.x, @@ -45,36 +50,53 @@ pub enum Visibility { /// // TODO: figure out what file types use this exact format #[derive(Clone, Copy, PartialEq)] -pub struct UniqueId([u8; 8]); +pub enum UniqueId { + /// Altium's old style UUID + Simple([u8; 8]), + Uuid(Uuid), +} impl UniqueId { - pub(crate) fn from_slice>(buf: S) -> Option { - buf.as_ref().try_into().ok().map(Self) + fn from_slice>(buf: S) -> Option { + buf.as_ref() + .try_into() + .ok() + .map(Self::Simple) + .or_else(|| Uuid::try_parse_ascii(buf.as_ref()).ok().map(Self::Uuid)) } +} - /// Get this `UniqueId` as a string - pub fn as_str(&self) -> &str { - str::from_utf8(&self.0).expect("unique IDs should always be ASCII") +impl fmt::Display for UniqueId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + UniqueId::Simple(v) => str::from_utf8(v) + .expect("unique IDs should always be ASCII") + .fmt(f), + UniqueId::Uuid(v) => v.as_hyphenated().fmt(f), + } } } impl Default for UniqueId { fn default() -> Self { - Self(*b"00000000") + Self::Simple(*b"00000000") } } impl fmt::Debug for UniqueId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("UniqueId").field(&self.as_str()).finish() + f.debug_tuple("UniqueId").field(&self.to_string()).finish() } } impl FromUtf8<'_> for UniqueId { fn from_utf8(buf: &[u8]) -> Result { - Ok(Self(buf.as_ref().try_into().map_err(|_| { - ErrorKind::InvalidUniqueId(TruncBuf::new(buf)) - })?)) + buf.as_ref() + .try_into() + .ok() + .map(Self::Simple) + .or_else(|| Uuid::try_parse_ascii(buf).ok().map(Self::Uuid)) + .ok_or(ErrorKind::InvalidUniqueId(TruncBuf::new(buf))) } } @@ -225,3 +247,17 @@ pub enum PosVert { Middle, Bottom, } + +/// Verify a number pattern matches, e.g. `X100` +pub fn is_number_pattern(s: &[u8], prefix: u8) -> bool { + if let Some(stripped) = s + .strip_prefix(&[prefix]) + .map(|s| s.strip_prefix(&[b'-']).unwrap_or(s)) + { + if stripped.iter().all(u8::is_ascii_digit) { + return true; + } + } + + false +} diff --git a/altium/src/draw/canvas.rs b/altium/src/draw/canvas.rs new file mode 100644 index 0000000..173d2ec --- /dev/null +++ b/altium/src/draw/canvas.rs @@ -0,0 +1,58 @@ +use crate::{ + common::{Color, Location, PosHoriz, PosVert, Rotation}, + font::Font, + sch::Justification, +}; + +/// Generic trait for something that can be drawn. Beware, unstable! +pub trait Canvas { + fn draw_text(&mut self, item: DrawText); + fn draw_line(&mut self, item: DrawLine); + fn draw_polygon(&mut self, item: DrawPolygon); + fn draw_rectangle(&mut self, item: DrawRectangle); + fn draw_image(&mut self, item: DrawImage); + fn add_comment>(&mut self, comment: S) {} +} + +/// Helper struct to write some text +#[derive(Clone, Debug, Default)] +pub struct DrawText<'a> { + pub x: i32, + pub y: i32, + pub text: &'a str, + pub font: &'a Font, + pub anchor_h: PosHoriz, + pub anchor_v: PosVert, + pub color: Color, + pub rotation: Rotation, +} + +#[derive(Clone, Debug, Default)] +pub struct DrawLine { + pub start: Location, + pub end: Location, + pub color: Color, + pub width: u16, + // pub width: Option<&'a str>, +} + +#[derive(Clone, Debug, Default)] +pub struct DrawRectangle { + pub x: i32, + pub y: i32, + pub width: i32, + pub height: i32, + pub fill_color: Color, + pub stroke_color: Color, + pub stroke_width: u16, +} + +#[derive(Clone, Debug, Default)] +pub struct DrawPolygon<'a> { + pub locations: &'a [Location], + pub fill_color: Color, + pub stroke_color: Color, + pub stroke_width: u16, +} + +pub struct DrawImage {} diff --git a/altium/src/draw/mod.rs b/altium/src/draw/mod.rs index f1bb878..c8e0510 100644 --- a/altium/src/draw/mod.rs +++ b/altium/src/draw/mod.rs @@ -1,107 +1,17 @@ -mod text; +pub(crate) mod canvas; +mod svg; use core::{ cmp::{max, min}, mem, }; -use svg::node::element::SVG as Svg; -pub use text::{DrawLine, DrawText}; +pub use canvas::{Canvas, DrawImage, DrawLine, DrawPolygon, DrawRectangle, DrawText}; +pub use self::svg::SvgCtx; +pub use crate::common::{Color, Location, PosHoriz, PosVert}; use crate::font::Font; -pub struct SvgCtx { - svg: Svg, - /// `(min, max)` values of x - x_range: Option<(i32, i32)>, - /// `(min, max)` values of y - y_range: Option<(i32, i32)>, - /// True if the image header has already been set - has_embedded_images: bool, -} - -impl SvgCtx { - pub fn new() -> Self { - Self { - svg: Svg::new(), - x_range: None, - y_range: None, - has_embedded_images: false, - } - } - - /// Add a node to this svg - pub fn add_node(&mut self, node: T) - where - T: Into>, - { - // Bad API means we need to do memory tricks... - let mut working = Svg::new(); - mem::swap(&mut self.svg, &mut working); - working = working.add(node); - mem::swap(&mut self.svg, &mut working); - } - - /// Translate from (0, 0) in bottom left to (0, 0) in top left. Makes sure - /// `x` and `x + width` are within the view box. - pub fn x_coord(&mut self, x: i32, width: i32) -> i32 { - let (mut min_x, mut max_x) = self.x_range.unwrap_or((x, x)); - let edge = x + width; // Add width (allows for negative values) - min_x = min(min(min_x, x), edge); - max_x = max(max(max_x, x), edge); - - self.x_range = Some((min_x, max_x)); - x - } - - /// Translate from (0, 0) in bottom left to (0, 0) in top left - /// - /// Updates the y location ranges if needed - pub fn y_coord(&mut self, y: i32, height: i32) -> i32 { - let new_y = -y - height; - let (mut min_y, mut max_y) = self.y_range.unwrap_or((new_y, new_y)); - let edge = new_y + height; // Add height (allows for negative values) - min_y = min(min(min_y, new_y), edge); - max_y = max(max(max_y, new_y), edge); - - self.y_range = Some((min_y, max_y)); - new_y - } - - /// Get the svg - pub fn svg(self) -> Svg { - let mut svg = self.svg; - let (min_x, max_x) = self.x_range.unwrap_or((0, 0)); - let (min_y, max_y) = self.y_range.unwrap_or((0, 0)); - - // Add a 5% border on all sides - let side_extra = (max_x - min_x) / 20; - let vert_extra = (max_y - min_y) / 20; - - svg = svg.set( - "viewBox", - format!( - "{} {} {} {}", - min_x - side_extra, - min_y - vert_extra, - (max_x - min_x) + side_extra * 2, - (max_y - min_y) + vert_extra * 2, - ), - ); - - if self.has_embedded_images { - svg = svg.set("xmlns:xlink", "http://www.w3.org/1999/xlink"); - } - - svg - } - - /// Set xlink header for embedded images - pub fn enable_inline_images(&mut self) { - self.has_embedded_images = true; - } -} - pub trait Draw { type Context<'a>; @@ -110,18 +20,5 @@ pub trait Draw { /// This has a defualt implementation that does nothing for easier /// reusability #[allow(unused)] - fn draw_svg(&self, svg: &mut SvgCtx, ctx: &Self::Context<'_>) {} -} - -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn test_coord_offsets() { - let mut svg = SvgCtx::new(); - assert_eq!(10, svg.x_coord(10, 20)); - assert_eq!(-30, svg.y_coord(10, 20)); - assert_eq!(svg.x_range, Some((10, 30))); - assert_eq!(svg.y_range, Some((-30, -10))); - } + fn draw(&self, canvas: &mut C, ctx: &Self::Context<'_>) {} } diff --git a/altium/src/draw/svg.rs b/altium/src/draw/svg.rs new file mode 100644 index 0000000..0f66d19 --- /dev/null +++ b/altium/src/draw/svg.rs @@ -0,0 +1,238 @@ +use std::cmp::{max, min}; +use std::mem; + +use svg::node::element::SVG as Svg; +use svg::node::{element as el, Text}; +use svg::Node; + +use super::{canvas, Canvas}; +use crate::common::Location; +use crate::{ + common::{Color, PosHoriz, PosVert, Rotation}, + font::Font, + sch::Justification, +}; + +#[derive(Clone, Debug)] +pub struct SvgCtx { + svg: Svg, + /// `(min, max)` values of x + x_range: Option<(i32, i32)>, + /// `(min, max)` values of y + y_range: Option<(i32, i32)>, + /// True if the image header has already been set + has_embedded_images: bool, +} + +impl SvgCtx { + pub fn new() -> Self { + Self { + svg: Svg::new(), + x_range: None, + y_range: None, + has_embedded_images: false, + } + } + + /// Add a node to this svg + pub fn add_node(&mut self, node: T) + where + T: Into>, + { + // Bad API means we need to do memory tricks... + let mut working = Svg::new(); + mem::swap(&mut self.svg, &mut working); + working = working.add(node); + mem::swap(&mut self.svg, &mut working); + } + + /// Translate from (0, 0) in bottom left to (0, 0) in top left. Makes sure + /// `x` and `x + width` are within the view box. + pub fn x_coord(&mut self, x: i32, width: i32) -> i32 { + let (mut min_x, mut max_x) = self.x_range.unwrap_or((x, x)); + let edge = x + width; // Add width (allows for negative values) + min_x = min(min(min_x, x), edge); + max_x = max(max(max_x, x), edge); + + self.x_range = Some((min_x, max_x)); + x + } + + /// Translate from (0, 0) in bottom left to (0, 0) in top left + /// + /// Updates the y location ranges if needed + pub fn y_coord(&mut self, y: i32, height: i32) -> i32 { + let new_y = -y - height; + let (mut min_y, mut max_y) = self.y_range.unwrap_or((new_y, new_y)); + let edge = new_y + height; // Add height (allows for negative values) + min_y = min(min(min_y, new_y), edge); + max_y = max(max(max_y, new_y), edge); + + self.y_range = Some((min_y, max_y)); + new_y + } + + /// Get the svg + pub fn svg(self) -> Svg { + let mut svg = self.svg; + let (min_x, max_x) = self.x_range.unwrap_or((0, 0)); + let (min_y, max_y) = self.y_range.unwrap_or((0, 0)); + + // Add a 5% border on all sides + let side_extra = (max_x - min_x) / 20; + let vert_extra = (max_y - min_y) / 20; + + svg = svg.set( + "viewBox", + format!( + "{} {} {} {}", + min_x - side_extra, + min_y - vert_extra, + (max_x - min_x) + side_extra * 2, + (max_y - min_y) + vert_extra * 2, + ), + ); + + if self.has_embedded_images { + svg = svg.set("xmlns:xlink", "http://www.w3.org/1999/xlink"); + } + + svg + } + + /// Set xlink header for embedded images + pub fn enable_inline_images(&mut self) { + self.has_embedded_images = true; + } +} + +impl Canvas for SvgCtx { + #[allow(clippy::similar_names)] + fn draw_text(&mut self, item: canvas::DrawText) { + use Justification as J; + use PosHoriz::{Center, Left, Right}; + use PosVert::{Bottom, Middle, Top}; + use Rotation::{R0, R180, R270, R90}; + + let cmt = svg::node::Comment::new(format!("{item:#?}")); + self.add_node(cmt); + + let (x, y) = (item.x, item.y); + let (width, height) = text_dims(item.text, item.font.size); + let halfwidth = width / 2; + let halfheight = height / 2; + + let anchor = match item.anchor_h { + PosHoriz::Left => "start", + PosHoriz::Center => "middle", + PosHoriz::Right => "end", + }; + + /// Offset of max x from min x + let (xoffn, xoffp) = match item.anchor_h { + Left => (0, width), + Center => (halfwidth, halfwidth), + Right => (width, 0), + }; + + let (yoffn, yoffp) = match item.anchor_v { + Top => (height, 0), + Middle => (halfheight, halfheight), + Bottom => (0, height), + }; + + let (xmin, xmax, ymin, ymax) = match item.rotation { + R0 => (x - xoffn, x + xoffp, y - yoffn, y + yoffp), + R90 => (x - yoffp, x + yoffn, y - xoffn, y + xoffp), + R180 => (x - xoffp, x + xoffn, y - yoffp, y + yoffn), + R270 => (x - yoffn, x + yoffp, y - xoffp, y + xoffn), + }; + + self.x_coord(xmin, xmax - xmin); + self.x_coord(ymin, ymax - ymin); + + let txtnode = Text::new(item.text); + let mut node = el::Text::new() + .set("x", self.x_coord(x, width)) + .set("y", self.y_coord(y, height)) + .set("text-anchor", anchor) + .set("font-size", format!("{}px", item.font.size * 7 / 10)) + .set("font-family", format!("{}, sans-serif", item.font.name)) + .set("transform", format!("rotate({})", item.rotation.as_int())); + node.append(txtnode); + self.add_node(node); + + // Add a circle to the text anchor + let node2 = el::Circle::new() + .set("cx", self.x_coord(x, width)) + .set("cy", self.y_coord(y, height)) + .set("r", 0.5) + .set("fill", "red"); + self.add_node(node2); + } + + fn draw_line(&mut self, item: canvas::DrawLine) { + let dx = item.start.x - item.end.x; + let dy = item.start.y - item.end.y; + + let mut node = el::Line::new() + .set("x1", self.x_coord(item.start.x, 0)) + .set("x2", self.x_coord(item.end.x, 0)) + .set("y1", self.y_coord(item.start.y, dy)) + .set("y2", self.y_coord(item.end.y, dy)) + .set("stroke", item.color.to_hex()); + + // if let Some(w) = item.width { + // node = node.set("stroke-width", w); + // } + node = node.set("stroke-width", format!("{}px", item.width)); + + self.add_node(node); + } + + fn draw_rectangle(&mut self, item: canvas::DrawRectangle) { + let node = el::Rectangle::new() + .set("width", item.width) + .set("height", item.height) + // Need top left corner to set location + .set("x", self.x_coord(item.x, item.width)) + .set("y", self.y_coord(item.y, item.height)) + .set("fill", item.fill_color.to_hex()) + .set("stroke", item.stroke_color.to_hex()) + .set("stroke-width", item.stroke_width); + self.add_node(node); + } + + fn draw_polygon(&mut self, _item: canvas::DrawPolygon) { + // todo!() + } + + fn draw_image(&mut self, item: canvas::DrawImage) {} + + fn add_comment>(&mut self, comment: S) { + let cmt = svg::node::Comment::new(comment); + self.add_node(cmt); + } +} + +/// Estimate the size of text +fn text_dims(text: &str, font_size: u16) -> (i32, i32) { + let fsize_i32: i32 = font_size.into(); + let width = fsize_i32 * i32::try_from(text.len()).unwrap(); + let height = fsize_i32; + + (width, height) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_coord_offsets() { + let mut svg = SvgCtx::new(); + assert_eq!(10, svg.x_coord(10, 20)); + assert_eq!(-30, svg.y_coord(10, 20)); + assert_eq!(svg.x_range, Some((10, 30))); + assert_eq!(svg.y_range, Some((-30, -10))); + } +} diff --git a/altium/src/lib.rs b/altium/src/lib.rs index b8c5931..e084c3f 100644 --- a/altium/src/lib.rs +++ b/altium/src/lib.rs @@ -11,14 +11,15 @@ #![allow(clippy::missing_errors_doc)] #![allow(clippy::struct_excessive_bools)] #![allow(clippy::missing_panics_doc)] +#![allow(clippy::new_without_default)] mod common; -mod draw; mod logging; mod parse; #[doc(hidden)] pub mod __private; +pub mod draw; pub mod dwf; pub mod error; pub mod font; diff --git a/altium/src/prj/parse.rs b/altium/src/prj/parse.rs index 30a990e..af8065a 100644 --- a/altium/src/prj/parse.rs +++ b/altium/src/prj/parse.rs @@ -2,7 +2,12 @@ use std::borrow::ToOwned; use ini::Properties; -use crate::{common::UniqueId, error::ErrorKind}; +use crate::{ + common::UniqueId, + error::{AddContext, ErrorKind}, + parse::FromUtf8, + Error, +}; /// Parse a string or default to an empty string pub fn parse_string(sec: &Properties, key: &str) -> String { @@ -22,8 +27,9 @@ pub fn parse_bool(sec: &Properties, key: &str) -> bool { } /// Extract a `UniqueId` from a buffer -pub fn parse_unique_id(sec: &Properties, key: &str) -> Result { +pub fn parse_unique_id(sec: &Properties, key: &str) -> Result { sec.get(key) .ok_or_else(|| ErrorKind::MissingSection(key.to_owned())) - .map(|v| UniqueId::from_slice(v).ok_or(ErrorKind::MissingUniqueId(v.to_owned())))? + .and_then(|v| UniqueId::from_utf8(v.as_bytes())) + .map_err(|e| e.context("parse_unique_id")) } diff --git a/altium/src/prj/prjcfg.rs b/altium/src/prj/prjcfg.rs index 98af78d..863f951 100644 --- a/altium/src/prj/prjcfg.rs +++ b/altium/src/prj/prjcfg.rs @@ -177,7 +177,7 @@ impl Document { } /// Create a vector of `Document`s from an ini file - fn from_prj_ini(ini: &Ini) -> Result, ErrorKind> { + fn from_prj_ini(ini: &Ini) -> Result, Error> { let mut doc_sections: Vec<&str> = ini .sections() .filter_map(|nameopt| { @@ -207,7 +207,7 @@ impl Document { } /// Create a single `Document` from an ini section - fn from_section(sec: &Properties) -> Result { + fn from_section(sec: &Properties) -> Result { Ok(Self { document_path: parse_string(sec, "DocumentPath"), annotation_en: parse_bool(sec, "AnnotationEnabled"), diff --git a/altium/src/sch/component.rs b/altium/src/sch/component.rs index d71f590..7a25199 100644 --- a/altium/src/sch/component.rs +++ b/altium/src/sch/component.rs @@ -1,6 +1,7 @@ //! Things related to the entire component use core::fmt::Write; +use std::cmp::Ordering; use std::fs::File; use std::io; use std::path::Path; @@ -10,7 +11,7 @@ use svg::node::element::SVG as Svg; use super::storage::Storage; use super::{SchDrawCtx, SchRecord}; -use crate::draw::{Draw, SvgCtx}; +use crate::draw::{Canvas, Draw, SvgCtx}; use crate::error::AddContext; use crate::font::FontCollection; use crate::sch::pin::SchPin; @@ -53,7 +54,7 @@ impl Component { }; for record in &self.records { - record.draw_svg(&mut draw, &ctx); + record.draw(&mut draw, &ctx); } draw.svg() } @@ -64,6 +65,14 @@ impl Component { svg::write(&file, &self.svg()) } + pub fn draw(&self, canvas: &mut C) { + let ctx = SchDrawCtx { + fonts: &self.fonts, + storage: &self.storage, + }; + self.records.iter().for_each(|r| r.draw(canvas, &ctx)); + } + /// The name of this part pub fn name(&self) -> &str { &self.name @@ -84,6 +93,11 @@ impl Component { .expect("no metadata record"); meta.description.as_deref().unwrap_or("") } + + /// Compare + pub fn name_cmp(&self, other: &Self) -> Option { + self.name.partial_cmp(&other.name) + } } /// Given a buffer for a component, split the records up diff --git a/altium/src/sch/pin.rs b/altium/src/sch/pin.rs index 92e3780..6da3ab5 100644 --- a/altium/src/sch/pin.rs +++ b/altium/src/sch/pin.rs @@ -3,6 +3,8 @@ use core::fmt; use std::str::{self, Utf8Error}; +use log::warn; + use super::SchRecord; use crate::common::{Location, Rotation, Visibility}; @@ -55,10 +57,9 @@ impl SchPin { let (name, rest) = sized_buf_to_utf8(rest, "name")?; let (designator, rest) = sized_buf_to_utf8(rest, "designator")?; - assert!( - matches!(rest, [_, 0x03, b'|', b'&', b'|']), - "unexpected rest: {rest:02x?}" - ); + if !matches!(rest, [_, 0x03, b'|', b'&', b'|']) { + warn!("unexpected rest: {rest:02x?}"); + } let retval = Self { owner_index: 0, @@ -91,16 +92,15 @@ impl SchPin { /// Altium stores the position of the pin at its non-connecting end. Which /// seems dumb. This provides the connecting end. pub(crate) fn location_conn(&self) -> Location { - let (x_orig, y_orig, len) = ( - self.location_x, - self.location_y, - i32::try_from(self.length).unwrap(), - ); + let x_orig = self.location_x; + let y_orig = self.location_y; + let len = i32::try_from(self.length).unwrap(); + let (x, y) = match self.rotation { Rotation::R0 => (x_orig + len, y_orig), - Rotation::R90 => (x_orig, y_orig - len), + Rotation::R90 => (x_orig, y_orig + len), Rotation::R180 => (x_orig - len, y_orig), - Rotation::R270 => (x_orig, y_orig + len), + Rotation::R270 => (x_orig, y_orig - len), }; Location { x, y } } diff --git a/altium/src/sch/record.rs b/altium/src/sch/record.rs index 3fc236d..66ba438 100644 --- a/altium/src/sch/record.rs +++ b/altium/src/sch/record.rs @@ -60,7 +60,7 @@ use num_enum::TryFromPrimitive; use super::params::Justification; use super::pin::SchPin; -use crate::common::{ReadOnlyState, UniqueId}; +use crate::common::{Location, ReadOnlyState, UniqueId}; use crate::error::AddContext; use crate::Error; use crate::{ @@ -181,6 +181,7 @@ pub struct MetaData { area_color: Color, color: Color, current_part_id: u8, + database_table_name: Box, #[from_record(rename = b"ComponentDescription")] pub(crate) description: Option>, /// Alternative display modes @@ -193,7 +194,9 @@ pub struct MetaData { /// Number of parts part_count: u8, part_id_locked: bool, + not_use_db_table_name: bool, sheet_part_file_name: Box, + design_item_id: Box, source_library_name: Box, target_file_name: Box, unique_id: UniqueId, @@ -216,6 +219,7 @@ pub struct Label { font_id: u16, index_in_sheet: i16, is_not_accessible: bool, + is_mirrored: bool, location_x: i32, location_y: i32, owner_index: u8, @@ -228,8 +232,16 @@ pub struct Label { #[derive(Clone, Debug, Default, PartialEq, FromRecord)] #[from_record(id = 5)] pub struct Bezier { + color: Color, + index_in_sheet: i16, + is_not_accessible: bool, + line_width: u16, + #[from_record(array = true, count = b"LocationCount")] + locations: Vec, owner_index: u8, owner_part_id: i8, + owner_part_display_mode: i8, + unique_id: UniqueId, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] @@ -238,26 +250,48 @@ pub struct PolyLine { owner_index: u8, owner_part_id: i8, is_not_accessible: bool, - line_width: i8, + index_in_sheet: i16, + line_width: u16, color: Color, - location_count: u16, - // TODO: how to handle X1 Y1 X2 Y2 headers + #[from_record(array = true, count = b"LocationCount")] + locations: Vec, unique_id: UniqueId, - index_in_sheet: i16, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] #[from_record(id = 7)] pub struct Polygon { + area_color: Color, + color: Color, + index_in_sheet: i16, + is_not_accessible: bool, + is_solid: bool, + line_width: u16, + #[from_record(array = true, count = b"LocationCount")] + locations: Vec, owner_index: u8, owner_part_id: i8, + owner_part_display_mode: i8, + unique_id: UniqueId, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] #[from_record(id = 8)] pub struct Ellipse { + area_color: Color, + color: Color, + index_in_sheet: i16, + is_not_accessible: bool, + is_solid: bool, + line_width: u16, + location_x: i32, + location_y: i32, owner_index: u8, owner_part_id: i8, + owner_part_display_mode: i8, + radius: i32, + secondary_radius: i32, + unique_id: UniqueId, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] @@ -270,9 +304,23 @@ pub struct Piechart { #[derive(Clone, Debug, Default, PartialEq, FromRecord)] #[from_record(id = 10)] pub struct RectangleRounded { + area_color: Color, + color: Color, + corner_x: i32, + corner_y: i32, + corner_x_radius: i32, + corner_y_radius: i32, + index_in_sheet: i16, + is_not_accessible: bool, + is_solid: bool, + line_width: u16, + location_x: i32, + location_y: i32, owner_index: u8, owner_part_id: i8, owner_part_display_mode: i8, + transparent: bool, + unique_id: UniqueId, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] @@ -283,11 +331,13 @@ pub struct ElipticalArc { is_not_accessible: bool, index_in_sheet: i16, location_x: i32, + location_x_frac: i32, location_y: i32, + location_y_frac: i32, radius: i8, - radius_frac: i16, + radius_frac: i32, secondary_radius: i8, - secondary_radius_frac: i16, + secondary_radius_frac: i32, line_width: i8, start_angle: f32, end_angle: f32, @@ -300,13 +350,38 @@ pub struct ElipticalArc { pub struct Arc { owner_index: u8, owner_part_id: i8, + is_not_accessible: bool, + index_in_sheet: i16, + location_x: i32, + location_y: i32, + radius: i8, + radius_frac: i32, + secondary_radius: i8, + secondary_radius_frac: i32, + line_width: i8, + start_angle: f32, + end_angle: f32, + color: Color, + unique_id: UniqueId, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] #[from_record(id = 13)] pub struct Line { + color: Color, + corner_x: i32, + corner_y: i32, + index_in_sheet: i16, + is_not_accessible: bool, + is_solid: bool, + line_width: u16, + location_count: u16, + location_x: i32, + location_y: i32, owner_index: u8, owner_part_id: i8, + owner_part_display_mode: i8, + unique_id: UniqueId, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] @@ -327,6 +402,7 @@ pub struct Rectangle { owner_index: u8, owner_part_id: i8, owner_part_display_mode: i8, + transparent: bool, unique_id: UniqueId, } @@ -482,6 +558,7 @@ pub struct Parameter { color: Color, font_id: u16, unique_id: UniqueId, + read_only_state: i8, name: Box, is_hidden: bool, text: Box, diff --git a/altium/src/sch/record/draw.rs b/altium/src/sch/record/draw.rs index c4aaf7e..aadb3e9 100644 --- a/altium/src/sch/record/draw.rs +++ b/altium/src/sch/record/draw.rs @@ -4,7 +4,9 @@ use svg::node::{element as el, Text}; use svg::Node; use crate::common::{Color, Location, PosHoriz, PosVert, Rotation, Visibility}; -use crate::draw::{Draw, DrawLine, DrawText, SvgCtx}; +use crate::draw::canvas::DrawRectangle; +use crate::draw::canvas::{Canvas, DrawLine, DrawText}; +use crate::draw::{Draw, DrawPolygon}; use crate::font::{Font, FontCollection}; use crate::sch::params::Justification; use crate::sch::pin::SchPin; @@ -26,39 +28,39 @@ pub struct SchDrawCtx<'a> { impl Draw for record::SchRecord { type Context<'a> = SchDrawCtx<'a>; - fn draw_svg(&self, svg: &mut SvgCtx, ctx: &SchDrawCtx<'_>) { + fn draw(&self, canvas: &mut C, ctx: &SchDrawCtx<'_>) { match self { - // record::SchRecord::MetaData(v) => v.draw_svg(svg, ctx), - record::SchRecord::Pin(v) => v.draw_svg(svg, ctx), - // record::SchRecord::IeeeSymbol(v) => v.draw_svg(svg, ctx), - record::SchRecord::Label(v) => v.draw_svg(svg, ctx), - // record::SchRecord::Bezier(v) => v.draw_svg(svg, ctx), - // record::SchRecord::PolyLine(v) => v.draw_svg(svg, ctx), - // record::SchRecord::Polygon(v) => v.draw_svg(svg, ctx), - // record::SchRecord::Ellipse(v) => v.draw_svg(svg, ctx), - // record::SchRecord::Piechart(v) => v.draw_svg(svg, ctx), - // record::SchRecord::RectangleRounded(v) => v.draw_svg(svg, ctx), - // record::SchRecord::ElipticalArc(v) => v.draw_svg(svg, ctx), - // record::SchRecord::Arc(v) => v.draw_svg(svg, ctx), - // record::SchRecord::Line(v) => v.draw_svg(svg, ctx), - record::SchRecord::Rectangle(v) => v.draw_svg(svg, ctx), - // record::SchRecord::SheetSymbol(v) => v.draw_svg(svg, ctx), - // record::SchRecord::SheetEntry(v) => v.draw_svg(svg, ctx), - // record::SchRecord::PowerPort(v) => v.draw_svg(svg, ctx), - // record::SchRecord::Port(v) => v.draw_svg(svg, ctx), - // record::SchRecord::NoErc(v) => v.draw_svg(svg, ctx), - // record::SchRecord::NetLabel(v) => v.draw_svg(svg, ctx), - // record::SchRecord::Bus(v) => v.draw_svg(svg, ctx), - // record::SchRecord::Wire(v) => v.draw_svg(svg, ctx), - // record::SchRecord::TextFrame(v) => v.draw_svg(svg, ctx), - // record::SchRecord::Junction(v) => v.draw_svg(svg, ctx), - record::SchRecord::Image(v) => v.draw_svg(svg, ctx), - // record::SchRecord::Sheet(v) => v.draw_svg(svg, ctx), - // record::SchRecord::SheetName(v) => v.draw_svg(svg, ctx), - // record::SchRecord::FileName(v) => v.draw_svg(svg, ctx), - // record::SchRecord::BusEntry(v) => v.draw_svg(svg, ctx), - // record::SchRecord::Template(v) => v.draw_svg(svg, ctx), - record::SchRecord::Parameter(v) => v.draw_svg(svg, ctx), + // record::SchRecord::MetaData(v) => v.draw(canvas, ctx), + record::SchRecord::Pin(v) => v.draw(canvas, ctx), + // record::SchRecord::IeeeSymbol(v) => v.draw(canvas, ctx), + record::SchRecord::Label(v) => v.draw(canvas, ctx), + // record::SchRecord::Bezier(v) => v.draw(canvas, ctx), + record::SchRecord::PolyLine(v) => v.draw(canvas, ctx), + record::SchRecord::Polygon(v) => v.draw(canvas, ctx), + // record::SchRecord::Ellipse(v) => v.draw(canvas, ctx), + // record::SchRecord::Piechart(v) => v.draw(canvas, ctx), + record::SchRecord::RectangleRounded(v) => v.draw(canvas, ctx), + // record::SchRecord::ElipticalArc(v) => v.draw(canvas, ctx), + // record::SchRecord::Arc(v) => v.draw(canvas, ctx), + record::SchRecord::Line(v) => v.draw(canvas, ctx), + record::SchRecord::Rectangle(v) => v.draw(canvas, ctx), + // record::SchRecord::SheetSymbol(v) => v.draw(canvas, ctx), + // record::SchRecord::SheetEntry(v) => v.draw(canvas, ctx), + // record::SchRecord::PowerPort(v) => v.draw(canvas, ctx), + // record::SchRecord::Port(v) => v.draw(canvas, ctx), + // record::SchRecord::NoErc(v) => v.draw(canvas, ctx), + // record::SchRecord::NetLabel(v) => v.draw(canvas, ctx), + // record::SchRecord::Bus(v) => v.draw(canvas, ctx), + // record::SchRecord::Wire(v) => v.draw(canvas, ctx), + // record::SchRecord::TextFrame(v) => v.draw(canvas, ctx), + // record::SchRecord::Junction(v) => v.draw(canvas, ctx), + record::SchRecord::Image(v) => v.draw(canvas, ctx), + // record::SchRecord::Sheet(v) => v.draw(canvas, ctx), + // record::SchRecord::SheetName(v) => v.draw(canvas, ctx), + // record::SchRecord::FileName(v) => v.draw(canvas, ctx), + // record::SchRecord::BusEntry(v) => v.draw(canvas, ctx), + // record::SchRecord::Template(v) => v.draw(canvas, ctx), + record::SchRecord::Parameter(v) => v.draw(canvas, ctx), // record::SchRecord::ImplementationList(v) => v.draw_svg(svg, ctx), // non-printing types // record::SchRecord::Designator(_) | record::SchRecord::Undefined => (), @@ -71,13 +73,12 @@ impl Draw for record::SchRecord { impl Draw for SchPin { type Context<'a> = SchDrawCtx<'a>; - fn draw_svg(&self, svg: &mut SvgCtx, ctx: &SchDrawCtx<'_>) { + fn draw(&self, canvas: &mut C, ctx: &SchDrawCtx<'_>) { use PosHoriz::{Center, Left, Right}; use PosVert::{Bottom, Middle, Top}; use Rotation::{R0, R180, R270, R90}; - let cmt = svg::node::Comment::new(format!("{self:#?}")); - svg.add_node(cmt); + canvas.add_comment(format!("{self:#?}")); let len: i32 = self.length.try_into().unwrap(); let (x1, y1) = (self.location_x, self.location_y); @@ -85,31 +86,29 @@ impl Draw for SchPin { let start = self.location(); let end = self.location_conn(); - DrawLine { + canvas.draw_line(DrawLine { start, end, color: Color::black(), - ..Default::default() - } - .draw(svg); + width: 4, + // ..Default::default() + }); // Altium draws a small white plus at the pin's connect position, so we // do too - DrawLine { + canvas.draw_line(DrawLine { start: end.add_x(1), end: end.add_x(-1), - color: Color::blue(), - width: Some("0.5px"), - } - .draw(svg); + color: Color::white(), + width: 1, + }); - DrawLine { + canvas.draw_line(DrawLine { start: end.add_y(1), end: end.add_y(-1), - color: Color::red(), - width: Some("0.5px"), - } - .draw(svg); + color: Color::white(), + width: 1, + }); // FIXME: use actual spacing & fonts from pin spec let (name_x, name_y, des_x, des_y, txt_rotation) = match self.rotation { @@ -126,7 +125,7 @@ impl Draw for SchPin { }; // Display name to the right of the pin - DrawText { + canvas.draw_text(DrawText { x: name_x, y: name_y, text: &self.name, @@ -134,8 +133,7 @@ impl Draw for SchPin { anchor_v, rotation: txt_rotation, ..Default::default() - } - .draw(svg); + }); } if self.designator_vis == Visibility::Visible { @@ -144,7 +142,7 @@ impl Draw for SchPin { R180 | R270 => (Right, Bottom), }; - DrawText { + canvas.draw_text(DrawText { x: des_x, y: des_y, text: &self.designator, @@ -152,8 +150,7 @@ impl Draw for SchPin { anchor_v, rotation: txt_rotation, ..Default::default() - } - .draw(svg); + }); } } } @@ -164,10 +161,10 @@ impl Draw for SchPin { impl Draw for record::Label { type Context<'a> = SchDrawCtx<'a>; - fn draw_svg(&self, svg: &mut SvgCtx, ctx: &SchDrawCtx<'_>) { + fn draw(&self, canvas: &mut C, ctx: &SchDrawCtx<'_>) { let font = &ctx.fonts.get_idx(self.font_id.into()); let (anchor_h, anchor_v) = self.justification.into(); - DrawText { + canvas.draw_text(DrawText { x: self.location_x, y: self.location_y, text: &self.text, @@ -176,38 +173,97 @@ impl Draw for record::Label { anchor_v, color: self.color, ..Default::default() // rotation: todo!(), - } - .draw(svg); + }); } } // impl Draw for record::Bezier {} -// impl Draw for record::PolyLine {} -// impl Draw for record::Polygon {} + +impl Draw for record::PolyLine { + type Context<'a> = SchDrawCtx<'a>; + + fn draw(&self, canvas: &mut C, ctx: &Self::Context<'_>) { + for window in self.locations.windows(2) { + let &[a, b] = window else { unreachable!() }; + + canvas.draw_line(DrawLine { + start: a, + end: b, + color: self.color, + width: self.line_width * 4, + }); + } + } +} + +impl Draw for record::Polygon { + type Context<'a> = SchDrawCtx<'a>; + + fn draw(&self, canvas: &mut C, ctx: &SchDrawCtx<'_>) { + canvas.draw_polygon(DrawPolygon { + locations: &self.locations, + fill_color: self.area_color, + stroke_color: self.color, + stroke_width: self.line_width, + }); + } +} + // impl Draw for record::Ellipse {} // impl Draw for record::Piechart {} -// impl Draw for record::RectangleRounded {} + +impl Draw for record::RectangleRounded { + type Context<'a> = SchDrawCtx<'a>; + + fn draw(&self, canvas: &mut C, ctx: &SchDrawCtx<'_>) { + let width = self.corner_x - self.location_x; + let height = self.corner_y - self.location_y; + + // FIXME: rounded rectangle + canvas.draw_rectangle(DrawRectangle { + x: self.location_x, + y: self.location_y, + width, + height, + fill_color: self.area_color, + stroke_color: self.color, + stroke_width: self.line_width, + }); + } +} + // impl Draw for record::ElipticalArc {} // impl Draw for record::Arc {} -// impl Draw for record::Line {} + +impl Draw for record::Line { + type Context<'a> = SchDrawCtx<'a>; + + fn draw(&self, canvas: &mut C, ctx: &Self::Context<'_>) { + canvas.draw_line(DrawLine { + start: Location::new(self.location_x, self.location_y), + end: Location::new(self.corner_x, self.corner_y), + color: self.color, + width: self.line_width, + }); + } +} impl Draw for record::Rectangle { type Context<'a> = SchDrawCtx<'a>; - fn draw_svg(&self, svg: &mut SvgCtx, ctx: &SchDrawCtx<'_>) { + fn draw(&self, canvas: &mut C, ctx: &SchDrawCtx<'_>) { let width = self.corner_x - self.location_x; let height = self.corner_y - self.location_y; - let node = el::Rectangle::new() - .set("width", width) - .set("height", height) - // Need top left corner to set location - .set("x", svg.x_coord(self.location_x, width)) - .set("y", svg.y_coord(self.location_y, height)) - .set("fill", self.area_color.to_hex()) - .set("stroke", self.color.to_hex()) - .set("stroke-width", self.line_width); - svg.add_node(node); + canvas.draw_rectangle(DrawRectangle { + x: self.location_x, + y: self.location_y, + width, + height, + fill_color: self.area_color, + stroke_color: self.color, + stroke_width: self.line_width, + }); } } @@ -224,39 +280,40 @@ impl Draw for record::Rectangle { impl Draw for record::Image { type Context<'a> = SchDrawCtx<'a>; - fn draw_svg(&self, svg: &mut SvgCtx, ctx: &Self::Context<'_>) { + fn draw(&self, canvas: &mut C, ctx: &SchDrawCtx<'_>) { + // TODO // TODO: just set to the URL. Maybe set whether or not to encode // somehow? - if !self.embed_image { - return; - } - - let Some(data) = ctx.storage.get_data(&self.file_name) else { - eprintln!("unable to find image at {}", self.file_name); - return; - }; - - let width = self.corner_x - self.location_x; - let height = self.corner_y - self.location_y; - - let mut b64_str = "data:image/png;base64,".to_owned(); - STANDARD_NO_PAD.encode_string(data, &mut b64_str); - assert!( - b64_str.len() < MAX_EMBED_SIZE, - "max size {MAX_EMBED_SIZE} bytes but got {}", - b64_str.len() - ); - - let node = el::Image::new() - .set("width", width) - .set("height", height) - // Need top left corner to set location - .set("x", svg.x_coord(self.location_x, width)) - .set("y", svg.y_coord(self.location_y, height)) - .set("xlink:href", b64_str); - svg.add_node(node); - - svg.enable_inline_images(); + // if !self.embed_image { + // return; + // } + + // let Some(data) = ctx.storage.get_data(&self.file_name) else { + // eprintln!("unable to find image at {}", self.file_name); + // return; + // }; + + // let width = self.corner_x - self.location_x; + // let height = self.corner_y - self.location_y; + + // let mut b64_str = "data:image/png;base64,".to_owned(); + // STANDARD_NO_PAD.encode_string(data, &mut b64_str); + // assert!( + // b64_str.len() < MAX_EMBED_SIZE, + // "max size {MAX_EMBED_SIZE} bytes but got {}", + // b64_str.len() + // ); + + // let node = el::Image::new() + // .set("width", width) + // .set("height", height) + // // Need top left corner to set location + // .set("x", svg.x_coord(self.location_x, width)) + // .set("y", svg.y_coord(self.location_y, height)) + // .set("xlink:href", b64_str); + // svg.add_node(node); + + // svg.enable_inline_images(); } } // impl Draw for record::Sheet {} @@ -268,16 +325,15 @@ impl Draw for record::Image { impl Draw for record::Parameter { type Context<'a> = SchDrawCtx<'a>; - fn draw_svg(&self, svg: &mut SvgCtx, ctx: &SchDrawCtx<'_>) { + fn draw(&self, canvas: &mut C, ctx: &SchDrawCtx<'_>) { let font = &ctx.fonts.get_idx(self.font_id.into()); - DrawText { + canvas.draw_text(DrawText { x: self.location_x, y: self.location_y, text: &self.text, font, ..Default::default() - } - .draw(svg); + }); } } diff --git a/altium/src/sch/schlib.rs b/altium/src/sch/schlib.rs index 4a9fc23..5cc201f 100644 --- a/altium/src/sch/schlib.rs +++ b/altium/src/sch/schlib.rs @@ -94,10 +94,10 @@ impl SchLib { // Scope of refcell borrow let mut cfile_ref = self.cfile.borrow_mut(); let mut stream = cfile_ref.open_stream(&data_path).unwrap_or_else(|e| { - panic!( - "missing required stream `{}` with error {e}", - data_path.display() - ) + dbg!(&meta); + dbg!(&data_path); + let path_disp = data_path.display(); + panic!("missing required stream `{path_disp}` with error {e}") }); stream.read_to_end(&mut buf).unwrap(); } @@ -256,8 +256,6 @@ impl SchLibMeta { let mut stream = cfile.open_stream(Self::STREAMNAME)?; stream.read_to_end(tmp_buf)?; - println!("parsing cfile:\n{}", String::from_utf8_lossy(tmp_buf)); - let to_parse = tmp_buf .get(Self::PFX_LEN..) .ok_or(ErrorKind::new_invalid_stream(Self::STREAMNAME, 0))? @@ -351,13 +349,10 @@ impl SchLibMeta { } ret.fonts = Arc::new(fonts.into()); - println!("done parsing cfile"); Ok(ret) } } -// pub struct Components {} - /// Information available in the header about a single component: includes /// libref and part count #[derive(Clone, Debug, Default)] diff --git a/altium/src/sch/schlib/section_keys.rs b/altium/src/sch/schlib/section_keys.rs index 1c94d1d..a9aade7 100644 --- a/altium/src/sch/schlib/section_keys.rs +++ b/altium/src/sch/schlib/section_keys.rs @@ -10,32 +10,37 @@ use crate::logging::log_unsupported_key; use crate::parse::ParseUtf8; use crate::Error; -const STREAMNAME: &str = "SectionKeys"; +const SEC_KEY_STREAM: &str = "SectionKeys"; const PFX_LEN: usize = 5; const SFX: &[u8] = &[0x00]; const LIBREF: &[u8] = b"LibRef"; const SECKEY: &[u8] = b"SectionKey"; /// Update a header with section keys. +/// +/// The `SectionKeys` stream stores a map of `libref -> section key` for some +/// librefs that are too long to be a section key (stream name) themselves. Go +/// through our extracted components and make sure that `sec_key` is replaced by +/// this map where needed. pub(crate) fn update_section_keys( cfile: &mut CompoundFile, tmp_buf: &mut Vec, header: &mut SchLibMeta, ) -> Result<(), Error> { - if !cfile.exists(STREAMNAME) { + if !cfile.exists(SEC_KEY_STREAM) { return Ok(()); } - let mut stream = cfile.open_stream(STREAMNAME)?; + let mut stream = cfile.open_stream(SEC_KEY_STREAM)?; stream.read_to_end(tmp_buf)?; let to_parse = tmp_buf .get(PFX_LEN..) - .ok_or(ErrorKind::new_invalid_stream(STREAMNAME, 0))? + .ok_or(ErrorKind::new_invalid_stream(SEC_KEY_STREAM, 0))? .strip_suffix(SFX) - .ok_or(ErrorKind::new_invalid_stream(STREAMNAME, tmp_buf.len()))?; + .ok_or(ErrorKind::new_invalid_stream(SEC_KEY_STREAM, tmp_buf.len()))?; - // libref -> section key + // keep a map of libref -> section key let mut map: Vec<(&str, &str)> = Vec::new(); for (key, val) in split_altium_map(to_parse) { @@ -54,12 +59,20 @@ pub(crate) fn update_section_keys( } for comp in &mut header.components { - // Find any keys that exist in our map + // Find any keys that exist in our map and replace them let Ok(idx) = map.binary_search_by_key(&comp.libref(), |x| x.0) else { + // If they aren't in our map, fixup only + comp.sec_key = fixup_sec_key(&comp.sec_key); continue; }; - comp.sec_key = map[idx].1.into(); + + comp.sec_key = fixup_sec_key(map[idx].1); } Ok(()) } + +/// Altium does some transformations for its stream paths, e.g. `/` -> `_` +fn fixup_sec_key(path: &str) -> Box { + path.replace('/', "_").into() +}