diff --git a/.gitignore b/.gitignore index d1dd692..a5be0e8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ History/ # demo output altium/*.svg .cargo +**.xcodeproj diff --git a/Cargo.lock b/Cargo.lock index 7da03b4..22309fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -140,6 +140,7 @@ dependencies = [ "image 0.25.1", "lazy_static", "log", + "num-traits", "num_enum", "quick-xml", "regex", @@ -147,6 +148,7 @@ dependencies = [ "serde", "serde-xml-rs", "svg", + "uom", "uuid", "xml-rs", ] @@ -3328,6 +3330,16 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +[[package]] +name = "uom" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd36e5350a65d112584053ee91843955826bf9e56ec0d1351214e01f6d7cd9c" +dependencies = [ + "num-traits", + "typenum", +] + [[package]] name = "url" version = "2.5.0" diff --git a/altium-macros/src/lib.rs b/altium-macros/src/lib.rs index ca2528f..076093a 100644 --- a/altium-macros/src/lib.rs +++ b/altium-macros/src/lib.rs @@ -6,7 +6,7 @@ use convert_case::{Case, Casing}; use proc_macro::TokenStream; use proc_macro2::{Delimiter, Ident, Literal, Span, TokenStream as TokenStream2, TokenTree}; use quote::{quote, ToTokens}; -use syn::{parse2, Attribute, Data, DeriveInput, Meta, Type}; +use syn::{parse2, Attribute, Data, DeriveInput, Field, Meta, Type}; /// Derive `FromRecord` for a type. See that trait for better information. /// @@ -69,100 +69,7 @@ fn inner(tokens: TokenStream2) -> syn::Result { // Loop through each field in the struct for field in data.fields { - let Type::Path(path) = field.ty else { - panic!("invalid type") - }; - - let field_ident = field.ident.unwrap(); - - // Parse attributes that exist on the field - let mut field_attr_map = parse_attrs(field.attrs).unwrap_or_default(); - - // Check if we need to parse an array - 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"); - - let arr_map = field_attr_map - .remove("map") - .expect("missing 'map' attribute"); - - process_array( - &struct_ident, - &field_ident, - count_ident, - arr_map, - &mut match_arms, - ); - 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` - // 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_ident), - }; - - // If we haven't consumed all attributes, yell - error_if_map_not_empty(&field_attr_map); - - let update_stmt = if path.path.segments.first().unwrap().ident == "Option" { - // Wrap our field is an `Option` - quote! { ret.#field_ident = Some(parsed); } - } else { - quote! { ret.#field_ident = parsed; } - }; - - let path_str = path.to_token_stream().to_string(); - - // Types `Location` and `LocationFract` are special cases - let is_location_fract = path_str.contains("LocationFract"); - if is_location_fract || path_str.contains("Location") { - process_location( - &struct_ident, - &field_ident, - is_location_fract, - &mut match_arms, - ); - continue; - } - - let Utf8Handler { - arm: utf8_arm, - define_flag: utf8_def_flag, - check_flag: utf8_check_flag, - } = if path_str.contains("String") || path_str.contains("str") { - // Altium does this weird thing where it will create a normal key and a key - // with `%UTF8%` if a value is utf8. We need to discard those redundant values - make_utf8_handler(&match_pat, &field_ident, &struct_ident, &update_stmt) - } else { - Utf8Handler::default() - }; - - let ctx_msg = make_ctx_message(&match_pat, &field_ident, &struct_ident); - - let quoted = quote! { - #utf8_arm - - #match_pat => { - #utf8_check_flag - - let parsed = val.parse_as_utf8().context(#ctx_msg)?; - #update_stmt - }, - }; - - outer_flags.push(utf8_def_flag); - match_arms.push(quoted); + handle_field(field, &struct_ident, &mut match_arms, &mut outer_flags); } let ret_val = if use_box { @@ -198,6 +105,104 @@ fn inner(tokens: TokenStream2) -> syn::Result { Ok(ret) } +fn handle_field( + field: Field, + struct_ident: &Ident, + match_arms: &mut Vec, + outer_flags: &mut Vec, +) { + let Type::Path(path) = field.ty else { + panic!("invalid type") + }; + + let field_ident = field.ident.unwrap(); + + // Parse attributes that exist on the field + let mut field_attr_map = parse_attrs(field.attrs).unwrap_or_default(); + + // Check if we need to parse an array + 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"); + + let arr_map = field_attr_map + .remove("map") + .expect("missing 'map' attribute"); + + process_array(struct_ident, &field_ident, count_ident, arr_map, match_arms); + error_if_map_not_empty(&field_attr_map); + return; + } 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` + // 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_ident), + }; + + let convert = match field_attr_map.remove("convert") { + Some(conv_fn) => quote! { .map_err(Into::into).and_then(#conv_fn) }, + None => TokenStream2::new(), + }; + + // If we haven't consumed all attributes, yell + error_if_map_not_empty(&field_attr_map); + + let update_stmt = if path.path.segments.first().unwrap().ident == "Option" { + // Wrap our field is an `Option` + quote! { ret.#field_ident = Some(parsed); } + } else { + quote! { ret.#field_ident = parsed; } + }; + + let path_str = path.to_token_stream().to_string(); + + // Types `Location` and `LocationFract` are special cases + let is_location_fract = path_str.contains("LocationFract"); + if is_location_fract || path_str.contains("Location") { + process_location(struct_ident, &field_ident, is_location_fract, match_arms); + return; + } + + let Utf8Handler { + arm: utf8_arm, + define_flag: utf8_def_flag, + check_flag: utf8_check_flag, + } = if path_str.contains("String") || path_str.contains("str") { + // Altium does this weird thing where it will create a normal key and a key + // with `%UTF8%` if a value is utf8. We need to discard those redundant values + make_utf8_handler(&match_pat, &field_ident, struct_ident, &update_stmt) + } else { + Utf8Handler::default() + }; + + let ctx_msg = make_ctx_message(&match_pat, &field_ident, struct_ident); + + let quoted = quote! { + #utf8_arm + + #match_pat => { + #utf8_check_flag + + let parsed = val.parse_as_utf8() + #convert + .context(#ctx_msg)?; + #update_stmt + }, + }; + + outer_flags.push(utf8_def_flag); + match_arms.push(quoted); +} + /// Next type of token we are expecting #[derive(Clone, Debug, PartialEq)] enum AttrParseState { @@ -364,7 +369,7 @@ fn process_location( #match_pat => #assign_field = val.parse_as_utf8() .map_err(Into::into) - .and_then(crate::common::i32_mils_to_nm) + .and_then(crate::common::mils_to_nm) .context(#ctx_msg)?, } } else { diff --git a/altium/Cargo.toml b/altium/Cargo.toml index bfc83f9..0625580 100644 --- a/altium/Cargo.toml +++ b/altium/Cargo.toml @@ -17,6 +17,7 @@ flate2 = "1.0.30" image = { version = "0.25.1", default-features = false, features = ["png", "bmp", "jpeg"] } lazy_static = "1.4.0" log = "0.4.21" +num-traits = "0.2.18" num_enum = "0.7.2" quick-xml = "0.31.0" regex = "1.10.4" @@ -24,6 +25,7 @@ rust-ini = "0.21.0" serde = "1.0.200" serde-xml-rs = "0.6.0" svg = "0.17.0" +uom = "0.36.0" uuid = { version = "1.8.0", features = ["v1", "v4", "fast-rng"]} xml-rs = "0.8.20" diff --git a/altium/src/common.rs b/altium/src/common.rs index ffc34e5..6fa9a64 100644 --- a/altium/src/common.rs +++ b/altium/src/common.rs @@ -1,5 +1,6 @@ use std::{fmt, str}; +use num_traits::CheckedMul; use uuid::Uuid; use crate::error::{AddContext, ErrorKind, Result, TruncBuf}; @@ -9,7 +10,7 @@ use crate::parse::{FromUtf8, ParseUtf8}; const SEP: u8 = b'|'; const KV_SEP: u8 = b'='; -/// Common coordinate type +/// Common coordinate type with x and y positions in nnaometers. #[derive(Clone, Copy, Debug, Default, PartialEq)] pub struct Location { // These are nonpublic because we might want to combine `Location` and `LocationFract` @@ -180,27 +181,36 @@ impl Rgb { format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b) } - pub fn from_hex(r: u8, g: u8, b: u8) -> Self { + pub const fn from_hex(r: u8, g: u8, b: u8) -> Self { Self { r, g, b } } - pub fn black() -> Self { + pub fn as_float_rgba(self) -> [f32; 4] { + [ + f32::from(self.r) / 255.0, + f32::from(self.g) / 255.0, + f32::from(self.b) / 255.0, + 1.0, + ] + } + + pub const fn black() -> Self { Self::from_hex(0x00, 0x00, 0x00) } - pub fn white() -> Self { + pub const fn white() -> Self { Self::from_hex(0xff, 0xff, 0xff) } - pub fn red() -> Self { + pub const fn red() -> Self { Self::from_hex(0xff, 0x00, 0x00) } - pub fn green() -> Self { + pub const fn green() -> Self { Self::from_hex(0x00, 0xff, 0x00) } - pub fn blue() -> Self { + pub const fn blue() -> Self { Self::from_hex(0x00, 0x00, 0xff) } } @@ -307,16 +317,14 @@ pub fn is_number_pattern(s: &[u8], prefix: &[u8]) -> bool { } /// Infallible conversion -pub fn i32_mils_to_nm(mils: i32) -> Result { - const FACTOR: i32 = 25400; - mils.checked_mul(FACTOR).ok_or_else(|| { - ErrorKind::Overflow(mils.into(), FACTOR.into(), '*').context("converting units") - }) -} - -pub fn u32_mils_to_nm(mils: u32) -> Result { - const FACTOR: u32 = 25400; - mils.checked_mul(FACTOR).ok_or_else(|| { +pub fn mils_to_nm(mils: T) -> Result +where + T: CheckedMul, + T: From, + i64: From, +{ + const FACTOR: u16 = 25400; + mils.checked_mul(&FACTOR.into()).ok_or_else(|| { ErrorKind::Overflow(mils.into(), FACTOR.into(), '*').context("converting units") }) } diff --git a/altium/src/draw/canvas.rs b/altium/src/draw/canvas.rs index 01f6916..cc5cb4f 100644 --- a/altium/src/draw/canvas.rs +++ b/altium/src/draw/canvas.rs @@ -3,7 +3,7 @@ use crate::{ font::Font, }; -/// Generic trait for something that can be drawn. Beware, unstable! +/// Generic trait for something that can be drawn to. Beware, unstable! pub trait Canvas: crate::sealed::Sealed { fn draw_text(&mut self, item: DrawText); fn draw_line(&mut self, item: DrawLine); @@ -13,6 +13,36 @@ pub trait Canvas: crate::sealed::Sealed { fn add_comment>(&mut self, _comment: S) {} } +/// Line ending. +/// +/// See for more. +#[derive(Clone, Debug, Default)] +pub enum LineCap { + /// Stop at the endpoint + #[default] + Butt, + /// Square past the endpoint + Square, + /// Rounded cap centered at the endpoint + Round, +} + +/// How two lines should be combined +/// +/// See for more. +#[derive(Clone, Debug, Default)] +pub enum LineJoin { + /// Sharp corners + #[default] + Miter, + /// Miter but possibly don't come to a fine point + MiterClip, + /// Round over the join point + Round, + /// Square off the join point + Bevel, +} + /// Helper struct to write some text #[derive(Clone, Debug, Default)] pub struct DrawText<'a> { @@ -31,8 +61,10 @@ pub struct DrawLine { pub start: Location, pub end: Location, pub color: Rgb, - pub width: u16, - // pub width: Option<&'a str>, + pub width: u32, + pub start_cap: LineCap, + pub end_cap: LineCap, + pub line_join: LineJoin, } #[derive(Clone, Debug, Default)] @@ -43,7 +75,7 @@ pub struct DrawRectangle { pub height: i32, pub fill_color: Rgb, pub stroke_color: Rgb, - pub stroke_width: u16, + pub stroke_width: u32, } #[derive(Clone, Debug, Default)] @@ -51,7 +83,7 @@ pub struct DrawPolygon<'a> { pub locations: &'a [Location], pub fill_color: Rgb, pub stroke_color: Rgb, - pub stroke_width: u16, + pub stroke_width: u32, } pub struct DrawImage {} diff --git a/altium/src/draw/mod.rs b/altium/src/draw/mod.rs index 9f169ae..0b077f2 100644 --- a/altium/src/draw/mod.rs +++ b/altium/src/draw/mod.rs @@ -3,7 +3,16 @@ pub(crate) mod canvas; mod svg; -pub use canvas::{Canvas, DrawImage, DrawLine, DrawPolygon, DrawRectangle, DrawText}; +pub use canvas::{ + Canvas, + DrawImage, + DrawLine, + DrawPolygon, + DrawRectangle, + DrawText, + LineCap, + LineJoin, +}; pub use self::svg::SvgCtx; pub use crate::common::{Location, PosHoriz, PosVert, Rgb}; diff --git a/altium/src/parse/bin.rs b/altium/src/parse/bin.rs index 78b9b29..badea94 100644 --- a/altium/src/parse/bin.rs +++ b/altium/src/parse/bin.rs @@ -1,4 +1,4 @@ -use std::str; +use std::{fmt::Debug, str}; use crate::{common::str_from_utf8, error::TruncBuf, ErrorKind}; @@ -26,35 +26,28 @@ pub fn extract_sized_buf( ) -> Result<(&[u8], &[u8]), ErrorKind> { let (data_len, rest): (usize, _) = match len_match { BufLenMatch::U24UpperOne | BufLenMatch::U24UpperZero => { - let [l0, l1, l2, l3, rest @ ..] = buf else { - return Err(ErrorKind::BufferTooShort(4, TruncBuf::new(buf))); - }; + let (arr, rest) = split_chunk::<4>(buf)?; + let mut arr = *arr; + let l3 = arr[3]; + arr[3] = 0x00; if len_match == BufLenMatch::U24UpperOne { - assert_eq!(*l3, 0x01, "expected 0x01 in uppper bit but got {l3}"); + assert_eq!(l3, 0x01, "expected 0x01 in uppper bit but got {l3}"); } else if len_match == BufLenMatch::U24UpperZero { - assert_eq!(*l3, 0x00, "expected 0x00 in uppper bit but got {l3}"); + assert_eq!(l3, 0x00, "expected 0x00 in uppper bit but got {l3}"); } - let len = u32::from_le_bytes([*l0, *l1, *l2, 0x00]) - .try_into() - .unwrap(); + let len = u32::from_le_bytes(arr).try_into().unwrap(); (len, rest) } BufLenMatch::U32 => { - let (Some(len_buf), Some(rest)) = (buf.get(..4), buf.get(4..)) else { - return Err(ErrorKind::BufferTooShort(4, TruncBuf::new(buf))); - }; - let len = u32::from_le_bytes(len_buf.try_into().unwrap()) - .try_into() - .unwrap(); + let (arr, rest) = split_chunk::<4>(buf)?; + let len = u32::from_le_bytes(*arr).try_into().unwrap(); (len, rest) } BufLenMatch::U8 => { - let [l0, rest @ ..] = buf else { - return Err(ErrorKind::BufferTooShort(4, TruncBuf::new(buf))); - }; - ((*l0).into(), rest) + let (arr, rest) = split_chunk::<1>(buf)?; + (arr[0].into(), rest) } }; @@ -73,6 +66,12 @@ pub fn extract_sized_buf( } } +/// Helper method for `split_first_chunk` that returns a buffer error +pub fn split_chunk(buf: &[u8]) -> Result<(&[u8; N], &[u8]), ErrorKind> { + buf.split_first_chunk::() + .ok_or_else(|| ErrorKind::BufferTooShort(N, TruncBuf::new(buf))) +} + /// Extract a buffer that starts with a 1-, 3- or 4-byte header to a string pub fn extract_sized_utf8_buf( buf: &[u8], diff --git a/altium/src/sch/component.rs b/altium/src/sch/component.rs index 6710672..2cc65ba 100644 --- a/altium/src/sch/component.rs +++ b/altium/src/sch/component.rs @@ -67,14 +67,6 @@ 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 @@ -106,3 +98,15 @@ impl Component { self.records.iter() } } + +impl Draw for Component { + type Context<'a> = (); + + fn draw(&self, canvas: &mut C, _ctx: &()) { + let ctx = SchDrawCtx { + fonts: &self.fonts, + storage: &self.storage, + }; + self.records.iter().for_each(|r| r.draw(canvas, &ctx)); + } +} diff --git a/altium/src/sch/pin.rs b/altium/src/sch/pin.rs index bf5fa8a..d68509b 100644 --- a/altium/src/sch/pin.rs +++ b/altium/src/sch/pin.rs @@ -7,7 +7,7 @@ use altium_macros::FromRecord; use log::warn; use super::SchRecord; -use crate::common::{i32_mils_to_nm, u32_mils_to_nm, Location, Rotation90, Visibility}; +use crate::common::{mils_to_nm, Location, Rotation90, Visibility}; use crate::error::AddContext; use crate::parse::ParseUtf8; use crate::parse::{FromRecord, FromUtf8}; @@ -45,20 +45,21 @@ pub struct SchPin { impl SchPin { pub(crate) fn parse(buf: &[u8]) -> Result { // 6 bytes unknown - let [_, _, _, _, _, _, rest @ ..] = buf else { - return Err(PinError::TooShort(buf.len(), "initial group").into()); - }; + let (_unknown, rest) = buf + .split_first_chunk::<6>() + .ok_or(PinError::TooShort(buf.len(), "initial group"))?; + // 6 more bytes unknown - symbols - let [_, _, _, _, _, _, rest @ ..] = rest else { - return Err(PinError::TooShort(rest.len(), "second group").into()); - }; + let (_unknown, rest) = rest + .split_first_chunk::<6>() + .ok_or(PinError::TooShort(rest.len(), "second group"))?; let (description, rest) = sized_buf_to_utf8(rest, "description")?; // TODO: ty_info - let [formal_type, _ty_info, rot_hide, l0, l1, x0, x1, y0, y1, rest @ ..] = rest else { - return Err(PinError::TooShort(rest.len(), "position extraction").into()); - }; + let ([formal_type, _ty_info, rot_hide, l0, l1, x0, x1, y0, y1], rest) = rest + .split_first_chunk() + .ok_or(PinError::TooShort(rest.len(), "position extraction"))?; assert_eq!( *formal_type, 1, @@ -69,9 +70,9 @@ impl SchPin { let location_x = i16::from_le_bytes([*x0, *x1]); let location_y = i16::from_le_bytes([*y0, *y1]); - let [_, _, _, _, rest @ ..] = rest else { - return Err(PinError::TooShort(rest.len(), "remaining buffer").into()); - }; + let (_unknown, rest) = rest + .split_first_chunk::<4>() + .ok_or(PinError::TooShort(rest.len(), "remaining buffer"))?; let (name, rest) = sized_buf_to_utf8(rest, "name")?; let (designator, rest) = sized_buf_to_utf8(rest, "designator")?; @@ -81,8 +82,8 @@ impl SchPin { } let location = Location { - x: i32_mils_to_nm(i32::from(location_x))?, - y: i32_mils_to_nm(i32::from(location_y))?, + x: mils_to_nm(i32::from(location_x))?, + y: mils_to_nm(i32::from(location_y))?, }; let retval = Self { formal_type: *formal_type, @@ -92,7 +93,7 @@ impl SchPin { designator: designator.into(), name: name.into(), location, - length: u32_mils_to_nm(u32::from(length))?, + length: mils_to_nm(u32::from(length))?, // location_x: i32::from(location_x) * 10, // location_y: i32::from(location_y) * 10, // length: u32::from(length) * 10, diff --git a/altium/src/sch/record.rs b/altium/src/sch/record.rs index e18c5fa..ae8ab87 100644 --- a/altium/src/sch/record.rs +++ b/altium/src/sch/record.rs @@ -61,7 +61,7 @@ pub(super) use parse::parse_all_records; use super::params::Justification; use super::pin::SchPin; -use crate::common::{Location, LocationFract, ReadOnlyState, UniqueId}; +use crate::common::{mils_to_nm, Location, LocationFract, ReadOnlyState, UniqueId}; use crate::error::{AddContext, TruncBuf}; use crate::font::FontCollection; use crate::Error; @@ -242,7 +242,8 @@ pub struct Bezier { color: Rgb, index_in_sheet: i16, is_not_accessible: bool, - line_width: u16, + #[from_record(convert = mils_to_nm)] + line_width: u32, #[from_record(array = true, count = b"LocationCount", map = (X -> x, Y -> y))] pub locations: Vec, owner_index: u8, @@ -259,7 +260,8 @@ pub struct PolyLine { owner_part_id: i8, is_not_accessible: bool, index_in_sheet: i16, - line_width: u16, + #[from_record(convert = mils_to_nm)] + line_width: u32, pub color: Rgb, #[from_record(array = true, count = b"LocationCount", map = (X -> x, Y -> y))] pub locations: Vec, @@ -275,7 +277,8 @@ pub struct Polygon { index_in_sheet: i16, is_not_accessible: bool, is_solid: bool, - line_width: u16, + #[from_record(convert = mils_to_nm)] + line_width: u32, #[from_record(array = true, count = b"LocationCount", map = (X -> x, Y -> y))] pub locations: Vec, owner_index: u8, @@ -293,7 +296,8 @@ pub struct Ellipse { index_in_sheet: i16, is_not_accessible: bool, is_solid: bool, - line_width: u16, + #[from_record(convert = mils_to_nm)] + line_width: u32, pub location: Location, owner_index: u8, owner_part_id: i8, @@ -323,7 +327,8 @@ pub struct RectangleRounded { index_in_sheet: i16, is_not_accessible: bool, is_solid: bool, - line_width: u16, + #[from_record(convert = mils_to_nm)] + line_width: u32, location: Location, owner_index: u8, owner_part_id: i8, @@ -345,7 +350,8 @@ pub struct ElipticalArc { radius_frac: i32, secondary_radius: i8, secondary_radius_frac: i32, - line_width: i8, + #[from_record(convert = mils_to_nm)] + line_width: i32, start_angle: f32, end_angle: f32, pub color: Rgb, @@ -365,7 +371,8 @@ pub struct Arc { radius_frac: i32, secondary_radius: i8, secondary_radius_frac: i32, - line_width: i8, + #[from_record(convert = mils_to_nm)] + line_width: i32, start_angle: f32, end_angle: f32, pub color: Rgb, @@ -382,7 +389,8 @@ pub struct Line { index_in_sheet: i16, is_not_accessible: bool, is_solid: bool, - line_width: u16, + #[from_record(convert = mils_to_nm)] + line_width: u32, location_count: u16, location_x: i32, location_y: i32, @@ -403,7 +411,8 @@ pub struct Rectangle { index_in_sheet: i16, is_not_accessible: bool, pub is_solid: bool, - line_width: u16, + #[from_record(convert = mils_to_nm)] + line_width: u32, /// Bottom left corner pub location: Location, owner_index: u8, @@ -420,7 +429,8 @@ pub struct SheetSymbol { owner_index: u8, owner_part_id: i8, index_in_sheet: i16, - line_width: u16, + #[from_record(convert = mils_to_nm)] + line_width: u32, pub color: Rgb, pub area_color: Rgb, is_solid: bool, @@ -533,7 +543,8 @@ pub struct Bus { owner_index: u8, owner_part_id: i8, index_in_sheet: i16, - line_width: u16, + #[from_record(convert = mils_to_nm)] + line_width: u32, pub color: Rgb, #[from_record(array = true, count = b"LocationCount", map = (X -> x, Y -> y))] pub locations: Vec, @@ -546,7 +557,8 @@ pub struct Bus { pub struct Wire { owner_index: u8, owner_part_id: i8, - line_width: u16, + #[from_record(convert = mils_to_nm)] + line_width: u32, pub color: Rgb, #[from_record(array = true, count = b"LocationCount", map = (X -> x, Y -> y))] pub locations: Vec, diff --git a/altium/src/sch/record/draw.rs b/altium/src/sch/record/draw.rs index ce91227..d39c7f3 100644 --- a/altium/src/sch/record/draw.rs +++ b/altium/src/sch/record/draw.rs @@ -1,8 +1,8 @@ //! How to draw records, components, etc use crate::common::{Location, PosHoriz, PosVert, Rgb, Rotation90, Visibility}; -use crate::draw::canvas::DrawRectangle; use crate::draw::canvas::{Canvas, DrawLine, DrawText}; +use crate::draw::canvas::{DrawRectangle, LineCap}; use crate::draw::{Draw, DrawPolygon}; use crate::font::FontCollection; use crate::sch::pin::SchPin; @@ -86,24 +86,27 @@ impl Draw for SchPin { start, end, color: Rgb::black(), - width: 4, - // ..Default::default() + width: 30000, + start_cap: LineCap::Round, + ..Default::default() }); // Altium draws a small white plus at the pin's connect position, so we // do too canvas.draw_line(DrawLine { - start: end.add_x(1), - end: end.add_x(-1), + start: end.add_x(10000), + end: end.add_x(-10000), color: Rgb::white(), - width: 1, + width: 5000, + ..Default::default() }); canvas.draw_line(DrawLine { - start: end.add_y(1), - end: end.add_y(-1), + start: end.add_y(10000), + end: end.add_y(-10000), color: Rgb::white(), - width: 1, + width: 5000, + ..Default::default() }); // FIXME: use actual spacing & fonts from pin spec @@ -187,6 +190,7 @@ impl Draw for record::PolyLine { end: b, color: self.color, width: self.line_width * 4, + ..Default::default() }); } } @@ -240,6 +244,7 @@ impl Draw for record::Line { end: Location::new(self.corner_x, self.corner_y), color: self.color, width: self.line_width, + ..Default::default() }); } } @@ -347,6 +352,7 @@ impl Draw for record::Bus { end: b, color: self.color, width: self.line_width * 4, + ..Default::default() }); } } @@ -364,6 +370,7 @@ impl Draw for record::Wire { end: b, color: self.color, width: self.line_width * 4, + ..Default::default() }); } } diff --git a/altium/src/sch/record/parse.rs b/altium/src/sch/record/parse.rs index 77568f3..9a6220f 100644 --- a/altium/src/sch/record/parse.rs +++ b/altium/src/sch/record/parse.rs @@ -17,29 +17,25 @@ pub fn parse_all_records(buf: &[u8], err_name: &str) -> Result, E const LEN_MASK: u32 = 0x00ffffff; const UTF8_RECORD_TY: u32 = 0x00; const PIN_RECORD_TY: u32 = 0x01; - // No magic values :) - const U32_BYTES: usize = 4; let mut working = buf; let mut parsed = Vec::new(); while !working.is_empty() { - assert!( - working.len() >= 4, - "expected at least 4 bytes, only got {}", - working.len() - ); + let (arr, rest) = working + .split_first_chunk::<4>() + .unwrap_or_else(|| panic!("expected at least 4 bytes, only got {}", working.len())); - let info = u32::from_le_bytes(working[..4].try_into().unwrap()); - let ty = (info & TY_MAEK) >> TY_SHIFT; - let len: usize = (info & LEN_MASK).try_into().unwrap(); + let ty_info = u32::from_le_bytes(*arr); + let ty = (ty_info & TY_MAEK) >> TY_SHIFT; + let len: usize = (ty_info & LEN_MASK).try_into().unwrap(); // Don't include the null terminator (which is included in `len`) - let to_parse = &working[U32_BYTES..(U32_BYTES + len - 1)]; + let to_parse = &rest[..(len - 1)]; // But do do a sanity check that the null exists - assert_eq!(working[U32_BYTES + len - 1], 0, "Expected null terimation"); + assert_eq!(rest[len - 1], 0, "Expected null terimation"); - working = &working[U32_BYTES + len..]; + working = &rest[len..]; let record = match ty { UTF8_RECORD_TY => parse_any_record(to_parse), diff --git a/ecadg/src/app.rs b/ecadg/src/app.rs index 608a60e..b2a772c 100644 --- a/ecadg/src/app.rs +++ b/ecadg/src/app.rs @@ -1,10 +1,13 @@ +use std::sync::Arc; use std::{path::PathBuf, sync::atomic::Ordering::SeqCst}; -use egui::{ScrollArea, TextStyle, Ui, Vec2}; +use egui::{ScrollArea, TextStyle, Ui}; use log::debug; use crate::backend::{ open_file_async, + rect_disp, + vec_disp, GlobalQueue, SchDocTab, SchLibTab, @@ -32,17 +35,6 @@ pub struct GuiApp { active_tab: Option, } -// impl Default for GuiApp { -// fn default() -> Self { -// Self { -// active_tab: None, -// tabs: vec![], -// recent_files: vec![], -// errors: vec![], -// } -// } -// } - impl GuiApp { /// Called once before the first frame. #[must_use] @@ -216,20 +208,20 @@ fn make_center_panel(app: &mut GuiApp, ui: &mut Ui) { let tabdata = &mut app.tabs[tab_idx]; let hovered = response.hovered(); - // let hovered = ui.input(|istate| { - // istate - // .pointer - // .latest_pos() - // .is_some_and(|pos| response.rect.contains(pos)) - // }); let view_state = &mut tabdata.view_state; + view_state.rect = rect; if hovered { view_state.update_dragged_by(response.drag_delta()); ui.input(|istate| view_state.update_with_input_state(istate)); } #[cfg(feature = "_debug")] - ui.label(format!("view_state: {view_state:?}, hovered: {hovered}",)); + ui.label(format!( + "view_state: {view_state:?}; vp: {}; hovered: {hovered}; vs offset_gfx: {}; pos world: {}", + rect_disp(view_state.world_viewport()), + vec_disp(view_state.offset_gfx()), + vec_disp(view_state.px_to_world(view_state.latest_pos.unwrap_or_default().to_vec2())) + )); match &mut tabdata.inner { TabDataInner::SchLib(tab) => make_center_panel_schlib(ui, rect, tab, view_state), @@ -239,15 +231,11 @@ fn make_center_panel(app: &mut GuiApp, ui: &mut Ui) { #[allow(clippy::needless_pass_by_ref_mut)] fn make_center_panel_schlib(ui: &mut Ui, rect: egui::Rect, tab: &SchLibTab, vs: &ViewState) { - let comp = &tab.components[tab.active_component]; - let dims = Vec2 { - x: rect.width(), - y: rect.height(), - }; - ui.label(format!("rect: {rect:?}, dims: {dims:?}")); + let comp = Arc::clone(&tab.components[tab.active_component]); + ui.label(format!("rect: {rect:?}, vs: {vs:?}")); egui::Frame::canvas(ui.style()).show(ui, |ui| { ui.painter() - .add(crate::gfx::SchLibCallback::callback(rect, comp, vs, dims)) + .add(crate::gfx::SchLibCallback::callback(comp, vs)) }); } diff --git a/ecadg/src/backend.rs b/ecadg/src/backend.rs index 43cd218..95f6070 100644 --- a/ecadg/src/backend.rs +++ b/ecadg/src/backend.rs @@ -4,6 +4,7 @@ use std::{ path::{Path, PathBuf}, sync::{ atomic::{AtomicBool, Ordering::SeqCst}, + Arc, Mutex, }, thread, @@ -14,14 +15,21 @@ use altium::{ SchDoc, SchLib, }; -use egui::Vec2; +use egui::{Pos2, Rect, Vec2}; use log::{info, trace}; +use lyon::geom::euclid::{Point2D, UnknownUnit}; /// One entry per tab static GLOBAL_QUEUE: Mutex = Mutex::new(GlobalQueue::new()); /// Indicating that our GUI thread should pick this up pub static HAS_FRESH_DATA: AtomicBool = AtomicBool::new(false); +#[allow(dead_code)] +pub const NM_PER_M: f32 = 1e9; +pub const M_PER_NM: f32 = 1e-9; +/// Initial scale in m/px +const DEFAULT_SCALE: f32 = 1e-5; + #[derive(Debug)] pub struct GlobalQueue { pub tabs: VecDeque, @@ -96,14 +104,18 @@ pub struct TabData { pub struct ViewState { /// Zoom if applicable, in m/px pub scale: f32, - /// Center position - pub center: Vec2, + /// Offset of world center from view center, in (m, m) + pub offset: Vec2, + /// Postion of the cursor in window coordinates + pub latest_pos: Option, + /// Rectangle of our view in window coordinates + pub rect: Rect, } impl ViewState { /// Apply a drag to this view state. Only do for a secondary (right) click. pub fn update_dragged_by(&mut self, drag_delta: Vec2) { - self.center += drag_delta; + self.offset += flip_y(drag_delta) * self.scale; } /// Update with zoom or multitouch (trackpad). Requires a zoom delta separately @@ -112,15 +124,69 @@ impl ViewState { const SCALE_MAX: f32 = 10e-3; // 10 mm per px self.scale = f32::clamp(self.scale / istate.zoom_delta(), SCALE_MIN, SCALE_MAX); - self.center += istate.raw_scroll_delta; + self.offset += flip_y(istate.smooth_scroll_delta) * self.scale; + self.latest_pos = istate.pointer.latest_pos(); + } + + /// Convert a pixel-sized shape to a GUI-sized shape (in the window if within the scale of + /// (-1.0..1.0)). + pub fn px_to_gfx(&self, pos: Vec2) -> Vec2 { + Vec2 { + x: pos.x / (self.rect.width() / 2.0), + y: pos.y / (self.rect.height() / 2.0), + } + } + + /// Convert a point in world coordinates to graphics coordinates + #[allow(dead_code)] + pub fn world_to_gfx(&self, pos: Vec2) -> Vec2 { + self.px_to_gfx((pos + self.offset) / self.scale) + } + + pub fn px_to_world(&self, pos: Vec2) -> Vec2 { + flip_y(pos - self.rect.center().to_vec2()) * self.scale - self.offset } + + /// Offset in graphics coordinates + pub fn offset_gfx(&self) -> Vec2 { + self.px_to_gfx(self.offset / self.scale) + } + + /// What portion of the world we are able to view + pub fn world_viewport(&self) -> Rect { + Rect::from_center_size(self.offset.to_pos2(), self.rect.size() * self.scale) + } +} + +pub fn rect_disp(r: Rect) -> String { + format!("[{} - {}]", pos_disp(r.min), pos_disp(r.max)) +} + +pub fn vec_disp(v: Vec2) -> String { + format!("[{:.4} {:.4}]", v.x, v.y) +} + +pub fn pos_disp(v: Pos2) -> String { + vec_disp(v.to_vec2()) +} + +/// Flip vertically for converting from graphics to world coordinates +pub fn flip_y(mut v: Vec2) -> Vec2 { + v.y = -v.y; + v +} + +pub fn v_to_p2d(v: Vec2) -> Point2D { + Point2D::new(v.x, v.y) } impl Default for ViewState { fn default() -> Self { Self { - scale: 1e-4, // m/px - center: Vec2::default(), + scale: DEFAULT_SCALE, + offset: Vec2::default(), + latest_pos: None, + rect: Rect::ZERO, } } } @@ -136,7 +202,7 @@ pub enum TabDataInner { /// and track the selection #[derive(Debug, Default)] pub struct SchLibTab { - pub components: Vec, + pub components: Vec>, /// Index in `components` to display in a scrollable list pub active_component: usize, pub search_query: String, @@ -209,7 +275,7 @@ fn schlib_to_tab(path: PathBuf) -> Option { }; let mut inner = SchLibTab { - components: lib.components().collect(), + components: lib.components().map(Arc::new).collect(), ..Default::default() }; diff --git a/ecadg/src/draw.rs b/ecadg/src/draw.rs index b36a22b..fae3d2f 100644 --- a/ecadg/src/draw.rs +++ b/ecadg/src/draw.rs @@ -51,7 +51,7 @@ impl Canvas for PlotUiWrapper<'_> { [f64::from(item.end.x()), f64::from(item.end.y())], ]) .color(to_c32(item.color)) - .width(item.width), + .width(item.width as f32), ); } @@ -86,7 +86,7 @@ impl Canvas for PlotUiWrapper<'_> { .collect::(), ) .stroke(Stroke { - width: f32::from(item.stroke_width) * 20.0, + width: item.stroke_width as f32 * 20.0, color: to_c32(item.stroke_color), }) .fill_color(to_c32(item.fill_color).gamma_multiply(1.0)); diff --git a/ecadg/src/gfx.rs b/ecadg/src/gfx.rs index 0c3f5ca..334c813 100644 --- a/ecadg/src/gfx.rs +++ b/ecadg/src/gfx.rs @@ -1,14 +1,18 @@ //! Entrypoint for GPU rendering items mod grid; +mod origin; mod poly; +mod tessellated; mod triangle; // use std::sync::Arc; +use std::sync::Arc; + use altium::sch::Component; use eframe::egui_wgpu; -use egui::{PaintCallbackInfo, Vec2}; +use egui::PaintCallbackInfo; use egui_wgpu::wgpu::{CommandBuffer, CommandEncoder, Device, Queue, RenderPass}; use egui_wgpu::CallbackResources; @@ -28,40 +32,35 @@ pub fn init_graphics(cc: &eframe::CreationContext<'_>) { .write() .callback_resources .insert(GraphicsCtx { - // device: Arc::clone(device), - triangle_ctx: triangle::TriangleCtx::init(wgpu_render_state, device), - grid_ctx: grid::GridCtx::init(wgpu_render_state, device), + triangle: triangle::TriangleCtx::init(wgpu_render_state, device), + grid: grid::GridCtx::init(wgpu_render_state, device), + origin: origin::OriginCtx::init(wgpu_render_state, device), + tess: tessellated::TessCtx::init(wgpu_render_state, device), }); } /// Context that is created upon init and accessible via each render struct GraphicsCtx { - // device: Arc, - triangle_ctx: triangle::TriangleCtx, - grid_ctx: grid::GridCtx, + triangle: triangle::TriangleCtx, + grid: grid::GridCtx, + origin: origin::OriginCtx, + tess: tessellated::TessCtx, } /// Callback for drawing schlib items pub struct SchLibCallback { - scale: f32, - center: Vec2, - dims: Vec2, + view_state: ViewState, + comp: Arc, } impl SchLibCallback { /// Entrypoint for rendering a single component in a schematic library - pub fn callback( - rect: egui::Rect, - _comp: &Component, - vs: &ViewState, - dims: Vec2, - ) -> egui::PaintCallback { + pub fn callback(comp: Arc, vs: &ViewState) -> egui::PaintCallback { let cb_ctx = Self { - scale: vs.scale, - center: vs.center, - dims, + view_state: *vs, + comp, }; - egui_wgpu::Callback::new_paint_callback(rect, cb_ctx) + egui_wgpu::Callback::new_paint_callback(vs.rect, cb_ctx) } } @@ -76,9 +75,11 @@ impl egui_wgpu::CallbackTrait for SchLibCallback { ) -> Vec { let ctx: &mut GraphicsCtx = resources.get_mut().unwrap(); - ctx.triangle_ctx.prepare(queue); - ctx.grid_ctx - .prepare(queue, self.dims, self.scale, self.center); + ctx.triangle.prepare(queue); + ctx.grid.prepare(queue, self.view_state); + ctx.tess + .prepare(queue, self.view_state, self.comp.as_ref(), &()); + ctx.origin.prepare(queue, self.view_state); Vec::new() } @@ -90,7 +91,9 @@ impl egui_wgpu::CallbackTrait for SchLibCallback { resources: &'a CallbackResources, ) { let ctx: &GraphicsCtx = resources.get().unwrap(); - ctx.triangle_ctx.paint(render_pass); - ctx.grid_ctx.paint(render_pass, self.scale); + // ctx.triangle.paint(render_pass); + ctx.grid.paint(render_pass, self.view_state); + ctx.tess.paint(render_pass, self.view_state); + ctx.origin.paint(render_pass, self.view_state); } } diff --git a/ecadg/src/gfx/grid.rs b/ecadg/src/gfx/grid.rs index aaef790..ad170c0 100644 --- a/ecadg/src/gfx/grid.rs +++ b/ecadg/src/gfx/grid.rs @@ -1,7 +1,7 @@ #![allow(clippy::cast_sign_loss)] #![allow(clippy::cast_possible_truncation)] -use std::mem; +use std::mem::{self}; use bytemuck::{Pod, Zeroable}; use eframe::egui_wgpu::{self, wgpu, RenderState}; @@ -9,7 +9,21 @@ use egui::Vec2; use egui_wgpu::wgpu::{Device, Queue, RenderPass}; use wgpu::util::DeviceExt; -const GRID_INDICES: usize = 4; +use crate::backend::ViewState; + +/// spacing for major grid in m. 10mm currently +const MAJOR_SPACING_M: f32 = 10e-3; +const MAJOR_SPACING_MULT: f32 = 1.0; +/// Size of minor grid compared to major grid. 1mm currently. +const MINOR_SPACING_MULT: f32 = 0.1; + +/// Saturation of major gridlines +const MAJOR_SATURATION: f32 = 1.0; +/// Saturation of minor gridlines +const MINOR_SATURATION: f32 = 0.4; + +/// Total number of times we run the grid shader +const GRID_INSTANCES: usize = 4; /// Data of window position used to determine layout #[derive(Clone, Copy, Debug, Pod, Zeroable)] @@ -36,6 +50,7 @@ impl Default for GridUniformBuf { pub struct GridInstanceBuf { /// Multiplier of spacing to make major and minor grids spacing_mult: f32, + /// Unused saturation: f32, /// 0 for horizontal, 1 for vertical is_vert: u32, @@ -48,36 +63,30 @@ impl GridInstanceBuf { // const MAJOR_HORIZ_IDX: u8 = 2; // const MAJOR_VERT_IDX: u8 = 3; - const MAJOR_SPACING_MULT: f32 = 1.0; - const MINOR_SPACING_MULT: f32 = 0.1; - /// Horizontal & vertical for major and minor - fn all() -> [Self; GRID_INDICES] { - const MAJOR_SATURATION: f32 = 1.0; - const MINOR_SATURATION: f32 = 0.4; - + fn all() -> [Self; GRID_INSTANCES] { // First two are minor, second two are major. We do this so major overwrites minor [ Self { - spacing_mult: Self::MINOR_SPACING_MULT, + spacing_mult: MINOR_SPACING_MULT, saturation: MINOR_SATURATION, is_vert: 0, _padding: Default::default(), }, Self { - spacing_mult: Self::MINOR_SPACING_MULT, + spacing_mult: MINOR_SPACING_MULT, saturation: MINOR_SATURATION, is_vert: 1, _padding: Default::default(), }, Self { - spacing_mult: Self::MAJOR_SPACING_MULT, + spacing_mult: MAJOR_SPACING_MULT, saturation: MAJOR_SATURATION, is_vert: 0, _padding: Default::default(), }, Self { - spacing_mult: Self::MAJOR_SPACING_MULT, + spacing_mult: MAJOR_SPACING_MULT, saturation: MAJOR_SATURATION, is_vert: 1, _padding: Default::default(), @@ -218,21 +227,22 @@ impl GridCtx { /// Set up buffers to be ready to draw /// scale is m/px, center is in px - pub fn prepare(&mut self, queue: &Queue, window_dims: Vec2, scale: f32, center: Vec2) { - /// spacing for major grid in m. 10mm currently - const MAJOR_SPACING_M: f32 = 10e-3; + pub fn prepare(&mut self, queue: &Queue, vs: ViewState) { + let window_dims = vs.rect.size(); + let offset = vs.offset; + // spacing in pixels - let sp_px = MAJOR_SPACING_M / scale; + let sp_px = MAJOR_SPACING_M / vs.scale; // spacing as percent // TODO: find out why these need to be reversed to get accurate results, I have no clue let sp_pct_x = sp_px / window_dims.y; let sp_pct_y = sp_px / window_dims.x; - let offset_pct_x = (center.x / window_dims.x) % sp_pct_x; - let offset_pct_y = (center.y / window_dims.y) % sp_pct_y; + let offset_pct_x = (offset.x / window_dims.x) % sp_pct_x; + let offset_pct_y = (offset.y / window_dims.y) % sp_pct_y; - dbg!(offset_pct_x, sp_pct_x, offset_pct_y, sp_pct_y); + // dbg!(offset_pct_x, sp_pct_x, offset_pct_y, sp_pct_y); let uniform = GridUniformBuf { offset_mod_pct: Vec2 { @@ -252,18 +262,20 @@ impl GridCtx { } /// Draw needed lines - pub fn paint<'a>(&'a self, render_pass: &mut RenderPass<'a>, scale: f32) { + pub fn paint<'a>(&'a self, render_pass: &mut RenderPass<'a>, vs: ViewState) { render_pass.set_pipeline(&self.pipeline); render_pass.set_bind_group(0, &self.bind_group, &[]); render_pass.set_vertex_buffer(0, self.instance_buffer.slice(..)); + let scale = vs.scale; + if scale < 2e-3 { if scale < 200e-6 { // drawing minor - let x_lines = (2.0 / self.uniform_buffer_data.spacing_pct.x).ceil() - / GridInstanceBuf::MINOR_SPACING_MULT; - let y_lines = (2.0 / self.uniform_buffer_data.spacing_pct.y).ceil() - / GridInstanceBuf::MINOR_SPACING_MULT; + let x_lines = + (2.0 / self.uniform_buffer_data.spacing_pct.x).ceil() / MINOR_SPACING_MULT; + let y_lines = + (2.0 / self.uniform_buffer_data.spacing_pct.y).ceil() / MINOR_SPACING_MULT; debug_assert!(x_lines > 0.0); debug_assert!(y_lines > 0.0); @@ -272,10 +284,10 @@ impl GridCtx { } // drawing major - let x_lines = (2.0 / self.uniform_buffer_data.spacing_pct.x).ceil() - / GridInstanceBuf::MAJOR_SPACING_MULT; - let y_lines = (2.0 / self.uniform_buffer_data.spacing_pct.y).ceil() - / GridInstanceBuf::MAJOR_SPACING_MULT; + let x_lines = + (2.0 / self.uniform_buffer_data.spacing_pct.x).ceil() / MAJOR_SPACING_MULT; + let y_lines = + (2.0 / self.uniform_buffer_data.spacing_pct.y).ceil() / MAJOR_SPACING_MULT; debug_assert!(x_lines > 0.0); debug_assert!(y_lines > 0.0); diff --git a/ecadg/src/gfx/grid.wgsl b/ecadg/src/gfx/grid.wgsl index f5f1c26..553724c 100644 --- a/ecadg/src/gfx/grid.wgsl +++ b/ecadg/src/gfx/grid.wgsl @@ -22,7 +22,7 @@ struct GridInstanceBuf { @vertex fn vs_main( @builtin(vertex_index) idx: u32, - instance: GridInstanceBuf + instance: GridInstanceBuf, ) -> VertexOut { let rem = idx % u32(2); // -1.0 or +1.0 diff --git a/ecadg/src/gfx/origin.rs b/ecadg/src/gfx/origin.rs new file mode 100644 index 0000000..88cfecd --- /dev/null +++ b/ecadg/src/gfx/origin.rs @@ -0,0 +1,149 @@ +#![allow(clippy::cast_sign_loss)] +#![allow(clippy::cast_possible_truncation)] + +use std::mem; + +use bytemuck::{Pod, Zeroable}; +use eframe::egui_wgpu::{self, wgpu, RenderState}; +use egui::Vec2; +use egui_wgpu::wgpu::{Device, Queue, RenderPass}; +use wgpu::util::DeviceExt; + +use crate::backend::ViewState; + +const ORIGIN_LENGTH_PX: f32 = 60.0; +const ORIGIN_WIDTH_PX: f32 = 2.0; + +/// Data of window position used to determine layout +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +#[repr(C)] +struct OriginUniformBuf { + /// Offset of the origin from (0, 0), in GPU ranges + offset: Vec2, + /// Dimensions of the horizontal stroke in GPU ranges (-1.0..1.0) + hdims: Vec2, + /// Dimensions of the vertical stroke in GPU ranges (-1.0..1.0) + vdims: Vec2, +} + +impl Default for OriginUniformBuf { + fn default() -> Self { + Self { + offset: Vec2::ZERO, + hdims: Vec2::ZERO, + vdims: Vec2::ZERO, + } + } +} + +pub struct OriginCtx { + uniform_buffer: wgpu::Buffer, + uniform_buffer_data: OriginUniformBuf, + instance_buffer: wgpu::Buffer, + bind_group: wgpu::BindGroup, + pipeline: wgpu::RenderPipeline, +} + +impl OriginCtx { + pub fn init(render_state: &RenderState, device: &Device) -> Self { + let shader = device.create_shader_module(wgpu::include_wgsl!("origin.wgsl")); + let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("origin_uniform"), + contents: bytemuck::bytes_of(&OriginUniformBuf::default()), // 16 bytes aligned! + // Mapping at creation (as done by the create_buffer_init utility) doesn't require us to to add the MAP_WRITE usage + // (this *happens* to workaround this bug) + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + + let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("origin_instance"), + contents: &[], + usage: wgpu::BufferUsages::VERTEX, + }); + + let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("origin_uniform_layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some( + u64::try_from(mem::size_of::()) + .unwrap() + .try_into() + .unwrap(), + ), + }, + count: None, + }], + }); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("origin_bind_group"), + layout: &bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: uniform_buffer.as_entire_binding(), + }], + }); + + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("origin_pl_layout"), + bind_group_layouts: &[&bind_group_layout], + push_constant_ranges: &[], + }); + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("origin_pipeline"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs_main", + buffers: &[], + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_main", + targets: &[Some(render_state.target_format.into())], + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + + Self { + uniform_buffer, + bind_group, + pipeline, + instance_buffer, + uniform_buffer_data: OriginUniformBuf::default(), + } + } + + /// Set up buffers to be ready to draw + pub fn prepare(&mut self, queue: &Queue, vs: ViewState) { + let hdims = vs.px_to_gfx(Vec2::new(ORIGIN_LENGTH_PX, ORIGIN_WIDTH_PX)); + let vdims = vs.px_to_gfx(Vec2::new(ORIGIN_WIDTH_PX, ORIGIN_LENGTH_PX)); + let uniform = OriginUniformBuf { + offset: vs.offset_gfx(), + hdims, + vdims, + }; + + let buf = bytemuck::bytes_of(&uniform); + queue.write_buffer(&self.uniform_buffer, 0, buf); + + self.uniform_buffer_data = uniform; + } + + /// Draw needed triangles + pub fn paint<'a>(&'a self, render_pass: &mut RenderPass<'a>, _vs: ViewState) { + render_pass.set_pipeline(&self.pipeline); + render_pass.set_bind_group(0, &self.bind_group, &[]); + render_pass.set_vertex_buffer(0, self.instance_buffer.slice(..)); + render_pass.draw(0..12, 0..1); + } +} diff --git a/ecadg/src/gfx/origin.wgsl b/ecadg/src/gfx/origin.wgsl new file mode 100644 index 0000000..afc0f7f --- /dev/null +++ b/ecadg/src/gfx/origin.wgsl @@ -0,0 +1,50 @@ +// Shader to draw the origin crosshairs + +struct OriginUniformBuf { + offset: vec2, + hdims: vec2, + vdims: vec2, +} + +@group(0) @binding(0) +var origin_ctx: OriginUniformBuf; + +const HALF_HEIGHT = 0.4; +const HALF_WIDTH = 0.05; + +@vertex +fn vs_main( + @builtin(vertex_index) idx: u32, +) -> @builtin(position) vec4 { + let offset = origin_ctx.offset; + let hdims = origin_ctx.hdims; + let vdims = origin_ctx.vdims; + var pos: vec2; + + // 2 vertical triangles + if idx == 0 { + pos = vec2(offset.x - (vdims.x / 2), offset.y + (vdims.y / 2)); + } else if idx == 1 || idx == 3 { + pos = vec2(offset.x + (vdims.x / 2), offset.y + (vdims.y / 2)); + } else if idx == 2 || idx == 5 { + pos = vec2(offset.x - (vdims.x / 2), offset.y - (vdims.y / 2)); + } else if idx == 4 { + pos = vec2(offset.x + (vdims.x / 2), offset.y - (vdims.y / 2)); + // 2 horizontal triangles + } else if idx == 6 { + pos = vec2(offset.x - (hdims.x / 2), offset.y + (hdims.y / 2)); + } else if idx == 7 || idx == 9 { + pos = vec2(offset.x + (hdims.x / 2), offset.y + (hdims.y / 2)); + } else if idx == 8 || idx == 11{ + pos = vec2(offset.x - (hdims.x / 2), offset.y - (hdims.y / 2)); + } else if idx == 10 { + pos = vec2(offset.x + (hdims.x / 2), offset.y - (hdims.y / 2)); + } + + return vec4(pos.x, pos.y, 0.0, 1.0); +} + +@fragment +fn fs_main() -> @location(0) vec4 { + return vec4(1.0, 0.0, 1.0, 1.0); +} diff --git a/ecadg/src/gfx/tessellated.rs b/ecadg/src/gfx/tessellated.rs new file mode 100644 index 0000000..02cb1cc --- /dev/null +++ b/ecadg/src/gfx/tessellated.rs @@ -0,0 +1,564 @@ +#![allow(dead_code, unused_imports)] +#![allow(clippy::similar_names)] + +use std::mem; + +use alt_to_lyon::ToLyonTy; +use altium::draw::{Canvas, Draw}; +use bytemuck::{Pod, Zeroable}; +use eframe::egui_wgpu::{self, wgpu, RenderState}; +use egui::Vec2; +use egui_wgpu::wgpu::{Device, Queue, RenderPass}; +use log::debug; +use lyon::{ + geom::{euclid::Point2D, Box2D}, + math::point, + path::Path, + tessellation::{ + BuffersBuilder, + FillOptions, + FillTessellator, + FillVertex, + FillVertexConstructor, + StrokeOptions, + StrokeTessellator, + StrokeVertex, + StrokeVertexConstructor, + VertexBuffers, + }, +}; + +// use wgpu::util::DeviceExt; +use crate::backend::{v_to_p2d, ViewState, M_PER_NM, NM_PER_M}; + +/// Number of samples for anti-aliasing. Set to 1 to disable +// TODO +const SAMPLE_COUNT: u32 = 1; +// const SAMPLE_COUNT: u32 = 4; +const PRIM_BUFFER_LEN: usize = 256; + +const TOLERANCE: f32 = 0.0002; +const FILL_OPTIONS: FillOptions = FillOptions::DEFAULT.with_tolerance(TOLERANCE); +// const DEFAULT_STROKE +/// Stroke options, in world coordinates +const STROKE_OPTIONS: StrokeOptions = StrokeOptions::DEFAULT + .with_line_width(10000.0) + .with_line_cap(lyon::path::LineCap::Square) + .with_tolerance(100.0); +const DEFAULT_BUFFER_LEN: u64 = 1024 * 1024; + +#[repr(C)] +#[derive(Copy, Clone, Debug, Pod, Zeroable)] +struct Globals { + resolution: [f32; 2], + scroll_offset: [f32; 2], + scale: f32, + _pad: f32, +} + +#[repr(C)] +#[derive(Copy, Clone, Debug, Pod, Zeroable)] +struct Primitive { + color: [f32; 4], + translate: [f32; 2], + z_index: i32, + width: f32, + angle: f32, + scale: f32, + _pad: [f32; 2], +} + +impl Default for Primitive { + fn default() -> Self { + Self { + color: [0.0; 4], + translate: [0.0; 2], + z_index: 0, + width: 0.0, + angle: 0.0, + scale: 1.0, + _pad: [0.0; 2], + } + } +} + +/// Instance buffer for tesselation +#[repr(C)] +#[derive(Copy, Clone, Debug, Default, Pod, Zeroable)] +struct TessVertex { + position: [f32; 2], + /// Offset direction from the position, if needed + normal: [f32; 2], + color: [f32; 4], + /// Multiplier by the offser + stroke_width: f32, +} + +impl TessVertex { + fn desc() -> wgpu::VertexBufferLayout<'static> { + wgpu::VertexBufferLayout { + array_stride: mem::size_of::() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[ + wgpu::VertexAttribute { + offset: 0, + shader_location: 0, + format: wgpu::VertexFormat::Float32x2, + }, + wgpu::VertexAttribute { + offset: 0x8, + shader_location: 1, + format: wgpu::VertexFormat::Float32x2, + }, + wgpu::VertexAttribute { + offset: 0x10, + shader_location: 2, + format: wgpu::VertexFormat::Float32x4, + }, + wgpu::VertexAttribute { + offset: 0x20, + shader_location: 3, + format: wgpu::VertexFormat::Float32, + }, + ], + } + } +} + +pub struct TessCtx { + fill_vbo: wgpu::Buffer, + stroke_vbo: wgpu::Buffer, + fill_ibo: wgpu::Buffer, + stroke_ibo: wgpu::Buffer, + fill_geometry: VertexBuffers, + stroke_geometry: VertexBuffers, + /// Quantities to actually write if we need to pad them + prims_ubo: wgpu::Buffer, + globals_ubo: wgpu::Buffer, + bind_group: wgpu::BindGroup, + pipeline: wgpu::RenderPipeline, + /// Primitives as calculated by the CPU + cpu_primitives: Box<[Primitive]>, + fill_tess: FillTessellator, + stroke_tess: StrokeTessellator, + view_state: ViewState, +} + +impl TessCtx { + pub fn init(render_state: &RenderState, device: &Device) -> Self { + debug!("init tessellation shader"); + let shader = device.create_shader_module(wgpu::include_wgsl!("tessellated.wgsl")); + let fill_geometry: VertexBuffers = VertexBuffers::new(); + let stroke_geometry: VertexBuffers = VertexBuffers::new(); + + let globals_buffer_byte_size = mem::size_of::() as u64; + let prim_buffer_byte_size = (PRIM_BUFFER_LEN * mem::size_of::()) as u64; + + let fill_vbo = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("tess_fill_vbo"), + size: DEFAULT_BUFFER_LEN, + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + let fill_ibo = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("tess_fill_ibo"), + size: DEFAULT_BUFFER_LEN, + usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let stroke_vbo = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("tess_stroke_vbo"), + size: DEFAULT_BUFFER_LEN, + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + let stroke_ibo = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("tess_stroke_ibo"), + size: DEFAULT_BUFFER_LEN, + usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let prims_ubo = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("tess_prims_uniform_buff"), + size: prim_buffer_byte_size, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let globals_ubo = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("tess_globals_uniform_buff"), + size: globals_buffer_byte_size, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("tess_uniform_layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: wgpu::BufferSize::new(globals_buffer_byte_size), + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: wgpu::BufferSize::new(prim_buffer_byte_size), + }, + count: None, + }, + ], + }); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("tess_bind_group"), + layout: &bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(globals_ubo.as_entire_buffer_binding()), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Buffer(prims_ubo.as_entire_buffer_binding()), + }, + ], + }); + + let _depth_stencil_state = Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth32Float, + depth_write_enabled: true, + depth_compare: wgpu::CompareFunction::Greater, + stencil: wgpu::StencilState { + front: wgpu::StencilFaceState::IGNORE, + back: wgpu::StencilFaceState::IGNORE, + read_mask: 0, + write_mask: 0, + }, + bias: wgpu::DepthBiasState::default(), + }); + + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("tess_pl_layout"), + bind_group_layouts: &[&bind_group_layout], + push_constant_ranges: &[], + }); + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("tess_pipeline"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs_main", + buffers: &[TessVertex::desc()], + // buffers: &[TessVertex::desc(), TessVertex::desc()], + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_main", + // targets: &[Some(wgpu::ColorTargetState { + // format: wgpu::TextureFormat::Bgra8Unorm, + // blend: None, + // write_mask: wgpu::ColorWrites::ALL, + // })], + targets: &[Some(render_state.target_format.into())], + }), + primitive: wgpu::PrimitiveState { + // topology: wgpu::PrimitiveTopology::LineList, + topology: wgpu::PrimitiveTopology::TriangleList, + polygon_mode: wgpu::PolygonMode::Fill, + front_face: wgpu::FrontFace::Ccw, + // cull_mode: Some(wgpu::Face::Back), + ..wgpu::PrimitiveState::default() + }, + depth_stencil: None, + // depth_stencil: depth_stencil_state, + multisample: wgpu::MultisampleState { + count: SAMPLE_COUNT, + mask: u64::MAX, + alpha_to_coverage_enabled: false, + }, + // multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + + Self { + fill_vbo, + stroke_vbo, + fill_ibo, + stroke_ibo, + fill_geometry, + stroke_geometry, + prims_ubo, + globals_ubo, + bind_group, + pipeline, + cpu_primitives: [Primitive::default(); PRIM_BUFFER_LEN].into(), + fill_tess: FillTessellator::new(), + stroke_tess: StrokeTessellator::new(), + view_state: ViewState::default(), + } + } + + /// True if we need to update the fill buffer and run its shaders + fn render_fill(&self) -> bool { + debug_assert!( + !(self.fill_geometry.indices.is_empty() ^ self.fill_geometry.vertices.is_empty()) + ); + + !self.fill_geometry.indices.is_empty() + } + + /// True if we need to update the stroke buffer and run its shaders + fn render_stroke(&self) -> bool { + debug_assert!( + !(self.stroke_geometry.indices.is_empty() ^ self.stroke_geometry.vertices.is_empty()) + ); + + !self.stroke_geometry.indices.is_empty() + } + + fn needs_render(&self) -> bool { + self.render_fill() || self.render_stroke() + } + + /// Set up buffers to be ready to draw + pub fn prepare( + &mut self, + queue: &Queue, + vs: ViewState, + item: &D, + item_ctx: &D::Context<'_>, + ) { + self.view_state = vs; + self.fill_geometry.vertices.clear(); + self.fill_geometry.indices.clear(); + self.stroke_geometry.vertices.clear(); + self.stroke_geometry.indices.clear(); + + item.draw(self, item_ctx); + + if !self.needs_render() { + debug!("skipping tessellation prepare"); + return; + } + + let uniform = Globals { + resolution: [vs.rect.width(), vs.rect.height()], + scale: vs.scale, + scroll_offset: vs.offset.into(), + _pad: 0.0, + }; + + queue.write_buffer(&self.globals_ubo, 0, bytemuck::bytes_of(&uniform)); + queue.write_buffer( + &self.prims_ubo, + 0, + bytemuck::cast_slice(&self.cpu_primitives), + ); + + if self.render_fill() { + with_aligned_buf(&mut self.fill_geometry.vertices, |buf| { + queue.write_buffer(&self.fill_vbo, 0, buf); + }); + with_aligned_buf(&mut self.fill_geometry.indices, |buf| { + queue.write_buffer(&self.fill_ibo, 0, buf); + }); + } + + if self.render_stroke() { + with_aligned_buf(&mut self.stroke_geometry.vertices, |buf| { + queue.write_buffer(&self.stroke_vbo, 0, buf); + }); + with_aligned_buf(&mut self.stroke_geometry.indices, |buf| { + queue.write_buffer(&self.stroke_ibo, 0, buf); + }); + } + } + + /// Draw needed triangles + pub fn paint<'a>(&'a self, render_pass: &mut RenderPass<'a>, _vs: ViewState) { + if !self.needs_render() { + debug!("skipping tessellation paint"); + return; + } + + render_pass.insert_debug_marker("debug1"); + render_pass.set_pipeline(&self.pipeline); + render_pass.set_bind_group(0, &self.bind_group, &[]); + render_pass.insert_debug_marker("debug2"); + + if self.render_fill() { + render_pass.insert_debug_marker("debug fill"); + render_pass.set_vertex_buffer(0, self.fill_vbo.slice(..)); + render_pass.set_index_buffer(self.fill_ibo.slice(..), wgpu::IndexFormat::Uint16); + render_pass.draw_indexed( + 0..self.fill_geometry.indices.len().try_into().unwrap(), + 0, + 0..1, + ); + } + + if self.render_stroke() { + render_pass.insert_debug_marker("debug stroke"); + render_pass.set_vertex_buffer(0, self.stroke_vbo.slice(..)); + render_pass.set_index_buffer(self.stroke_ibo.slice(..), wgpu::IndexFormat::Uint16); + render_pass.draw_indexed( + 0..self.stroke_geometry.indices.len().try_into().unwrap(), + 0, + 0..1, + ); + } + } +} + +impl altium::sealed::Sealed for TessCtx {} +impl Canvas for TessCtx { + fn draw_text(&mut self, _item: altium::draw::DrawText) { + // todo!() + } + + fn draw_line(&mut self, item: altium::draw::DrawLine) { + let mut builder = Path::builder(); + builder.begin(point(item.start.x() as f32, item.start.y() as f32)); + builder.line_to(point(item.end.x() as f32, item.end.y() as f32)); + builder.close(); + let path = builder.build(); + + self.stroke_tess + .tessellate_path( + &path, + &STROKE_OPTIONS + .with_line_width(item.width as f32) + .with_start_cap(item.start_cap.to_lyon_ty()) + .with_end_cap(item.end_cap.to_lyon_ty()), + &mut BuffersBuilder::new( + &mut self.stroke_geometry, + WithColor(item.color.as_float_rgba()), + ), + ) + .unwrap(); + } + + fn draw_polygon(&mut self, _item: altium::draw::DrawPolygon) { + // todo!() + } + + fn draw_rectangle(&mut self, item: altium::draw::DrawRectangle) { + let min_x = item.x as f32; + let min_y = item.y as f32; + let max_x = min_x + item.width as f32; + let max_y = min_y + item.height as f32; + let rect = Box2D::new(Point2D::new(min_x, min_y), Point2D::new(max_x, max_y)); + + self.fill_tess + .tessellate_rectangle( + &rect, + &FILL_OPTIONS, + &mut BuffersBuilder::new( + &mut self.fill_geometry, + WithColor(item.fill_color.as_float_rgba()), + ), + ) + .unwrap(); + + self.stroke_tess + .tessellate_rectangle( + &rect, + &STROKE_OPTIONS.with_line_width(item.stroke_width as f32), + &mut BuffersBuilder::new( + &mut self.stroke_geometry, + WithColor(item.stroke_color.as_float_rgba()), + ), + ) + .unwrap(); + } + + fn draw_image(&mut self, _item: altium::draw::DrawImage) { + // todo!() + } +} + +/// This vertex constructor forwards the positions and normals provided by the +/// tessellators and add a shape id. +pub struct WithColor([f32; 4]); + +impl FillVertexConstructor for WithColor { + fn new_vertex(&mut self, vertex: FillVertex) -> TessVertex { + TessVertex { + position: vertex.position().to_array(), + normal: [0.0, 0.0], + // prim_id: self.0, + color: self.0, + stroke_width: 1.0, + } + } +} + +impl StrokeVertexConstructor for WithColor { + fn new_vertex(&mut self, vertex: StrokeVertex) -> TessVertex { + TessVertex { + position: vertex.position_on_path().to_array(), + normal: vertex.normal().to_array(), + // prim_id: self.0, + color: self.0, + stroke_width: vertex.line_width(), + } + } +} + +/// Temporarily extend a buffer to be a multiple of [`wgpu::COPY_BUFFER_ALIGNMENT`] for use +/// as a slice +/// +/// This is used because wgpu requires a buffer aligned to `COPY_BUFFER_ALIGNMENT` but we don't +/// want to zero-pad our tessellation buffers then forget about it (and wind up with extra +/// vertices / indices). So provide an aligned buffer only within the scope of a closure. +fn with_aligned_buf(buf: &mut Vec, f: F) +where + T: Default + bytemuck::NoUninit, + F: FnOnce(&[u8]), +{ + let t_size: wgpu::BufferAddress = std::mem::size_of::().try_into().unwrap(); + let len: wgpu::BufferAddress = buf.len().try_into().unwrap(); + let len_bytes = len * t_size; + // Next value that will meet the alignment + let target_len_bytes = len_bytes.next_multiple_of(wgpu::COPY_BUFFER_ALIGNMENT); + let to_add = (target_len_bytes - len_bytes).div_ceil(t_size); + + // push temporary elements to meet the needed alignment + for _ in 0..to_add { + buf.push(T::default()); + } + + f(bytemuck::cast_slice(buf)); + + // Remove the temporary elements so we can continue appending to the buffer later + buf.truncate(buf.len() - usize::try_from(to_add).unwrap()); +} + +mod alt_to_lyon { + pub trait ToLyonTy { + fn to_lyon_ty(&self) -> LyonTy; + } + + impl ToLyonTy for altium::draw::LineCap { + fn to_lyon_ty(&self) -> lyon::path::LineCap { + use altium::draw::LineCap; + match self { + LineCap::Butt => lyon::path::LineCap::Butt, + LineCap::Square => lyon::path::LineCap::Square, + LineCap::Round => lyon::path::LineCap::Round, + } + } + } +} diff --git a/ecadg/src/gfx/tessellated.wgsl b/ecadg/src/gfx/tessellated.wgsl new file mode 100644 index 0000000..1dd8c2f --- /dev/null +++ b/ecadg/src/gfx/tessellated.wgsl @@ -0,0 +1,81 @@ + +struct Globals { + resolution: vec2, + scroll_offset: vec2, + scale: f32, +}; + +struct Primitive { + color: vec4, + translate: vec2, + z_index: i32, + width: f32, + angle: f32, + scale: f32, + pad1: i32, + pad2: i32, +}; + +struct Primitives { + primitives: array, +}; + +@group(0) @binding(0) var globals: Globals; +@group(0) @binding(1) var u_primitives: Primitives; + +const INVERT_Y = vec2(1.0, -1.0); +const NM_PER_M: f32 = 1e9; +const M_PER_NM: f32 = 1e-9; + + +struct VertexOutput { + @location(0) v_color: vec4, + @builtin(position) position: vec4, +}; + +@vertex +fn vs_main( + @location(0) a_position: vec2, + @location(1) a_normal: vec2, + @location(2) a_color: vec4, + @location(3) a_stroke_width: f32, + @builtin(instance_index) instance_idx: u32 +) -> VertexOutput { + // let local_pos = (a_position * ) + let local_pos = a_position + a_normal * a_stroke_width; + let world_pos = (local_pos * M_PER_NM) + globals.scroll_offset; + let transformed_pos = (world_pos / globals.scale) / (0.5 * globals.resolution) ; + let position = vec4(transformed_pos.x, transformed_pos.y, 1.0, 1.0); + // let color = vec4(0.0, 1.0, 1.0, 0.8); + // var prim: Primitive = u_primitives.primitives[a_prim_id + instance_idx]; + + // var invert_y = vec2(1.0, -1.0); + + // var rotation = mat2x2( + // vec2(cos(prim.angle), -sin(prim.angle)), + // vec2(sin(prim.angle), cos(prim.angle)) + // ); + + // var local_pos = (a_position * prim.scale + a_normal * prim.width) * rotation; + // var world_pos = local_pos - globals.scroll_offset + prim.translate; + // var transformed_pos = world_pos * globals.zoom / (0.5 * globals.resolution) * invert_y; + + // var z = f32(prim.z_index) / 4096.0; + // var position = vec4(transformed_pos.x, transformed_pos.y, z, 1.0); + + // return VertexOutput(vec4(0.0, 1.0, 1.0, 0.8), vec4(a_position[0], a_position[1], 1.0, 1.0)); + // return VertexOutput(a_color, vec4(a_position[0], a_position[1], 1.0, 1.0)); + // return VertexOutput(prim.color,c position); + return VertexOutput(a_color, position); +} + +struct FragOutput { + @location(0) out_color: vec4, +}; + +@fragment +fn fs_main( + @location(0) v_color: vec4, +) -> FragOutput { + return FragOutput(v_color); +} diff --git a/ecadg/src/gfx/triangle.rs b/ecadg/src/gfx/triangle.rs index cc996a7..9f3b731 100644 --- a/ecadg/src/gfx/triangle.rs +++ b/ecadg/src/gfx/triangle.rs @@ -1,3 +1,4 @@ +#![allow(unused)] //! Just a demo triangle use std::num::NonZeroU64; diff --git a/ecadg/src/lib.rs b/ecadg/src/lib.rs index 1d3a83c..deb28b7 100644 --- a/ecadg/src/lib.rs +++ b/ecadg/src/lib.rs @@ -2,6 +2,8 @@ #![allow(clippy::module_name_repetitions)] #![allow(clippy::missing_errors_doc)] #![allow(clippy::missing_panics_doc)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::cast_precision_loss)] mod app; mod backend;