Init
This commit is contained in:
commit
a8421dab3a
|
|
@ -0,0 +1,4 @@
|
|||
/target
|
||||
Cargo.lock
|
||||
*.swp
|
||||
.DS_Store
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "siphon"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
description = "KiCad board data → vector geometry (pure Rust)"
|
||||
license = "Unlicense"
|
||||
|
||||
[dependencies]
|
||||
kiutils_sexpr = "0.1"
|
||||
kiutils_kicad = "0.3"
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
This is free to use, without conditions.
|
||||
|
||||
There is no licence here on purpose. Individuals, students, hobbyists — take what
|
||||
you need, make it yours, don't think twice. You'd flatter me.
|
||||
|
||||
The absence of a licence is deliberate. A licence is a legal surface. Words can be
|
||||
reinterpreted, and corporations employ lawyers whose job is exactly that. Silence is
|
||||
harder to exploit than language. If a company wants to use this, the lack of explicit
|
||||
permission makes it just inconvenient enough to matter.
|
||||
|
||||
This won't change the world. But it shifts the balance, even slightly, away from the
|
||||
system that co-opts open work for closed profit. That's enough for me.
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
use siphon::{render_to_svg, RenderOptions};
|
||||
|
||||
const DEMO: &str = r#"(kicad_pcb (version 20260206) (generator "pcbnew")
|
||||
(layers (0 "F.Cu" signal) (31 "B.Cu" signal))
|
||||
(segment (start 10 10) (end 40 10) (width 0.25) (layer "F.Cu") (uuid "1"))
|
||||
(segment (start 40 10) (end 40 30) (width 0.25) (layer "F.Cu") (uuid "2"))
|
||||
(segment (start 40 30) (end 10 30) (width 0.25) (layer "B.Cu") (uuid "3"))
|
||||
(arc (start 10 30) (mid 5 20) (end 10 10) (width 0.25) (layer "F.Cu") (uuid "4"))
|
||||
(via (at 40 20) (size 0.8) (drill 0.4) (layers "F.Cu" "B.Cu") (uuid "5"))
|
||||
(gr_rect (start 2 2) (end 50 35) (stroke (width 0.1)) (fill none) (layer "Edge.Cuts") (uuid "6"))
|
||||
(footprint "Resistor_SMD:R_0603_1608Metric"
|
||||
(layer "F.Cu")
|
||||
(at 25 18 0)
|
||||
(uuid "fp1")
|
||||
(fp_text reference "R1" (at 0 -1.3 0) (layer "F.Silkscreen")
|
||||
(effects (font (size 0.8 0.8) (thickness 0.12))))
|
||||
(fp_text value "10k" (at 0 1.3 0) (layer "F.Fab")
|
||||
(effects (font (size 0.6 0.6) (thickness 0.1))))
|
||||
(fp_line (start -0.8 -0.5) (end 0.8 -0.5) (stroke (width 0.1)) (layer "F.Silkscreen"))
|
||||
(fp_line (start -0.8 0.5) (end 0.8 0.5) (stroke (width 0.1)) (layer "F.Silkscreen"))
|
||||
(fp_rect (start -1.4 -0.7) (end 1.4 0.7) (stroke (width 0.05)) (fill none) (layer "F.Courtyard"))
|
||||
(pad "1" smd roundrect (at -0.8 0 0) (size 0.9 0.9)
|
||||
(layers "F.Cu" "F.Paste" "F.Mask") (roundrect_rratio 0.25))
|
||||
(pad "2" smd roundrect (at 0.8 0 0) (size 0.9 0.9)
|
||||
(layers "F.Cu" "F.Paste" "F.Mask") (roundrect_rratio 0.25))
|
||||
)
|
||||
(gr_text "BOARD TITLE"
|
||||
(at 26 4 0)
|
||||
(layer "F.Silkscreen")
|
||||
(effects (font (size 1.5 1.5) (thickness 0.2)))
|
||||
(uuid "title"))
|
||||
(footprint "Connector:PinHeader_1x02"
|
||||
(layer "F.Cu")
|
||||
(at 15 20 90)
|
||||
(uuid "fp2")
|
||||
(fp_circle (center 0 0) (end 0.5 0) (stroke (width 0.1)) (fill none) (layer "F.Silkscreen"))
|
||||
(pad "1" thru_hole circle (at 0 0 0) (size 1.7 1.7) (drill 1.0)
|
||||
(layers "F.Cu" "B.Cu" "F.Mask" "B.Mask"))
|
||||
(pad "2" thru_hole rect (at 0 2.54 0) (size 1.7 1.7) (drill 1.0)
|
||||
(layers "F.Cu" "B.Cu" "F.Mask" "B.Mask"))
|
||||
)
|
||||
)"#;
|
||||
|
||||
fn main() {
|
||||
let svg = render_to_svg(DEMO, &RenderOptions::default()).expect("render");
|
||||
std::fs::write("/tmp/siphon_demo.svg", &svg).expect("write");
|
||||
println!("wrote /tmp/siphon_demo.svg ({} bytes)", svg.len());
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
use siphon::{LibraryResolver, RenderOptions, SvgSink};
|
||||
use siphon::geom::BBox;
|
||||
use siphon::render::render as render_board;
|
||||
|
||||
fn main() {
|
||||
let resolver = LibraryResolver::from_global().expect("load global fp-lib-table");
|
||||
println!("loaded {} library entries", resolver.table.entries.len());
|
||||
|
||||
let lib_id = std::env::args().nth(1).unwrap_or_else(|| "Resistor_SMD:R_0603_1608Metric".into());
|
||||
println!("resolving {lib_id}");
|
||||
|
||||
let fp = resolver.load_footprint(&lib_id).expect("load footprint");
|
||||
println!("footprint with {} primitives, {} pads", fp.primitives.len(), fp.pads.len());
|
||||
|
||||
let board = siphon::Board { items: vec![siphon::Item::Footprint(fp)] };
|
||||
let mut sink = SvgSink::new(BBox::empty());
|
||||
let bb = render_board(&board, &mut sink, &RenderOptions::default());
|
||||
sink.bounds = bb;
|
||||
let out_path = format!("/tmp/siphon_lib_{}.svg", lib_id.replace(':', "_").replace('/', "_"));
|
||||
std::fs::write(&out_path, sink.finish()).expect("write");
|
||||
println!("wrote {out_path}");
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
use std::path::PathBuf;
|
||||
use std::time::Instant;
|
||||
|
||||
use siphon::geom::BBox;
|
||||
use siphon::render::render as render_board;
|
||||
use siphon::{render_to_svg, LibraryResolver, RenderOptions, SvgSink};
|
||||
|
||||
fn main() {
|
||||
let path: PathBuf = std::env::args().nth(1)
|
||||
.expect("usage: render_board <path/to/board.kicad_pcb> [out.svg]")
|
||||
.into();
|
||||
let out: PathBuf = std::env::args().nth(2)
|
||||
.map(Into::into)
|
||||
.unwrap_or_else(|| {
|
||||
let stem = path.file_stem().unwrap_or_default().to_string_lossy().to_string();
|
||||
PathBuf::from(format!("/tmp/siphon_{stem}.svg"))
|
||||
});
|
||||
|
||||
let source = std::fs::read_to_string(&path).expect("read board");
|
||||
let project_dir = path.parent().unwrap_or_else(|| std::path::Path::new("."));
|
||||
let _ = LibraryResolver::from_global().and_then(|r| r.with_project(project_dir));
|
||||
|
||||
let t0 = Instant::now();
|
||||
let svg = render_to_svg(&source, &RenderOptions::default()).expect("render");
|
||||
let dt = t0.elapsed();
|
||||
|
||||
let board = siphon::parse::board::parse(&source).expect("parse");
|
||||
let stats = count_items(&board);
|
||||
println!(
|
||||
"render {:?} → {} bytes in {:?}\n items: {} tracks, {} arcs, {} vias, {} gr_*, {} footprints, {} zones ({} fills), {} dims, {} texts",
|
||||
path.file_name().unwrap(),
|
||||
svg.len(),
|
||||
dt,
|
||||
stats.tracks, stats.arcs, stats.vias, stats.gr, stats.footprints,
|
||||
stats.zones, stats.zone_fills, stats.dims, stats.texts,
|
||||
);
|
||||
|
||||
std::fs::write(&out, svg).expect("write");
|
||||
println!("wrote {}", out.display());
|
||||
|
||||
let mut sink = SvgSink::new(BBox::empty());
|
||||
let bb = render_board(&board, &mut sink, &RenderOptions::default());
|
||||
sink.bounds = bb;
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Stats {
|
||||
tracks: usize,
|
||||
arcs: usize,
|
||||
vias: usize,
|
||||
gr: usize,
|
||||
footprints: usize,
|
||||
zones: usize,
|
||||
zone_fills: usize,
|
||||
dims: usize,
|
||||
texts: usize,
|
||||
}
|
||||
|
||||
fn count_items(board: &siphon::Board) -> Stats {
|
||||
use siphon::Item;
|
||||
let mut s = Stats::default();
|
||||
for item in &board.items {
|
||||
match item {
|
||||
Item::Segment(_) => s.tracks += 1,
|
||||
Item::Arc(_) => s.arcs += 1,
|
||||
Item::Via(_) => s.vias += 1,
|
||||
Item::GrLine(_) | Item::GrArc(_) | Item::GrRect(_) | Item::GrCircle(_) | Item::GrPoly(_) => s.gr += 1,
|
||||
Item::GrText(_) => s.texts += 1,
|
||||
Item::Footprint(f) => {
|
||||
s.footprints += 1;
|
||||
s.texts += f.texts.len();
|
||||
}
|
||||
Item::Zone(z) => {
|
||||
s.zones += 1;
|
||||
s.zone_fills += z.filled_polygons.len();
|
||||
}
|
||||
Item::Dimension(_) => s.dims += 1,
|
||||
}
|
||||
}
|
||||
s
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
use crate::geom::Point;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Arc {
|
||||
pub start: Point,
|
||||
pub mid: Point,
|
||||
pub end: Point,
|
||||
}
|
||||
|
||||
impl Arc {
|
||||
pub const fn new(start: Point, mid: Point, end: Point) -> Self {
|
||||
Self { start, mid, end }
|
||||
}
|
||||
|
||||
pub fn centre_and_radius(&self) -> Option<(Point, f64)> {
|
||||
let (ax, ay) = (self.start.x, self.start.y);
|
||||
let (bx, by) = (self.mid.x, self.mid.y);
|
||||
let (cx, cy) = (self.end.x, self.end.y);
|
||||
let d = 2.0 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
|
||||
if d.abs() < f64::EPSILON {
|
||||
return None;
|
||||
}
|
||||
let ax2_ay2 = ax * ax + ay * ay;
|
||||
let bx2_by2 = bx * bx + by * by;
|
||||
let cx2_cy2 = cx * cx + cy * cy;
|
||||
let ux = (ax2_ay2 * (by - cy) + bx2_by2 * (cy - ay) + cx2_cy2 * (ay - by)) / d;
|
||||
let uy = (ax2_ay2 * (cx - bx) + bx2_by2 * (ax - cx) + cx2_cy2 * (bx - ax)) / d;
|
||||
let centre = Point::new(ux, uy);
|
||||
let r = centre.distance_to(self.start);
|
||||
Some((centre, r))
|
||||
}
|
||||
|
||||
/// Sweep direction from start → end through mid, as +1 (CCW) or -1 (CW).
|
||||
pub fn sweep_sign(&self) -> f64 {
|
||||
let (ax, ay) = (self.start.x, self.start.y);
|
||||
let (bx, by) = (self.mid.x, self.mid.y);
|
||||
let (cx, cy) = (self.end.x, self.end.y);
|
||||
let cross = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
|
||||
if cross >= 0.0 { 1.0 } else { -1.0 }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
use crate::geom::Point;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct BBox {
|
||||
pub min_x: f64,
|
||||
pub min_y: f64,
|
||||
pub max_x: f64,
|
||||
pub max_y: f64,
|
||||
}
|
||||
|
||||
impl BBox {
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
min_x: f64::INFINITY,
|
||||
min_y: f64::INFINITY,
|
||||
max_x: f64::NEG_INFINITY,
|
||||
max_y: f64::NEG_INFINITY,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.min_x > self.max_x || self.min_y > self.max_y
|
||||
}
|
||||
|
||||
pub fn expand_point(&mut self, p: Point) {
|
||||
if p.x < self.min_x { self.min_x = p.x; }
|
||||
if p.y < self.min_y { self.min_y = p.y; }
|
||||
if p.x > self.max_x { self.max_x = p.x; }
|
||||
if p.y > self.max_y { self.max_y = p.y; }
|
||||
}
|
||||
|
||||
pub fn expand(&mut self, other: BBox) {
|
||||
if other.is_empty() { return; }
|
||||
if other.min_x < self.min_x { self.min_x = other.min_x; }
|
||||
if other.min_y < self.min_y { self.min_y = other.min_y; }
|
||||
if other.max_x > self.max_x { self.max_x = other.max_x; }
|
||||
if other.max_y > self.max_y { self.max_y = other.max_y; }
|
||||
}
|
||||
|
||||
pub fn inflate(&mut self, by: f64) {
|
||||
self.min_x -= by;
|
||||
self.min_y -= by;
|
||||
self.max_x += by;
|
||||
self.max_y += by;
|
||||
}
|
||||
|
||||
pub fn width(&self) -> f64 { self.max_x - self.min_x }
|
||||
pub fn height(&self) -> f64 { self.max_y - self.min_y }
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
pub mod point;
|
||||
pub mod arc;
|
||||
pub mod bbox;
|
||||
pub mod transform;
|
||||
|
||||
pub use arc::Arc;
|
||||
pub use bbox::BBox;
|
||||
pub use point::Point;
|
||||
pub use transform::Transform;
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Point {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
}
|
||||
|
||||
impl Point {
|
||||
pub const fn new(x: f64, y: f64) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
|
||||
pub fn distance_to(&self, other: Point) -> f64 {
|
||||
let dx = self.x - other.x;
|
||||
let dy = self.y - other.y;
|
||||
(dx * dx + dy * dy).sqrt()
|
||||
}
|
||||
|
||||
pub fn midpoint(&self, other: Point) -> Point {
|
||||
Point::new((self.x + other.x) * 0.5, (self.y + other.y) * 0.5)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
use crate::geom::Point;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Transform {
|
||||
pub translate: Point,
|
||||
pub rotation_rad: f64,
|
||||
pub mirror_x: bool,
|
||||
}
|
||||
|
||||
impl Transform {
|
||||
pub const IDENTITY: Transform = Transform {
|
||||
translate: Point::new(0.0, 0.0),
|
||||
rotation_rad: 0.0,
|
||||
mirror_x: false,
|
||||
};
|
||||
|
||||
pub fn at(p: Point, rotation_deg: f64) -> Self {
|
||||
Self {
|
||||
translate: p,
|
||||
rotation_rad: rotation_deg.to_radians(),
|
||||
mirror_x: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply(&self, local: Point) -> Point {
|
||||
let mut x = local.x;
|
||||
if self.mirror_x {
|
||||
x = -x;
|
||||
}
|
||||
let (s, c) = self.rotation_rad.sin_cos();
|
||||
Point::new(
|
||||
self.translate.x + c * x - s * local.y,
|
||||
self.translate.y + s * x + c * local.y,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Transform {
|
||||
fn default() -> Self { Self::IDENTITY }
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct LayerName(pub String);
|
||||
|
||||
impl LayerName {
|
||||
pub fn new<S: Into<String>>(s: S) -> Self { Self(s.into()) }
|
||||
pub fn as_str(&self) -> &str { &self.0 }
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct Rgba {
|
||||
pub r: u8,
|
||||
pub g: u8,
|
||||
pub b: u8,
|
||||
pub a: u8,
|
||||
}
|
||||
|
||||
impl Rgba {
|
||||
pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self { Self { r, g, b, a } }
|
||||
pub const fn opaque(r: u8, g: u8, b: u8) -> Self { Self::new(r, g, b, 255) }
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Style {
|
||||
pub stroke: Option<Rgba>,
|
||||
pub stroke_width_mm: f64,
|
||||
pub fill: Option<Rgba>,
|
||||
}
|
||||
|
||||
impl Style {
|
||||
pub const fn stroke_only(colour: Rgba, width_mm: f64) -> Self {
|
||||
Self { stroke: Some(colour), stroke_width_mm: width_mm, fill: None }
|
||||
}
|
||||
pub const fn fill_only(colour: Rgba) -> Self {
|
||||
Self { stroke: None, stroke_width_mm: 0.0, fill: Some(colour) }
|
||||
}
|
||||
pub const fn fill_and_stroke(fill: Rgba, stroke: Rgba, width_mm: f64) -> Self {
|
||||
Self { stroke: Some(stroke), stroke_width_mm: width_mm, fill: Some(fill) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
pub mod geom;
|
||||
pub mod layer;
|
||||
pub mod library;
|
||||
pub mod parse;
|
||||
pub mod render;
|
||||
pub mod sink;
|
||||
|
||||
pub use layer::{LayerName, Rgba, Style};
|
||||
pub use library::{FpLibTable, LibError, LibraryResolver};
|
||||
pub use parse::{Board, Item};
|
||||
pub use render::{render, Palette, RenderOptions};
|
||||
pub use sink::{Sink, SvgSink};
|
||||
|
||||
pub fn render_to_svg(source: &str, opts: &RenderOptions) -> Result<String, parse::board::BoardError> {
|
||||
let board = parse::board::parse(source)?;
|
||||
let mut sink = SvgSink::new(geom::BBox::empty());
|
||||
let bb = render::render(&board, &mut sink, opts);
|
||||
sink.bounds = bb;
|
||||
Ok(sink.finish())
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
pub mod table;
|
||||
pub mod resolver;
|
||||
|
||||
pub use resolver::{LibraryResolver, LibError};
|
||||
pub use table::{FpLibTable, LibEntry};
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use kiutils_sexpr::parse_one;
|
||||
|
||||
use crate::library::table::{FpLibTable, TableError};
|
||||
use crate::parse::footprint::{parse_footprint, Footprint};
|
||||
use crate::parse::sexpr::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LibError {
|
||||
UnknownNickname(String),
|
||||
MissingColon(String),
|
||||
ResolveFailed { uri: String, reason: String },
|
||||
ReadFailed(std::io::Error),
|
||||
Parse(kiutils_sexpr::ParseError),
|
||||
MalformedFootprint,
|
||||
Table(TableError),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for LibError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::UnknownNickname(n) => write!(f, "unknown lib nickname: {n}"),
|
||||
Self::MissingColon(s) => write!(f, "lib id must be 'Nickname:Name', got: {s}"),
|
||||
Self::ResolveFailed { uri, reason } => write!(f, "cannot resolve uri {uri}: {reason}"),
|
||||
Self::ReadFailed(e) => write!(f, "read: {e}"),
|
||||
Self::Parse(e) => write!(f, "parse: {e}"),
|
||||
Self::MalformedFootprint => write!(f, "malformed .kicad_mod"),
|
||||
Self::Table(e) => write!(f, "table: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for LibError {}
|
||||
|
||||
impl From<TableError> for LibError {
|
||||
fn from(e: TableError) -> Self { Self::Table(e) }
|
||||
}
|
||||
|
||||
pub struct LibraryResolver {
|
||||
pub table: FpLibTable,
|
||||
pub env: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl LibraryResolver {
|
||||
pub fn new(table: FpLibTable) -> Self {
|
||||
Self { table, env: default_env() }
|
||||
}
|
||||
|
||||
pub fn from_global() -> Result<Self, LibError> {
|
||||
let path = crate::library::table::global_table_path()
|
||||
.ok_or_else(|| LibError::Table(TableError::Malformed))?;
|
||||
let mut resolver = Self::new(FpLibTable::load(&path)?);
|
||||
resolver.flatten_nested_tables();
|
||||
Ok(resolver)
|
||||
}
|
||||
|
||||
pub fn with_project(mut self, project_dir: &Path) -> Result<Self, LibError> {
|
||||
if let Some(p) = crate::library::table::project_table_path(project_dir) {
|
||||
let mut project = FpLibTable::load(&p)?;
|
||||
let nested: Vec<_> = project.entries.iter()
|
||||
.filter(|e| e.lib_type == "Table")
|
||||
.cloned()
|
||||
.collect();
|
||||
for entry in nested {
|
||||
flatten_one(&mut project, &entry, &self.env);
|
||||
}
|
||||
self.table = project.merge(self.table);
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn flatten_nested_tables(&mut self) {
|
||||
let nested: Vec<_> = self.table.entries.iter()
|
||||
.filter(|e| e.lib_type == "Table")
|
||||
.cloned()
|
||||
.collect();
|
||||
for entry in nested {
|
||||
flatten_one(&mut self.table, &entry, &self.env);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_env(&mut self, key: impl Into<String>, value: impl Into<String>) {
|
||||
let key = key.into();
|
||||
self.env.retain(|(k, _)| k != &key);
|
||||
self.env.push((key, value.into()));
|
||||
}
|
||||
|
||||
pub fn resolve_dir(&self, nickname: &str) -> Result<PathBuf, LibError> {
|
||||
let entry = self.table.find(nickname)
|
||||
.ok_or_else(|| LibError::UnknownNickname(nickname.to_string()))?;
|
||||
let expanded = expand(&entry.uri, &self.env)
|
||||
.ok_or_else(|| LibError::ResolveFailed {
|
||||
uri: entry.uri.clone(),
|
||||
reason: "unresolved env var".into(),
|
||||
})?;
|
||||
Ok(PathBuf::from(expanded))
|
||||
}
|
||||
|
||||
pub fn load_footprint(&self, lib_id: &str) -> Result<Footprint, LibError> {
|
||||
let (nick, name) = split_id(lib_id)?;
|
||||
let dir = self.resolve_dir(nick)?;
|
||||
let path = dir.join(format!("{name}.kicad_mod"));
|
||||
let source = fs::read_to_string(&path).map_err(LibError::ReadFailed)?;
|
||||
parse_kicad_mod(&source)
|
||||
}
|
||||
}
|
||||
|
||||
fn split_id(lib_id: &str) -> Result<(&str, &str), LibError> {
|
||||
let (a, b) = lib_id.split_once(':')
|
||||
.ok_or_else(|| LibError::MissingColon(lib_id.to_string()))?;
|
||||
Ok((a, b))
|
||||
}
|
||||
|
||||
pub fn parse_kicad_mod(source: &str) -> Result<Footprint, LibError> {
|
||||
let doc = parse_one(source).map_err(LibError::Parse)?;
|
||||
let root = doc.nodes.first().ok_or(LibError::MalformedFootprint)?;
|
||||
if head(root) != Some("footprint") && head(root) != Some("module") {
|
||||
return Err(LibError::MalformedFootprint);
|
||||
}
|
||||
parse_footprint(root).ok_or(LibError::MalformedFootprint)
|
||||
}
|
||||
|
||||
fn expand(src: &str, env: &[(String, String)]) -> Option<String> {
|
||||
let mut out = String::with_capacity(src.len());
|
||||
let mut i = 0;
|
||||
let bytes = src.as_bytes();
|
||||
while i < bytes.len() {
|
||||
if bytes[i] == b'$' && i + 1 < bytes.len() && bytes[i + 1] == b'{' {
|
||||
let close = src[i + 2..].find('}')?;
|
||||
let key = &src[i + 2..i + 2 + close];
|
||||
let value = env_lookup(key, env)?;
|
||||
out.push_str(&value);
|
||||
i += 2 + close + 1;
|
||||
} else {
|
||||
out.push(bytes[i] as char);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
fn env_lookup(key: &str, env: &[(String, String)]) -> Option<String> {
|
||||
if let Some((_, v)) = env.iter().find(|(k, _)| k == key) {
|
||||
return Some(v.clone());
|
||||
}
|
||||
std::env::var(key).ok()
|
||||
}
|
||||
|
||||
fn flatten_one(into: &mut FpLibTable, table_entry: &crate::library::table::LibEntry, env: &[(String, String)]) {
|
||||
let Some(resolved) = expand(&table_entry.uri, env) else { return; };
|
||||
let path = Path::new(&resolved);
|
||||
if !path.exists() { return; }
|
||||
let Ok(inner) = FpLibTable::load(path) else { return; };
|
||||
for entry in inner.entries {
|
||||
if entry.lib_type == "Table" { continue; }
|
||||
if into.find(&entry.name).is_none() {
|
||||
into.entries.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_env() -> Vec<(String, String)> {
|
||||
let mut out = Vec::new();
|
||||
let add = |out: &mut Vec<(String, String)>, k: &str, v: String| {
|
||||
if Path::new(&v).exists() {
|
||||
out.push((k.to_string(), v));
|
||||
}
|
||||
};
|
||||
let home = std::env::var("HOME").unwrap_or_default();
|
||||
add(&mut out, "KICAD10_FOOTPRINT_DIR",
|
||||
"/Applications/KiCad/KiCad.app/Contents/SharedSupport/footprints".into());
|
||||
add(&mut out, "KICAD10_SYMBOL_DIR",
|
||||
"/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols".into());
|
||||
add(&mut out, "KICAD10_3DMODEL_DIR",
|
||||
"/Applications/KiCad/KiCad.app/Contents/SharedSupport/3dmodels".into());
|
||||
add(&mut out, "KICAD10_TEMPLATE_DIR",
|
||||
"/Applications/KiCad/KiCad.app/Contents/SharedSupport/template".into());
|
||||
add(&mut out, "KICAD10_3RD_PARTY", format!("{home}/Documents/KiCad/10.0/3rdparty"));
|
||||
out
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use kiutils_sexpr::parse_one;
|
||||
|
||||
use crate::parse::sexpr::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LibEntry {
|
||||
pub name: String,
|
||||
pub lib_type: String,
|
||||
pub uri: String,
|
||||
pub descr: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct FpLibTable {
|
||||
pub entries: Vec<LibEntry>,
|
||||
}
|
||||
|
||||
impl FpLibTable {
|
||||
pub fn parse(source: &str) -> Result<Self, TableError> {
|
||||
let doc = parse_one(source).map_err(TableError::Parse)?;
|
||||
let root = doc.nodes.first().ok_or(TableError::Malformed)?;
|
||||
if head(root) != Some("fp_lib_table") {
|
||||
return Err(TableError::Malformed);
|
||||
}
|
||||
let mut entries = Vec::new();
|
||||
for lib in find_lists(root, "lib") {
|
||||
let name = find_list(lib, "name").and_then(|n| arg_str(n, 0)).unwrap_or("").to_string();
|
||||
let lib_type = find_list(lib, "type").and_then(|n| arg_str(n, 0)).unwrap_or("").to_string();
|
||||
let uri = find_list(lib, "uri").and_then(|n| arg_str(n, 0)).unwrap_or("").to_string();
|
||||
let descr = find_list(lib, "descr").and_then(|n| arg_str(n, 0)).unwrap_or("").to_string();
|
||||
if name.is_empty() || uri.is_empty() { continue; }
|
||||
entries.push(LibEntry { name, lib_type, uri, descr });
|
||||
}
|
||||
Ok(FpLibTable { entries })
|
||||
}
|
||||
|
||||
pub fn load(path: &Path) -> Result<Self, TableError> {
|
||||
let s = fs::read_to_string(path).map_err(TableError::Io)?;
|
||||
Self::parse(&s)
|
||||
}
|
||||
|
||||
pub fn find(&self, nickname: &str) -> Option<&LibEntry> {
|
||||
self.entries.iter().find(|e| e.name == nickname)
|
||||
}
|
||||
|
||||
pub fn merge(mut self, other: FpLibTable) -> FpLibTable {
|
||||
for entry in other.entries {
|
||||
if self.find(&entry.name).is_none() {
|
||||
self.entries.push(entry);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn global_table_path() -> Option<PathBuf> {
|
||||
if let Ok(p) = std::env::var("KICAD10_CONFIG_DIR") {
|
||||
let pb = PathBuf::from(p).join("fp-lib-table");
|
||||
if pb.exists() { return Some(pb); }
|
||||
}
|
||||
let home = std::env::var("HOME").ok()?;
|
||||
let candidates = [
|
||||
format!("{home}/Library/Preferences/kicad/10.0/fp-lib-table"),
|
||||
format!("{home}/Library/Preferences/kicad/9.0/fp-lib-table"),
|
||||
format!("{home}/Library/Preferences/kicad/8.0/fp-lib-table"),
|
||||
format!("{home}/.config/kicad/10.0/fp-lib-table"),
|
||||
format!("{home}/.config/kicad/9.0/fp-lib-table"),
|
||||
];
|
||||
for c in &candidates {
|
||||
let p = PathBuf::from(c);
|
||||
if p.exists() { return Some(p); }
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn project_table_path(project_dir: &Path) -> Option<PathBuf> {
|
||||
let p = project_dir.join("fp-lib-table");
|
||||
if p.exists() { Some(p) } else { None }
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TableError {
|
||||
Parse(kiutils_sexpr::ParseError),
|
||||
Io(std::io::Error),
|
||||
Malformed,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TableError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Parse(e) => write!(f, "parse: {e}"),
|
||||
Self::Io(e) => write!(f, "io: {e}"),
|
||||
Self::Malformed => write!(f, "malformed fp-lib-table"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for TableError {}
|
||||
|
|
@ -0,0 +1,316 @@
|
|||
use kiutils_sexpr::{parse_one, Node, ParseError};
|
||||
|
||||
use crate::geom::Point;
|
||||
use crate::parse::dimension::{parse_dimension, Dimension};
|
||||
use crate::parse::footprint::{parse_footprint, Footprint};
|
||||
use crate::parse::sexpr::*;
|
||||
use crate::parse::text::{parse_gr_text, GrText};
|
||||
use crate::parse::zone::{parse_zone, Zone};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum BoardError {
|
||||
Parse(ParseError),
|
||||
NotABoard,
|
||||
Malformed(&'static str),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for BoardError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Parse(e) => write!(f, "parse: {e}"),
|
||||
Self::NotABoard => write!(f, "not a kicad_pcb s-expression"),
|
||||
Self::Malformed(s) => write!(f, "malformed board: {s}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for BoardError {}
|
||||
|
||||
impl From<ParseError> for BoardError {
|
||||
fn from(e: ParseError) -> Self { Self::Parse(e) }
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Board {
|
||||
pub items: Vec<Item>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Item {
|
||||
Segment(Segment),
|
||||
Arc(ArcItem),
|
||||
Via(Via),
|
||||
GrLine(GrLine),
|
||||
GrArc(GrArc),
|
||||
GrRect(GrRect),
|
||||
GrCircle(GrCircle),
|
||||
GrPoly(GrPoly),
|
||||
GrText(GrText),
|
||||
Footprint(Footprint),
|
||||
Zone(Zone),
|
||||
Dimension(Dimension),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Segment {
|
||||
pub start: Point,
|
||||
pub end: Point,
|
||||
pub width_mm: f64,
|
||||
pub layer: String,
|
||||
pub uuid: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ArcItem {
|
||||
pub start: Point,
|
||||
pub mid: Point,
|
||||
pub end: Point,
|
||||
pub width_mm: f64,
|
||||
pub layer: String,
|
||||
pub uuid: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Via {
|
||||
pub at: Point,
|
||||
pub size_mm: f64,
|
||||
pub drill_mm: f64,
|
||||
pub layers: Vec<String>,
|
||||
pub uuid: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GrLine {
|
||||
pub start: Point,
|
||||
pub end: Point,
|
||||
pub width_mm: f64,
|
||||
pub layer: String,
|
||||
pub uuid: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GrArc {
|
||||
pub start: Point,
|
||||
pub mid: Point,
|
||||
pub end: Point,
|
||||
pub width_mm: f64,
|
||||
pub layer: String,
|
||||
pub uuid: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GrRect {
|
||||
pub start: Point,
|
||||
pub end: Point,
|
||||
pub width_mm: f64,
|
||||
pub fill: bool,
|
||||
pub layer: String,
|
||||
pub uuid: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GrCircle {
|
||||
pub centre: Point,
|
||||
pub end: Point,
|
||||
pub width_mm: f64,
|
||||
pub fill: bool,
|
||||
pub layer: String,
|
||||
pub uuid: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GrPoly {
|
||||
pub points: Vec<Point>,
|
||||
pub width_mm: f64,
|
||||
pub fill: bool,
|
||||
pub layer: String,
|
||||
pub uuid: Option<String>,
|
||||
}
|
||||
|
||||
pub fn parse(source: &str) -> Result<Board, BoardError> {
|
||||
let doc = parse_one(source)?;
|
||||
let root = doc.nodes.first().ok_or(BoardError::NotABoard)?;
|
||||
if head(root) != Some("kicad_pcb") {
|
||||
return Err(BoardError::NotABoard);
|
||||
}
|
||||
let mut items = Vec::new();
|
||||
for child in children(root).iter().skip(1) {
|
||||
if let Some(item) = parse_item(child) {
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
Ok(Board { items })
|
||||
}
|
||||
|
||||
fn parse_item(node: &Node) -> Option<Item> {
|
||||
match head(node)? {
|
||||
"segment" => parse_segment(node).map(Item::Segment),
|
||||
"arc" => parse_arc(node).map(Item::Arc),
|
||||
"via" => parse_via(node).map(Item::Via),
|
||||
"gr_line" => parse_gr_line(node).map(Item::GrLine),
|
||||
"gr_arc" => parse_gr_arc(node).map(Item::GrArc),
|
||||
"gr_rect" => parse_gr_rect(node).map(Item::GrRect),
|
||||
"gr_circle" => parse_gr_circle(node).map(Item::GrCircle),
|
||||
"gr_poly" => parse_gr_poly(node).map(Item::GrPoly),
|
||||
"gr_text" => parse_gr_text(node).map(Item::GrText),
|
||||
"footprint" => parse_footprint(node).map(Item::Footprint),
|
||||
"zone" => parse_zone(node).map(Item::Zone),
|
||||
"dimension" => parse_dimension(node).map(Item::Dimension),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_segment(node: &Node) -> Option<Segment> {
|
||||
let (sx, sy) = xy_from(node, "start")?;
|
||||
let (ex, ey) = xy_from(node, "end")?;
|
||||
let width = find_list(node, "width").and_then(|n| arg_f64(n, 0)).unwrap_or(0.2);
|
||||
let layer = layer_str(node)?;
|
||||
Some(Segment {
|
||||
start: Point::new(sx, sy),
|
||||
end: Point::new(ex, ey),
|
||||
width_mm: width,
|
||||
layer,
|
||||
uuid: uuid_str(node),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_arc(node: &Node) -> Option<ArcItem> {
|
||||
let (sx, sy) = xy_from(node, "start")?;
|
||||
let (mx, my) = xy_from(node, "mid")?;
|
||||
let (ex, ey) = xy_from(node, "end")?;
|
||||
let width = find_list(node, "width").and_then(|n| arg_f64(n, 0)).unwrap_or(0.2);
|
||||
let layer = layer_str(node)?;
|
||||
Some(ArcItem {
|
||||
start: Point::new(sx, sy),
|
||||
mid: Point::new(mx, my),
|
||||
end: Point::new(ex, ey),
|
||||
width_mm: width,
|
||||
layer,
|
||||
uuid: uuid_str(node),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_via(node: &Node) -> Option<Via> {
|
||||
let (ax, ay) = xy_from(node, "at")?;
|
||||
let size = find_list(node, "size").and_then(|n| arg_f64(n, 0)).unwrap_or(0.8);
|
||||
let drill = find_list(node, "drill").and_then(|n| arg_f64(n, 0)).unwrap_or(0.4);
|
||||
let layers = find_list(node, "layers")
|
||||
.map(|l| {
|
||||
children(l)
|
||||
.iter()
|
||||
.skip(1)
|
||||
.filter_map(symbol)
|
||||
.map(str::to_string)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Some(Via {
|
||||
at: Point::new(ax, ay),
|
||||
size_mm: size,
|
||||
drill_mm: drill,
|
||||
layers,
|
||||
uuid: uuid_str(node),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_gr_line(node: &Node) -> Option<GrLine> {
|
||||
let (sx, sy) = xy_from(node, "start")?;
|
||||
let (ex, ey) = xy_from(node, "end")?;
|
||||
let layer = layer_str(node)?;
|
||||
Some(GrLine {
|
||||
start: Point::new(sx, sy),
|
||||
end: Point::new(ex, ey),
|
||||
width_mm: stroke_width(node).unwrap_or(0.1),
|
||||
layer,
|
||||
uuid: uuid_str(node),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_gr_arc(node: &Node) -> Option<GrArc> {
|
||||
let (sx, sy) = xy_from(node, "start")?;
|
||||
let (mx, my) = xy_from(node, "mid")?;
|
||||
let (ex, ey) = xy_from(node, "end")?;
|
||||
let layer = layer_str(node)?;
|
||||
Some(GrArc {
|
||||
start: Point::new(sx, sy),
|
||||
mid: Point::new(mx, my),
|
||||
end: Point::new(ex, ey),
|
||||
width_mm: stroke_width(node).unwrap_or(0.1),
|
||||
layer,
|
||||
uuid: uuid_str(node),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_gr_rect(node: &Node) -> Option<GrRect> {
|
||||
let (sx, sy) = xy_from(node, "start")?;
|
||||
let (ex, ey) = xy_from(node, "end")?;
|
||||
let layer = layer_str(node)?;
|
||||
Some(GrRect {
|
||||
start: Point::new(sx, sy),
|
||||
end: Point::new(ex, ey),
|
||||
width_mm: stroke_width(node).unwrap_or(0.1),
|
||||
fill: has_fill_solid(node),
|
||||
layer,
|
||||
uuid: uuid_str(node),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_gr_circle(node: &Node) -> Option<GrCircle> {
|
||||
let (cx, cy) = xy_from(node, "center")
|
||||
.or_else(|| xy_from(node, "centre"))?;
|
||||
let (ex, ey) = xy_from(node, "end")?;
|
||||
let layer = layer_str(node)?;
|
||||
Some(GrCircle {
|
||||
centre: Point::new(cx, cy),
|
||||
end: Point::new(ex, ey),
|
||||
width_mm: stroke_width(node).unwrap_or(0.1),
|
||||
fill: has_fill_solid(node),
|
||||
layer,
|
||||
uuid: uuid_str(node),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_gr_poly(node: &Node) -> Option<GrPoly> {
|
||||
let pts = find_list(node, "pts")?;
|
||||
let mut points = Vec::new();
|
||||
for xy in find_lists(pts, "xy") {
|
||||
if let (Some(x), Some(y)) = (arg_f64(xy, 0), arg_f64(xy, 1)) {
|
||||
points.push(Point::new(x, y));
|
||||
}
|
||||
}
|
||||
if points.is_empty() { return None; }
|
||||
let layer = layer_str(node)?;
|
||||
Some(GrPoly {
|
||||
points,
|
||||
width_mm: stroke_width(node).unwrap_or(0.1),
|
||||
fill: has_fill_solid(node),
|
||||
layer,
|
||||
uuid: uuid_str(node),
|
||||
})
|
||||
}
|
||||
|
||||
fn stroke_width(node: &Node) -> Option<f64> {
|
||||
if let Some(stroke) = find_list(node, "stroke") {
|
||||
if let Some(w) = find_list(stroke, "width") {
|
||||
return arg_f64(w, 0);
|
||||
}
|
||||
}
|
||||
find_list(node, "width").and_then(|n| arg_f64(n, 0))
|
||||
}
|
||||
|
||||
fn layer_str(node: &Node) -> Option<String> {
|
||||
find_list(node, "layer").and_then(|n| arg_str(n, 0)).map(str::to_string)
|
||||
}
|
||||
|
||||
fn uuid_str(node: &Node) -> Option<String> {
|
||||
find_list(node, "uuid").and_then(|n| arg_str(n, 0)).map(str::to_string)
|
||||
}
|
||||
|
||||
fn has_fill_solid(node: &Node) -> bool {
|
||||
if let Some(fill) = find_list(node, "fill") {
|
||||
if let Some(v) = arg_str(fill, 0) {
|
||||
return matches!(v, "solid" | "yes");
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
use kiutils_sexpr::Node;
|
||||
|
||||
use crate::geom::Point;
|
||||
use crate::parse::sexpr::*;
|
||||
use crate::parse::text::{parse_gr_text, GrText};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DimensionKind {
|
||||
Aligned,
|
||||
Orthogonal,
|
||||
Leader,
|
||||
Center,
|
||||
Radial,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Dimension {
|
||||
pub kind: DimensionKind,
|
||||
pub layer: String,
|
||||
pub uuid: Option<String>,
|
||||
pub points: Vec<Point>,
|
||||
pub height: f64,
|
||||
pub arrow_length_mm: f64,
|
||||
pub thickness_mm: f64,
|
||||
pub extension_height: f64,
|
||||
pub extension_offset: f64,
|
||||
pub text: Option<GrText>,
|
||||
}
|
||||
|
||||
pub fn parse_dimension(node: &Node) -> Option<Dimension> {
|
||||
if head(node) != Some("dimension") { return None; }
|
||||
|
||||
let kind_tok = find_list(node, "type")
|
||||
.and_then(|n| arg_str(n, 0))
|
||||
.unwrap_or("aligned");
|
||||
let kind = match kind_tok {
|
||||
"aligned" => DimensionKind::Aligned,
|
||||
"orthogonal" => DimensionKind::Orthogonal,
|
||||
"leader" => DimensionKind::Leader,
|
||||
"center" => DimensionKind::Center,
|
||||
"radial" => DimensionKind::Radial,
|
||||
_ => DimensionKind::Unknown,
|
||||
};
|
||||
let layer = find_list(node, "layer")
|
||||
.and_then(|n| arg_str(n, 0))
|
||||
.unwrap_or("Dwgs.User")
|
||||
.to_string();
|
||||
let uuid = find_list(node, "uuid").and_then(|n| arg_str(n, 0)).map(str::to_string);
|
||||
|
||||
let points = find_list(node, "pts")
|
||||
.map(|pts| {
|
||||
children(pts).iter().skip(1).filter_map(|c| {
|
||||
if head(c) == Some("xy") {
|
||||
Some(Point::new(arg_f64(c, 0)?, arg_f64(c, 1)?))
|
||||
} else { None }
|
||||
}).collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let height = find_list(node, "height").and_then(|n| arg_f64(n, 0)).unwrap_or(0.0);
|
||||
let style = find_list(node, "style");
|
||||
let arrow_length_mm = style.and_then(|s| find_list(s, "arrow_length")).and_then(|n| arg_f64(n, 0)).unwrap_or(1.27);
|
||||
let thickness_mm = style.and_then(|s| find_list(s, "thickness")).and_then(|n| arg_f64(n, 0)).unwrap_or(0.1);
|
||||
let extension_height = style.and_then(|s| find_list(s, "extension_height")).and_then(|n| arg_f64(n, 0)).unwrap_or(0.58);
|
||||
let extension_offset = style.and_then(|s| find_list(s, "extension_offset")).and_then(|n| arg_f64(n, 0)).unwrap_or(0.5);
|
||||
|
||||
let text = find_list(node, "gr_text").and_then(parse_gr_text);
|
||||
|
||||
Some(Dimension {
|
||||
kind, layer, uuid, points, height,
|
||||
arrow_length_mm, thickness_mm,
|
||||
extension_height, extension_offset,
|
||||
text,
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,394 @@
|
|||
use kiutils_sexpr::Node;
|
||||
|
||||
use crate::geom::Point;
|
||||
use crate::parse::sexpr::*;
|
||||
use crate::parse::text::{parse_fp_text, FpText};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Footprint {
|
||||
pub library_id: String,
|
||||
pub at: Point,
|
||||
pub rotation_deg: f64,
|
||||
pub reference_layer: String,
|
||||
pub uuid: Option<String>,
|
||||
pub primitives: Vec<FpPrimitive>,
|
||||
pub pads: Vec<Pad>,
|
||||
pub texts: Vec<FpText>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum FpPrimitive {
|
||||
Line { start: Point, end: Point, width_mm: f64, layer: String },
|
||||
Arc { start: Point, mid: Point, end: Point, width_mm: f64, layer: String },
|
||||
Circle { centre: Point, end: Point, width_mm: f64, fill: bool, layer: String },
|
||||
Rect { start: Point, end: Point, width_mm: f64, fill: bool, layer: String },
|
||||
Poly { points: Vec<Point>, width_mm: f64, fill: bool, layer: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Pad {
|
||||
pub number: String,
|
||||
pub kind: PadKind,
|
||||
pub shape: PadShape,
|
||||
pub at: Point,
|
||||
pub rotation_deg: f64,
|
||||
pub size: (f64, f64),
|
||||
pub drill: Option<Drill>,
|
||||
pub layers: Vec<String>,
|
||||
pub anchor_shape: AnchorShape,
|
||||
pub primitives: Vec<FpPrimitive>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum AnchorShape {
|
||||
#[default]
|
||||
Rect,
|
||||
Circle,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PadKind {
|
||||
Smd,
|
||||
ThruHole,
|
||||
Connect,
|
||||
NpThruHole,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PadShape {
|
||||
Circle,
|
||||
Rect,
|
||||
Roundrect { radius_ratio: f64 },
|
||||
Oval,
|
||||
Trapezoid { delta: (f64, f64) },
|
||||
Custom,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Drill {
|
||||
pub diameter: f64,
|
||||
pub oval: Option<f64>,
|
||||
pub offset: Option<(f64, f64)>,
|
||||
}
|
||||
|
||||
pub fn parse_footprint(node: &Node) -> Option<Footprint> {
|
||||
if head(node) != Some("footprint") { return None; }
|
||||
|
||||
let library_id = arg_str(node, 0).unwrap_or("").to_string();
|
||||
let (ax, ay, rot) = read_at(node);
|
||||
let reference_layer = layer_str(node).unwrap_or_else(|| "F.Cu".to_string());
|
||||
let uuid = uuid_str(node);
|
||||
|
||||
let mut primitives = Vec::new();
|
||||
let mut pads = Vec::new();
|
||||
let mut texts = Vec::new();
|
||||
|
||||
for child in children(node).iter().skip(1) {
|
||||
match head(child) {
|
||||
Some("fp_line") => if let Some(p) = parse_fp_line(child) { primitives.push(p); }
|
||||
Some("fp_arc") => if let Some(p) = parse_fp_arc(child) { primitives.push(p); }
|
||||
Some("fp_circle") => if let Some(p) = parse_fp_circle(child) { primitives.push(p); }
|
||||
Some("fp_rect") => if let Some(p) = parse_fp_rect(child) { primitives.push(p); }
|
||||
Some("fp_poly") => if let Some(p) = parse_fp_poly(child) { primitives.push(p); }
|
||||
Some("pad") => if let Some(p) = parse_pad(child) { pads.push(p); }
|
||||
Some("fp_text") => if let Some(t) = parse_fp_text(child) { texts.push(t); }
|
||||
Some("property") => if let Some(t) = crate::parse::text::parse_property(child) { texts.push(t); }
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Some(Footprint {
|
||||
library_id,
|
||||
at: Point::new(ax, ay),
|
||||
rotation_deg: rot,
|
||||
reference_layer,
|
||||
uuid,
|
||||
primitives,
|
||||
pads,
|
||||
texts,
|
||||
})
|
||||
}
|
||||
|
||||
fn read_at(node: &Node) -> (f64, f64, f64) {
|
||||
let Some(at) = find_list(node, "at") else { return (0.0, 0.0, 0.0); };
|
||||
let x = arg_f64(at, 0).unwrap_or(0.0);
|
||||
let y = arg_f64(at, 1).unwrap_or(0.0);
|
||||
let r = arg_f64(at, 2).unwrap_or(0.0);
|
||||
(x, y, r)
|
||||
}
|
||||
|
||||
fn parse_fp_line(node: &Node) -> Option<FpPrimitive> {
|
||||
let (sx, sy) = xy_from(node, "start")?;
|
||||
let (ex, ey) = xy_from(node, "end")?;
|
||||
let layer = layer_str(node).unwrap_or_default();
|
||||
Some(FpPrimitive::Line {
|
||||
start: Point::new(sx, sy),
|
||||
end: Point::new(ex, ey),
|
||||
width_mm: stroke_width(node).unwrap_or(0.1),
|
||||
layer,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_fp_arc(node: &Node) -> Option<FpPrimitive> {
|
||||
let (sx, sy) = xy_from(node, "start")?;
|
||||
let (mx, my) = xy_from(node, "mid")?;
|
||||
let (ex, ey) = xy_from(node, "end")?;
|
||||
let layer = layer_str(node).unwrap_or_default();
|
||||
Some(FpPrimitive::Arc {
|
||||
start: Point::new(sx, sy),
|
||||
mid: Point::new(mx, my),
|
||||
end: Point::new(ex, ey),
|
||||
width_mm: stroke_width(node).unwrap_or(0.1),
|
||||
layer,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_fp_circle(node: &Node) -> Option<FpPrimitive> {
|
||||
let (cx, cy) = xy_from(node, "center").or_else(|| xy_from(node, "centre"))?;
|
||||
let (ex, ey) = xy_from(node, "end")?;
|
||||
let layer = layer_str(node).unwrap_or_default();
|
||||
Some(FpPrimitive::Circle {
|
||||
centre: Point::new(cx, cy),
|
||||
end: Point::new(ex, ey),
|
||||
width_mm: stroke_width(node).unwrap_or(0.1),
|
||||
fill: has_fill_solid(node),
|
||||
layer,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_fp_rect(node: &Node) -> Option<FpPrimitive> {
|
||||
let (sx, sy) = xy_from(node, "start")?;
|
||||
let (ex, ey) = xy_from(node, "end")?;
|
||||
let layer = layer_str(node).unwrap_or_default();
|
||||
Some(FpPrimitive::Rect {
|
||||
start: Point::new(sx, sy),
|
||||
end: Point::new(ex, ey),
|
||||
width_mm: stroke_width(node).unwrap_or(0.1),
|
||||
fill: has_fill_solid(node),
|
||||
layer,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_fp_poly(node: &Node) -> Option<FpPrimitive> {
|
||||
let pts = find_list(node, "pts")?;
|
||||
let mut points = Vec::new();
|
||||
for xy in find_lists(pts, "xy") {
|
||||
if let (Some(x), Some(y)) = (arg_f64(xy, 0), arg_f64(xy, 1)) {
|
||||
points.push(Point::new(x, y));
|
||||
}
|
||||
}
|
||||
if points.is_empty() { return None; }
|
||||
let layer = layer_str(node).unwrap_or_default();
|
||||
Some(FpPrimitive::Poly {
|
||||
points,
|
||||
width_mm: stroke_width(node).unwrap_or(0.1),
|
||||
fill: has_fill_solid(node),
|
||||
layer,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_pad(node: &Node) -> Option<Pad> {
|
||||
let number = arg_str(node, 0)?.to_string();
|
||||
let kind = match arg_str(node, 1)? {
|
||||
"smd" => PadKind::Smd,
|
||||
"thru_hole" => PadKind::ThruHole,
|
||||
"connect" => PadKind::Connect,
|
||||
"np_thru_hole" => PadKind::NpThruHole,
|
||||
_ => return None,
|
||||
};
|
||||
let shape_tok = arg_str(node, 2)?;
|
||||
let shape = match shape_tok {
|
||||
"circle" => PadShape::Circle,
|
||||
"rect" => PadShape::Rect,
|
||||
"roundrect" => {
|
||||
let rr = find_list(node, "roundrect_rratio")
|
||||
.and_then(|n| arg_f64(n, 0))
|
||||
.unwrap_or(0.25);
|
||||
PadShape::Roundrect { radius_ratio: rr }
|
||||
}
|
||||
"oval" => PadShape::Oval,
|
||||
"trapezoid" => {
|
||||
let rd = find_list(node, "rect_delta");
|
||||
let delta = match rd {
|
||||
Some(l) => (arg_f64(l, 0).unwrap_or(0.0), arg_f64(l, 1).unwrap_or(0.0)),
|
||||
None => (0.0, 0.0),
|
||||
};
|
||||
PadShape::Trapezoid { delta }
|
||||
}
|
||||
"custom" => PadShape::Custom,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let (ax, ay, rot) = read_at(node);
|
||||
let size = find_list(node, "size")
|
||||
.map(|s| (arg_f64(s, 0).unwrap_or(0.0), arg_f64(s, 1).unwrap_or(0.0)))
|
||||
.unwrap_or((0.0, 0.0));
|
||||
let drill = parse_drill(node);
|
||||
let layers = find_list(node, "layers")
|
||||
.map(|l| {
|
||||
children(l)
|
||||
.iter()
|
||||
.skip(1)
|
||||
.filter_map(symbol)
|
||||
.map(str::to_string)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let anchor_shape = find_list(node, "options")
|
||||
.and_then(|o| find_list(o, "anchor"))
|
||||
.and_then(|a| arg_str(a, 0))
|
||||
.map(|s| if s == "circle" { AnchorShape::Circle } else { AnchorShape::Rect })
|
||||
.unwrap_or(AnchorShape::Rect);
|
||||
|
||||
let primitives = matches!(shape, PadShape::Custom)
|
||||
.then(|| parse_pad_primitives(node))
|
||||
.unwrap_or_default();
|
||||
|
||||
Some(Pad {
|
||||
number,
|
||||
kind,
|
||||
shape,
|
||||
at: Point::new(ax, ay),
|
||||
rotation_deg: rot,
|
||||
size,
|
||||
drill,
|
||||
layers,
|
||||
anchor_shape,
|
||||
primitives,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_pad_primitives(pad_node: &Node) -> Vec<FpPrimitive> {
|
||||
let Some(block) = find_list(pad_node, "primitives") else { return Vec::new(); };
|
||||
let mut out = Vec::new();
|
||||
for child in children(block).iter().skip(1) {
|
||||
let prim = match head(child) {
|
||||
Some("gr_line") => parse_pad_line(child),
|
||||
Some("gr_arc") => parse_pad_arc(child),
|
||||
Some("gr_circle") => parse_pad_circle(child),
|
||||
Some("gr_rect") => parse_pad_rect(child),
|
||||
Some("gr_poly") => parse_pad_poly(child),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(p) = prim { out.push(p); }
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn parse_pad_line(node: &Node) -> Option<FpPrimitive> {
|
||||
let (sx, sy) = xy_from(node, "start")?;
|
||||
let (ex, ey) = xy_from(node, "end")?;
|
||||
Some(FpPrimitive::Line {
|
||||
start: Point::new(sx, sy),
|
||||
end: Point::new(ex, ey),
|
||||
width_mm: stroke_width(node).unwrap_or(0.1),
|
||||
layer: String::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_pad_arc(node: &Node) -> Option<FpPrimitive> {
|
||||
let (sx, sy) = xy_from(node, "start")?;
|
||||
let (mx, my) = xy_from(node, "mid")?;
|
||||
let (ex, ey) = xy_from(node, "end")?;
|
||||
Some(FpPrimitive::Arc {
|
||||
start: Point::new(sx, sy),
|
||||
mid: Point::new(mx, my),
|
||||
end: Point::new(ex, ey),
|
||||
width_mm: stroke_width(node).unwrap_or(0.1),
|
||||
layer: String::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_pad_circle(node: &Node) -> Option<FpPrimitive> {
|
||||
let (cx, cy) = xy_from(node, "center").or_else(|| xy_from(node, "centre"))?;
|
||||
let (ex, ey) = xy_from(node, "end")?;
|
||||
Some(FpPrimitive::Circle {
|
||||
centre: Point::new(cx, cy),
|
||||
end: Point::new(ex, ey),
|
||||
width_mm: stroke_width(node).unwrap_or(0.1),
|
||||
fill: true,
|
||||
layer: String::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_pad_rect(node: &Node) -> Option<FpPrimitive> {
|
||||
let (sx, sy) = xy_from(node, "start")?;
|
||||
let (ex, ey) = xy_from(node, "end")?;
|
||||
Some(FpPrimitive::Rect {
|
||||
start: Point::new(sx, sy),
|
||||
end: Point::new(ex, ey),
|
||||
width_mm: stroke_width(node).unwrap_or(0.1),
|
||||
fill: true,
|
||||
layer: String::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_pad_poly(node: &Node) -> Option<FpPrimitive> {
|
||||
let pts = find_list(node, "pts")?;
|
||||
let mut points = Vec::new();
|
||||
for xy in find_lists(pts, "xy") {
|
||||
if let (Some(x), Some(y)) = (arg_f64(xy, 0), arg_f64(xy, 1)) {
|
||||
points.push(Point::new(x, y));
|
||||
}
|
||||
}
|
||||
if points.is_empty() { return None; }
|
||||
Some(FpPrimitive::Poly {
|
||||
points,
|
||||
width_mm: stroke_width(node).unwrap_or(0.1),
|
||||
fill: true,
|
||||
layer: String::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_drill(node: &Node) -> Option<Drill> {
|
||||
let d = find_list(node, "drill")?;
|
||||
let items = children(d);
|
||||
let mut diameter = 0.0;
|
||||
let mut oval = None;
|
||||
let read_two = |i: usize| items.get(i + 1).and_then(as_f64);
|
||||
|
||||
let oval_flag = arg_str(d, 0) == Some("oval");
|
||||
|
||||
if oval_flag {
|
||||
let a = read_two(1).unwrap_or(0.0);
|
||||
let b = read_two(2).unwrap_or(a);
|
||||
diameter = a;
|
||||
oval = Some(b);
|
||||
} else if let Some(v) = read_two(0) {
|
||||
diameter = v;
|
||||
}
|
||||
|
||||
let mut offset = None;
|
||||
if let Some(off) = find_list(d, "offset") {
|
||||
offset = Some((arg_f64(off, 0).unwrap_or(0.0), arg_f64(off, 1).unwrap_or(0.0)));
|
||||
}
|
||||
|
||||
Some(Drill { diameter, oval, offset })
|
||||
}
|
||||
|
||||
fn stroke_width(node: &Node) -> Option<f64> {
|
||||
if let Some(stroke) = find_list(node, "stroke") {
|
||||
if let Some(w) = find_list(stroke, "width") {
|
||||
return arg_f64(w, 0);
|
||||
}
|
||||
}
|
||||
find_list(node, "width").and_then(|n| arg_f64(n, 0))
|
||||
}
|
||||
|
||||
fn layer_str(node: &Node) -> Option<String> {
|
||||
find_list(node, "layer").and_then(|n| arg_str(n, 0)).map(str::to_string)
|
||||
}
|
||||
|
||||
fn uuid_str(node: &Node) -> Option<String> {
|
||||
find_list(node, "uuid").and_then(|n| arg_str(n, 0)).map(str::to_string)
|
||||
}
|
||||
|
||||
fn has_fill_solid(node: &Node) -> bool {
|
||||
if let Some(fill) = find_list(node, "fill") {
|
||||
if let Some(v) = arg_str(fill, 0) {
|
||||
return matches!(v, "solid" | "yes");
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
pub mod board;
|
||||
pub mod dimension;
|
||||
pub mod footprint;
|
||||
pub mod sexpr;
|
||||
pub mod text;
|
||||
pub mod zone;
|
||||
|
||||
pub use board::{Board, Item, Segment, ArcItem, GrLine, GrArc, GrRect, GrCircle, GrPoly, Via};
|
||||
pub use dimension::{Dimension, DimensionKind};
|
||||
pub use footprint::{AnchorShape, Drill, Footprint, FpPrimitive, Pad, PadKind, PadShape};
|
||||
pub use text::{FpText, FpTextKind, GrText, JustifyH, JustifyV, TextEffects};
|
||||
pub use zone::{FilledPolygon, Zone};
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
use kiutils_sexpr::{Atom, Node};
|
||||
|
||||
pub fn head(node: &Node) -> Option<&str> {
|
||||
let Node::List { items, .. } = node else { return None; };
|
||||
items.first().and_then(symbol)
|
||||
}
|
||||
|
||||
pub fn symbol(node: &Node) -> Option<&str> {
|
||||
match node {
|
||||
Node::Atom { atom: Atom::Symbol(s), .. } => Some(s.as_str()),
|
||||
Node::Atom { atom: Atom::Quoted(s), .. } => Some(s.as_str()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn children<'a>(node: &'a Node) -> &'a [Node] {
|
||||
match node {
|
||||
Node::List { items, .. } => items,
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_list<'a>(parent: &'a Node, head_name: &str) -> Option<&'a Node> {
|
||||
children(parent)
|
||||
.iter()
|
||||
.find(|n| head(n) == Some(head_name))
|
||||
}
|
||||
|
||||
pub fn find_lists<'a>(parent: &'a Node, head_name: &str) -> Vec<&'a Node> {
|
||||
children(parent)
|
||||
.iter()
|
||||
.filter(|n| head(n) == Some(head_name))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn arg<'a>(list_node: &'a Node, index: usize) -> Option<&'a Node> {
|
||||
children(list_node).get(index + 1)
|
||||
}
|
||||
|
||||
pub fn arg_f64(list_node: &Node, index: usize) -> Option<f64> {
|
||||
arg(list_node, index).and_then(as_f64)
|
||||
}
|
||||
|
||||
pub fn arg_str<'a>(list_node: &'a Node, index: usize) -> Option<&'a str> {
|
||||
arg(list_node, index).and_then(symbol)
|
||||
}
|
||||
|
||||
pub fn as_f64(node: &Node) -> Option<f64> {
|
||||
symbol(node).and_then(|s| s.parse::<f64>().ok())
|
||||
}
|
||||
|
||||
pub fn xy_from<'a>(list_node: &'a Node, head_name: &str) -> Option<(f64, f64)> {
|
||||
let l = find_list(list_node, head_name)?;
|
||||
Some((arg_f64(l, 0)?, arg_f64(l, 1)?))
|
||||
}
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
use kiutils_sexpr::Node;
|
||||
|
||||
use crate::geom::Point;
|
||||
use crate::parse::sexpr::*;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FpTextKind {
|
||||
Reference,
|
||||
Value,
|
||||
User,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum JustifyH {
|
||||
#[default]
|
||||
Centre,
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum JustifyV {
|
||||
#[default]
|
||||
Centre,
|
||||
Top,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TextEffects {
|
||||
pub size_mm: (f64, f64),
|
||||
pub thickness_mm: f64,
|
||||
pub bold: bool,
|
||||
pub italic: bool,
|
||||
pub mirror: bool,
|
||||
pub justify_h: JustifyH,
|
||||
pub justify_v: JustifyV,
|
||||
pub hide: bool,
|
||||
}
|
||||
|
||||
impl Default for TextEffects {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
size_mm: (1.0, 1.0),
|
||||
thickness_mm: 0.15,
|
||||
bold: false,
|
||||
italic: false,
|
||||
mirror: false,
|
||||
justify_h: JustifyH::Centre,
|
||||
justify_v: JustifyV::Centre,
|
||||
hide: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GrText {
|
||||
pub text: String,
|
||||
pub at: Point,
|
||||
pub rotation_deg: f64,
|
||||
pub layer: String,
|
||||
pub effects: TextEffects,
|
||||
pub uuid: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FpText {
|
||||
pub kind: FpTextKind,
|
||||
pub text: String,
|
||||
pub at: Point,
|
||||
pub rotation_deg: f64,
|
||||
pub unlocked: bool,
|
||||
pub layer: String,
|
||||
pub effects: TextEffects,
|
||||
pub uuid: Option<String>,
|
||||
}
|
||||
|
||||
pub fn parse_gr_text(node: &Node) -> Option<GrText> {
|
||||
let text = arg_str(node, 0)?.to_string();
|
||||
let (ax, ay, rot) = read_at(node);
|
||||
let layer = layer_str(node).unwrap_or_default();
|
||||
let effects = read_effects(node);
|
||||
let uuid = uuid_str(node);
|
||||
Some(GrText {
|
||||
text,
|
||||
at: Point::new(ax, ay),
|
||||
rotation_deg: rot,
|
||||
layer,
|
||||
effects,
|
||||
uuid,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_property(node: &Node) -> Option<FpText> {
|
||||
if head(node) != Some("property") { return None; }
|
||||
let name = arg_str(node, 0)?;
|
||||
let value = arg_str(node, 1)?.to_string();
|
||||
if value.is_empty() { return None; }
|
||||
|
||||
let kind = match name {
|
||||
"Reference" => FpTextKind::Reference,
|
||||
"Value" => FpTextKind::Value,
|
||||
_ => FpTextKind::User,
|
||||
};
|
||||
let (ax, ay, rot) = read_at(node);
|
||||
let layer = layer_str(node).unwrap_or_default();
|
||||
let mut effects = read_effects(node);
|
||||
if children(node).iter().any(|n| head(n) == Some("hide")) {
|
||||
if let Some(h) = find_list(node, "hide") {
|
||||
if arg_str(h, 0) == Some("yes") {
|
||||
effects.hide = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
let uuid = uuid_str(node);
|
||||
let unlocked = children(node).iter().any(|n| {
|
||||
matches!(n, Node::Atom { atom: kiutils_sexpr::Atom::Symbol(s), .. } if s == "unlocked")
|
||||
});
|
||||
Some(FpText {
|
||||
kind,
|
||||
text: value,
|
||||
at: Point::new(ax, ay),
|
||||
rotation_deg: rot,
|
||||
unlocked,
|
||||
layer,
|
||||
effects,
|
||||
uuid,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_fp_text(node: &Node) -> Option<FpText> {
|
||||
let kind_tok = arg_str(node, 0)?;
|
||||
let kind = match kind_tok {
|
||||
"reference" => FpTextKind::Reference,
|
||||
"value" => FpTextKind::Value,
|
||||
"user" => FpTextKind::User,
|
||||
_ => return None,
|
||||
};
|
||||
let text = arg_str(node, 1)?.to_string();
|
||||
let (ax, ay, rot) = read_at(node);
|
||||
let layer = layer_str(node).unwrap_or_default();
|
||||
let effects = read_effects(node);
|
||||
let unlocked = children(node).iter().skip(1).any(|n| {
|
||||
matches!(n, Node::Atom { atom: kiutils_sexpr::Atom::Symbol(s), .. } if s == "unlocked")
|
||||
});
|
||||
let uuid = uuid_str(node);
|
||||
Some(FpText {
|
||||
kind,
|
||||
text,
|
||||
at: Point::new(ax, ay),
|
||||
rotation_deg: rot,
|
||||
unlocked,
|
||||
layer,
|
||||
effects,
|
||||
uuid,
|
||||
})
|
||||
}
|
||||
|
||||
fn read_at(node: &Node) -> (f64, f64, f64) {
|
||||
let Some(at) = find_list(node, "at") else { return (0.0, 0.0, 0.0); };
|
||||
let x = arg_f64(at, 0).unwrap_or(0.0);
|
||||
let y = arg_f64(at, 1).unwrap_or(0.0);
|
||||
let r = arg_f64(at, 2).unwrap_or(0.0);
|
||||
(x, y, r)
|
||||
}
|
||||
|
||||
fn read_effects(node: &Node) -> TextEffects {
|
||||
let mut effects = TextEffects::default();
|
||||
let Some(eff) = find_list(node, "effects") else {
|
||||
return effects;
|
||||
};
|
||||
if let Some(font) = find_list(eff, "font") {
|
||||
if let Some(s) = find_list(font, "size") {
|
||||
let w = arg_f64(s, 0).unwrap_or(1.0);
|
||||
let h = arg_f64(s, 1).unwrap_or(w);
|
||||
effects.size_mm = (w, h);
|
||||
}
|
||||
if let Some(t) = find_list(font, "thickness") {
|
||||
effects.thickness_mm = arg_f64(t, 0).unwrap_or(0.15);
|
||||
}
|
||||
if flag_present(font, "bold") { effects.bold = true; }
|
||||
if flag_present(font, "italic") { effects.italic = true; }
|
||||
}
|
||||
if let Some(j) = find_list(eff, "justify") {
|
||||
for child in children(j).iter().skip(1) {
|
||||
match symbol(child) {
|
||||
Some("left") => effects.justify_h = JustifyH::Left,
|
||||
Some("right") => effects.justify_h = JustifyH::Right,
|
||||
Some("top") => effects.justify_v = JustifyV::Top,
|
||||
Some("bottom") => effects.justify_v = JustifyV::Bottom,
|
||||
Some("mirror") => effects.mirror = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if flag_present(eff, "hide") { effects.hide = true; }
|
||||
if let Some(h) = find_list(eff, "hide") {
|
||||
if arg_str(h, 0) == Some("yes") { effects.hide = true; }
|
||||
}
|
||||
effects
|
||||
}
|
||||
|
||||
fn flag_present(parent: &Node, name: &str) -> bool {
|
||||
children(parent).iter().any(|n| {
|
||||
matches!(n, Node::Atom { atom: kiutils_sexpr::Atom::Symbol(s), .. } if s == name)
|
||||
})
|
||||
}
|
||||
|
||||
fn layer_str(node: &Node) -> Option<String> {
|
||||
find_list(node, "layer").and_then(|n| arg_str(n, 0)).map(str::to_string)
|
||||
}
|
||||
|
||||
fn uuid_str(node: &Node) -> Option<String> {
|
||||
find_list(node, "uuid").and_then(|n| arg_str(n, 0)).map(str::to_string)
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
use kiutils_sexpr::Node;
|
||||
|
||||
use crate::geom::Point;
|
||||
use crate::parse::sexpr::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Zone {
|
||||
pub layer: String,
|
||||
pub uuid: Option<String>,
|
||||
pub net_name: Option<String>,
|
||||
pub outline: Vec<Point>,
|
||||
pub filled_polygons: Vec<FilledPolygon>,
|
||||
pub keepout: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FilledPolygon {
|
||||
pub layer: String,
|
||||
pub points: Vec<Point>,
|
||||
}
|
||||
|
||||
pub fn parse_zone(node: &Node) -> Option<Zone> {
|
||||
if head(node) != Some("zone") { return None; }
|
||||
|
||||
let primary_layer = find_list(node, "layer")
|
||||
.and_then(|n| arg_str(n, 0))
|
||||
.map(str::to_string);
|
||||
let layers_list: Vec<String> = find_list(node, "layers")
|
||||
.map(|l| children(l).iter().skip(1).filter_map(symbol).map(str::to_string).collect())
|
||||
.unwrap_or_default();
|
||||
let layer = primary_layer
|
||||
.or_else(|| layers_list.first().cloned())
|
||||
.unwrap_or_default();
|
||||
|
||||
let uuid = find_list(node, "uuid").and_then(|n| arg_str(n, 0)).map(str::to_string);
|
||||
let net_name = find_list(node, "net_name")
|
||||
.and_then(|n| arg_str(n, 0))
|
||||
.map(str::to_string);
|
||||
let keepout = find_list(node, "keepout").is_some();
|
||||
|
||||
let outline = find_list(node, "polygon")
|
||||
.and_then(|p| find_list(p, "pts"))
|
||||
.map(read_xy_list)
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut filled_polygons = Vec::new();
|
||||
for fp in find_lists(node, "filled_polygon") {
|
||||
let fp_layer = find_list(fp, "layer")
|
||||
.and_then(|n| arg_str(n, 0))
|
||||
.map(str::to_string)
|
||||
.unwrap_or_else(|| layer.clone());
|
||||
if let Some(pts) = find_list(fp, "pts") {
|
||||
let points = read_xy_list(pts);
|
||||
if points.len() >= 3 {
|
||||
filled_polygons.push(FilledPolygon { layer: fp_layer, points });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(Zone { layer, uuid, net_name, outline, filled_polygons, keepout })
|
||||
}
|
||||
|
||||
fn read_xy_list(pts: &Node) -> Vec<Point> {
|
||||
let mut out = Vec::new();
|
||||
for child in children(pts).iter().skip(1) {
|
||||
match head(child) {
|
||||
Some("xy") => {
|
||||
if let (Some(x), Some(y)) = (arg_f64(child, 0), arg_f64(child, 1)) {
|
||||
out.push(Point::new(x, y));
|
||||
}
|
||||
}
|
||||
Some("arc") => {
|
||||
if let (Some((sx, sy)), Some((mx, my)), Some((ex, ey))) = (
|
||||
xy_from(child, "start"),
|
||||
xy_from(child, "mid"),
|
||||
xy_from(child, "end"),
|
||||
) {
|
||||
if out.is_empty() || out.last() != Some(&Point::new(sx, sy)) {
|
||||
out.push(Point::new(sx, sy));
|
||||
}
|
||||
out.push(Point::new(mx, my));
|
||||
out.push(Point::new(ex, ey));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
use crate::geom::{BBox, Point};
|
||||
use crate::layer::{Rgba, Style};
|
||||
use crate::parse::{Dimension, DimensionKind};
|
||||
use crate::sink::Sink;
|
||||
|
||||
pub fn draw(dim: &Dimension, colour: Rgba, sink: &mut dyn Sink, bounds: &mut BBox) {
|
||||
match dim.kind {
|
||||
DimensionKind::Aligned | DimensionKind::Orthogonal => draw_linear(dim, colour, sink, bounds),
|
||||
DimensionKind::Leader => draw_leader(dim, colour, sink, bounds),
|
||||
DimensionKind::Center => draw_center(dim, colour, sink, bounds),
|
||||
DimensionKind::Radial => draw_leader(dim, colour, sink, bounds),
|
||||
DimensionKind::Unknown => {}
|
||||
}
|
||||
|
||||
if let Some(t) = &dim.text {
|
||||
if !t.effects.hide {
|
||||
sink.text(&t.text, t.at, t.rotation_deg, &t.effects, colour);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_linear(dim: &Dimension, colour: Rgba, sink: &mut dyn Sink, bounds: &mut BBox) {
|
||||
if dim.points.len() < 2 { return; }
|
||||
let p1 = dim.points[0];
|
||||
let p2 = dim.points[1];
|
||||
let style = Style::stroke_only(colour, dim.thickness_mm.max(0.05));
|
||||
|
||||
let (dir_x, dir_y) = match dim.kind {
|
||||
DimensionKind::Orthogonal => {
|
||||
let dx = (p2.x - p1.x).abs();
|
||||
let dy = (p2.y - p1.y).abs();
|
||||
if dx >= dy { (1.0, 0.0) } else { (0.0, 1.0) }
|
||||
}
|
||||
_ => {
|
||||
let dx = p2.x - p1.x;
|
||||
let dy = p2.y - p1.y;
|
||||
let len = (dx * dx + dy * dy).sqrt();
|
||||
if len < 1e-9 { (1.0, 0.0) } else { (dx / len, dy / len) }
|
||||
}
|
||||
};
|
||||
|
||||
let nx = -dir_y;
|
||||
let ny = dir_x;
|
||||
|
||||
let d1 = Point::new(p1.x + nx * dim.height, p1.y + ny * dim.height);
|
||||
let d2 = Point::new(p2.x + nx * dim.height, p2.y + ny * dim.height);
|
||||
|
||||
let offset = dim.extension_offset;
|
||||
let ext = dim.extension_height;
|
||||
let e1a = Point::new(p1.x + nx * offset, p1.y + ny * offset);
|
||||
let e1b = Point::new(p1.x + nx * (dim.height + ext), p1.y + ny * (dim.height + ext));
|
||||
let e2a = Point::new(p2.x + nx * offset, p2.y + ny * offset);
|
||||
let e2b = Point::new(p2.x + nx * (dim.height + ext), p2.y + ny * (dim.height + ext));
|
||||
|
||||
sink.line(e1a, e1b, &style);
|
||||
sink.line(e2a, e2b, &style);
|
||||
sink.line(d1, d2, &style);
|
||||
|
||||
draw_arrowhead(d1, (dir_x, dir_y), dim.arrow_length_mm, colour, sink);
|
||||
draw_arrowhead(d2, (-dir_x, -dir_y), dim.arrow_length_mm, colour, sink);
|
||||
|
||||
for p in [p1, p2, d1, d2, e1a, e1b, e2a, e2b] {
|
||||
bounds.expand_point(p);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_leader(dim: &Dimension, colour: Rgba, sink: &mut dyn Sink, bounds: &mut BBox) {
|
||||
let style = Style::stroke_only(colour, dim.thickness_mm.max(0.05));
|
||||
for pair in dim.points.windows(2) {
|
||||
sink.line(pair[0], pair[1], &style);
|
||||
bounds.expand_point(pair[0]);
|
||||
bounds.expand_point(pair[1]);
|
||||
}
|
||||
if dim.points.len() >= 2 {
|
||||
let p0 = dim.points[0];
|
||||
let p1 = dim.points[1];
|
||||
let dx = p1.x - p0.x;
|
||||
let dy = p1.y - p0.y;
|
||||
let len = (dx * dx + dy * dy).sqrt().max(1e-9);
|
||||
draw_arrowhead(p0, (-dx / len, -dy / len), dim.arrow_length_mm, colour, sink);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_center(dim: &Dimension, colour: Rgba, sink: &mut dyn Sink, bounds: &mut BBox) {
|
||||
let style = Style::stroke_only(colour, dim.thickness_mm.max(0.05));
|
||||
let Some(c) = dim.points.first().copied() else { return; };
|
||||
let arm = dim.arrow_length_mm.max(1.0);
|
||||
sink.line(Point::new(c.x - arm, c.y), Point::new(c.x + arm, c.y), &style);
|
||||
sink.line(Point::new(c.x, c.y - arm), Point::new(c.x, c.y + arm), &style);
|
||||
bounds.expand_point(Point::new(c.x - arm, c.y - arm));
|
||||
bounds.expand_point(Point::new(c.x + arm, c.y + arm));
|
||||
}
|
||||
|
||||
fn draw_arrowhead(tip: Point, dir: (f64, f64), length: f64, colour: Rgba, sink: &mut dyn Sink) {
|
||||
let (dx, dy) = dir;
|
||||
let (nx, ny) = (-dy, dx);
|
||||
let half_w = length * 0.25;
|
||||
let base_x = tip.x + dx * length;
|
||||
let base_y = tip.y + dy * length;
|
||||
let p_a = Point::new(base_x + nx * half_w, base_y + ny * half_w);
|
||||
let p_b = Point::new(base_x - nx * half_w, base_y - ny * half_w);
|
||||
sink.polygon(&[tip, p_a, p_b], &Style::fill_only(colour));
|
||||
}
|
||||
|
|
@ -0,0 +1,212 @@
|
|||
use crate::geom::{BBox, Point, Transform};
|
||||
use crate::layer::{Rgba, Style};
|
||||
use crate::parse::{AnchorShape, FpPrimitive, Pad, PadShape};
|
||||
use crate::render::prim;
|
||||
use crate::sink::Sink;
|
||||
|
||||
pub enum LayerCall<'a> {
|
||||
Primitive { prim: &'a FpPrimitive, tf: Transform, palette_colour: Rgba },
|
||||
Pad { pad: &'a Pad, fp_tf: Transform, palette_colour: Rgba },
|
||||
}
|
||||
|
||||
pub fn draw(call: &LayerCall<'_>, sink: &mut dyn Sink, bounds: &mut BBox) {
|
||||
match call {
|
||||
LayerCall::Primitive { prim, tf, palette_colour } => draw_primitive(prim, *palette_colour, tf, sink, bounds),
|
||||
LayerCall::Pad { pad, fp_tf, palette_colour } => draw_pad(pad, *palette_colour, fp_tf, sink, bounds),
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_primitive(p: &FpPrimitive, colour: Rgba, tf: &Transform, sink: &mut dyn Sink, bounds: &mut BBox) {
|
||||
match p {
|
||||
FpPrimitive::Line { start, end, width_mm, .. } => {
|
||||
prim::stroke_segment(*start, *end, *width_mm, colour, tf, sink, bounds);
|
||||
}
|
||||
FpPrimitive::Arc { start, mid, end, width_mm, .. } => {
|
||||
prim::stroke_arc(*start, *mid, *end, *width_mm, colour, tf, sink, bounds);
|
||||
}
|
||||
FpPrimitive::Circle { centre, end, width_mm, fill, .. } => {
|
||||
prim::circle(*centre, *end, *width_mm, *fill, colour, tf, sink, bounds);
|
||||
}
|
||||
FpPrimitive::Rect { start, end, width_mm, fill, .. } => {
|
||||
prim::rect(*start, *end, *width_mm, *fill, colour, tf, sink, bounds);
|
||||
}
|
||||
FpPrimitive::Poly { points, width_mm, fill, .. } => {
|
||||
prim::poly(points, *width_mm, *fill, colour, tf, sink, bounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_pad(pad: &Pad, colour: Rgba, fp_tf: &Transform, sink: &mut dyn Sink, bounds: &mut BBox) {
|
||||
let pad_local = Transform::at(pad.at, pad.rotation_deg);
|
||||
let pad_tf = compose(fp_tf, &pad_local);
|
||||
let (w, h) = pad.size;
|
||||
let hw = w * 0.5;
|
||||
let hh = h * 0.5;
|
||||
let style = Style::fill_only(colour);
|
||||
|
||||
match &pad.shape {
|
||||
PadShape::Circle => {
|
||||
let c = pad_tf.apply(Point::new(0.0, 0.0));
|
||||
let r = hw.max(hh);
|
||||
sink.circle(c, r, &style);
|
||||
bounds.expand_point(Point::new(c.x - r, c.y - r));
|
||||
bounds.expand_point(Point::new(c.x + r, c.y + r));
|
||||
}
|
||||
PadShape::Rect => {
|
||||
let pts = [
|
||||
pad_tf.apply(Point::new(-hw, -hh)),
|
||||
pad_tf.apply(Point::new( hw, -hh)),
|
||||
pad_tf.apply(Point::new( hw, hh)),
|
||||
pad_tf.apply(Point::new(-hw, hh)),
|
||||
];
|
||||
sink.polygon(&pts, &style);
|
||||
for p in &pts { bounds.expand_point(*p); }
|
||||
}
|
||||
PadShape::Roundrect { radius_ratio } => {
|
||||
let r = hw.min(hh) * radius_ratio;
|
||||
draw_rounded_rect(pad_tf, hw, hh, r, &style, sink, bounds);
|
||||
}
|
||||
PadShape::Oval => {
|
||||
draw_oval(pad_tf, hw, hh, &style, sink, bounds);
|
||||
}
|
||||
PadShape::Trapezoid { delta } => {
|
||||
let (dx, dy) = *delta;
|
||||
let pts = [
|
||||
pad_tf.apply(Point::new(-hw - dx * 0.5, -hh - dy * 0.5)),
|
||||
pad_tf.apply(Point::new( hw + dx * 0.5, -hh + dy * 0.5)),
|
||||
pad_tf.apply(Point::new( hw - dx * 0.5, hh + dy * 0.5)),
|
||||
pad_tf.apply(Point::new(-hw + dx * 0.5, hh - dy * 0.5)),
|
||||
];
|
||||
sink.polygon(&pts, &style);
|
||||
for p in &pts { bounds.expand_point(*p); }
|
||||
}
|
||||
PadShape::Custom => {
|
||||
match pad.anchor_shape {
|
||||
AnchorShape::Circle => {
|
||||
let c = pad_tf.apply(Point::new(0.0, 0.0));
|
||||
let r = hw.max(hh);
|
||||
if r > 1e-9 {
|
||||
sink.circle(c, r, &style);
|
||||
bounds.expand_point(Point::new(c.x - r, c.y - r));
|
||||
bounds.expand_point(Point::new(c.x + r, c.y + r));
|
||||
}
|
||||
}
|
||||
AnchorShape::Rect => {
|
||||
if hw > 1e-9 && hh > 1e-9 {
|
||||
let pts = [
|
||||
pad_tf.apply(Point::new(-hw, -hh)),
|
||||
pad_tf.apply(Point::new( hw, -hh)),
|
||||
pad_tf.apply(Point::new( hw, hh)),
|
||||
pad_tf.apply(Point::new(-hw, hh)),
|
||||
];
|
||||
sink.polygon(&pts, &style);
|
||||
for p in &pts { bounds.expand_point(*p); }
|
||||
}
|
||||
}
|
||||
}
|
||||
for prim in &pad.primitives {
|
||||
draw_custom_primitive(prim, colour, &pad_tf, sink, bounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(drill) = &pad.drill {
|
||||
let c = pad_tf.apply(Point::new(
|
||||
drill.offset.map(|o| o.0).unwrap_or(0.0),
|
||||
drill.offset.map(|o| o.1).unwrap_or(0.0),
|
||||
));
|
||||
let r = drill.diameter * 0.5;
|
||||
sink.circle(c, r, &Style::fill_only(Rgba::opaque(15, 15, 15)));
|
||||
}
|
||||
}
|
||||
|
||||
fn compose(outer: &Transform, inner: &Transform) -> Transform {
|
||||
let local = outer.apply(inner.translate);
|
||||
Transform {
|
||||
translate: local,
|
||||
rotation_rad: outer.rotation_rad + inner.rotation_rad,
|
||||
mirror_x: outer.mirror_x ^ inner.mirror_x,
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_custom_primitive(
|
||||
prim: &FpPrimitive,
|
||||
colour: Rgba,
|
||||
tf: &Transform,
|
||||
sink: &mut dyn Sink,
|
||||
bounds: &mut BBox,
|
||||
) {
|
||||
match prim {
|
||||
FpPrimitive::Line { start, end, width_mm, .. } => {
|
||||
prim::stroke_segment(*start, *end, *width_mm, colour, tf, sink, bounds);
|
||||
}
|
||||
FpPrimitive::Arc { start, mid, end, width_mm, .. } => {
|
||||
prim::stroke_arc(*start, *mid, *end, *width_mm, colour, tf, sink, bounds);
|
||||
}
|
||||
FpPrimitive::Circle { centre, end, width_mm, fill, .. } => {
|
||||
prim::circle(*centre, *end, *width_mm, *fill, colour, tf, sink, bounds);
|
||||
}
|
||||
FpPrimitive::Rect { start, end, width_mm, fill, .. } => {
|
||||
prim::rect(*start, *end, *width_mm, *fill, colour, tf, sink, bounds);
|
||||
}
|
||||
FpPrimitive::Poly { points, width_mm, fill, .. } => {
|
||||
prim::poly(points, *width_mm, *fill, colour, tf, sink, bounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_rounded_rect(
|
||||
tf: Transform,
|
||||
hw: f64,
|
||||
hh: f64,
|
||||
r: f64,
|
||||
style: &Style,
|
||||
sink: &mut dyn Sink,
|
||||
bounds: &mut BBox,
|
||||
) {
|
||||
let r = r.min(hw).min(hh);
|
||||
let segs_per_corner = 6;
|
||||
let mut pts = Vec::with_capacity(segs_per_corner * 4 + 4);
|
||||
let corners = [
|
||||
(Point::new( hw - r, hh - r), 0.0),
|
||||
(Point::new(-hw + r, hh - r), std::f64::consts::FRAC_PI_2),
|
||||
(Point::new(-hw + r, -hh + r), std::f64::consts::PI),
|
||||
(Point::new( hw - r, -hh + r), 3.0 * std::f64::consts::FRAC_PI_2),
|
||||
];
|
||||
for (centre, start_angle) in corners {
|
||||
for i in 0..=segs_per_corner {
|
||||
let t = i as f64 / segs_per_corner as f64;
|
||||
let a = start_angle + t * std::f64::consts::FRAC_PI_2;
|
||||
pts.push(Point::new(centre.x + r * a.cos(), centre.y + r * a.sin()));
|
||||
}
|
||||
}
|
||||
let world: Vec<Point> = pts.iter().map(|p| tf.apply(*p)).collect();
|
||||
sink.polygon(&world, style);
|
||||
for p in &world { bounds.expand_point(*p); }
|
||||
}
|
||||
|
||||
fn draw_oval(
|
||||
tf: Transform,
|
||||
hw: f64,
|
||||
hh: f64,
|
||||
style: &Style,
|
||||
sink: &mut dyn Sink,
|
||||
bounds: &mut BBox,
|
||||
) {
|
||||
if (hw - hh).abs() < 1e-9 {
|
||||
let c = tf.apply(Point::new(0.0, 0.0));
|
||||
sink.circle(c, hw, style);
|
||||
bounds.expand_point(Point::new(c.x - hw, c.y - hw));
|
||||
bounds.expand_point(Point::new(c.x + hw, c.y + hw));
|
||||
return;
|
||||
}
|
||||
let segs = 32;
|
||||
let mut pts = Vec::with_capacity(segs);
|
||||
for i in 0..segs {
|
||||
let a = (i as f64) / (segs as f64) * std::f64::consts::TAU;
|
||||
pts.push(Point::new(hw * a.cos(), hh * a.sin()));
|
||||
}
|
||||
let world: Vec<Point> = pts.iter().map(|p| tf.apply(*p)).collect();
|
||||
sink.polygon(&world, style);
|
||||
for p in &world { bounds.expand_point(*p); }
|
||||
}
|
||||
|
|
@ -0,0 +1,288 @@
|
|||
pub mod dimension;
|
||||
pub mod footprint;
|
||||
pub mod palette;
|
||||
pub mod prim;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::geom::{BBox, Point, Transform};
|
||||
use crate::layer::{LayerName, Rgba, Style};
|
||||
use crate::parse::{Board, Dimension, FilledPolygon, Footprint, FpText, GrText, Item};
|
||||
use crate::sink::Sink;
|
||||
|
||||
pub use palette::Palette;
|
||||
|
||||
use footprint::LayerCall;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RenderOptions {
|
||||
pub palette: Palette,
|
||||
pub layer_filter: Option<Vec<String>>,
|
||||
pub uuid_filter: Option<std::collections::HashSet<String>>,
|
||||
pub draw_vias: bool,
|
||||
pub draw_pads: bool,
|
||||
pub draw_text: bool,
|
||||
pub include_reference_text: bool,
|
||||
pub include_value_text: bool,
|
||||
pub include_hidden_text: bool,
|
||||
pub draw_zones: bool,
|
||||
pub draw_dimensions: bool,
|
||||
}
|
||||
|
||||
impl Default for RenderOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
palette: Palette::default(),
|
||||
layer_filter: None,
|
||||
uuid_filter: None,
|
||||
draw_vias: true,
|
||||
draw_pads: true,
|
||||
draw_text: true,
|
||||
include_reference_text: true,
|
||||
include_value_text: false,
|
||||
include_hidden_text: false,
|
||||
draw_zones: true,
|
||||
draw_dimensions: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(board: &Board, sink: &mut dyn Sink, opts: &RenderOptions) -> BBox {
|
||||
let mut by_layer: BTreeMap<String, Vec<Draw<'_>>> = BTreeMap::new();
|
||||
let mut vias: Vec<&crate::parse::Via> = Vec::new();
|
||||
|
||||
for item in &board.items {
|
||||
if let Some(uuid) = item_uuid(item) {
|
||||
if let Some(keep) = &opts.uuid_filter {
|
||||
if !keep.contains(uuid) { continue; }
|
||||
}
|
||||
}
|
||||
match item {
|
||||
Item::Via(v) => if opts.draw_vias { vias.push(v); },
|
||||
Item::Footprint(fp) => fold_footprint(fp, opts, &mut by_layer),
|
||||
Item::GrText(t) => {
|
||||
if !opts.draw_text { continue; }
|
||||
if t.effects.hide && !opts.include_hidden_text { continue; }
|
||||
if layer_allowed(&t.layer, opts) {
|
||||
by_layer.entry(t.layer.clone()).or_default().push(Draw::GrText(t));
|
||||
}
|
||||
}
|
||||
Item::Zone(z) => {
|
||||
if !opts.draw_zones { continue; }
|
||||
if z.keepout { continue; }
|
||||
for fp_poly in &z.filled_polygons {
|
||||
if layer_allowed(&fp_poly.layer, opts) {
|
||||
by_layer.entry(fp_poly.layer.clone()).or_default().push(Draw::ZoneFill(fp_poly));
|
||||
}
|
||||
}
|
||||
}
|
||||
Item::Dimension(d) => {
|
||||
if !opts.draw_dimensions { continue; }
|
||||
if layer_allowed(&d.layer, opts) {
|
||||
by_layer.entry(d.layer.clone()).or_default().push(Draw::Dimension(d));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let layer = item_layer(item).to_string();
|
||||
if layer_allowed(&layer, opts) {
|
||||
by_layer.entry(layer).or_default().push(Draw::Item(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut bounds = BBox::empty();
|
||||
for (layer, draws) in &by_layer {
|
||||
let ln = LayerName::new(layer.clone());
|
||||
sink.begin_layer(&ln);
|
||||
let colour = opts.palette.colour(layer);
|
||||
for draw in draws {
|
||||
match draw {
|
||||
Draw::Item(item) => draw_item(item, colour, sink, &mut bounds),
|
||||
Draw::Fp(call) => footprint::draw(call, sink, &mut bounds),
|
||||
Draw::GrText(t) => {
|
||||
sink.text(&t.text, t.at, t.rotation_deg, &t.effects, colour);
|
||||
expand_text_bounds(t.at, &t.effects, t.text.len(), &mut bounds);
|
||||
}
|
||||
Draw::FpText { text, tf } => {
|
||||
let world = tf.apply(text.at);
|
||||
let rot = tf.rotation_rad.to_degrees() + text.rotation_deg;
|
||||
sink.text(&text.text, world, rot, &text.effects, colour);
|
||||
expand_text_bounds(world, &text.effects, text.text.len(), &mut bounds);
|
||||
}
|
||||
Draw::ZoneFill(fp_poly) => {
|
||||
let style = Style::fill_only(colour);
|
||||
sink.polygon(&fp_poly.points, &style);
|
||||
for p in &fp_poly.points {
|
||||
bounds.expand_point(*p);
|
||||
}
|
||||
}
|
||||
Draw::Dimension(d) => {
|
||||
dimension::draw(d, colour, sink, &mut bounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
sink.end_layer(&ln);
|
||||
}
|
||||
if opts.draw_vias && !vias.is_empty() {
|
||||
let ln = LayerName::new("*.via".to_string());
|
||||
sink.begin_layer(&ln);
|
||||
for v in vias {
|
||||
draw_via(v, &opts.palette, sink, &mut bounds);
|
||||
}
|
||||
sink.end_layer(&ln);
|
||||
}
|
||||
bounds
|
||||
}
|
||||
|
||||
enum Draw<'a> {
|
||||
Item(&'a Item),
|
||||
Fp(LayerCall<'a>),
|
||||
GrText(&'a GrText),
|
||||
FpText { text: &'a FpText, tf: Transform },
|
||||
ZoneFill(&'a FilledPolygon),
|
||||
Dimension(&'a Dimension),
|
||||
}
|
||||
|
||||
fn expand_text_bounds(pos: Point, effects: &crate::parse::TextEffects, len: usize, bounds: &mut BBox) {
|
||||
let w = effects.size_mm.0 * len as f64 * 0.6;
|
||||
let h = effects.size_mm.1;
|
||||
bounds.expand_point(Point::new(pos.x - w, pos.y - h));
|
||||
bounds.expand_point(Point::new(pos.x + w, pos.y + h));
|
||||
}
|
||||
|
||||
fn fold_footprint<'a>(
|
||||
fp: &'a Footprint,
|
||||
opts: &RenderOptions,
|
||||
out: &mut BTreeMap<String, Vec<Draw<'a>>>,
|
||||
) {
|
||||
let tf = Transform::at(fp.at, fp.rotation_deg);
|
||||
|
||||
for p in &fp.primitives {
|
||||
let layer = match p {
|
||||
crate::parse::FpPrimitive::Line { layer, .. }
|
||||
| crate::parse::FpPrimitive::Arc { layer, .. }
|
||||
| crate::parse::FpPrimitive::Circle { layer, .. }
|
||||
| crate::parse::FpPrimitive::Rect { layer, .. }
|
||||
| crate::parse::FpPrimitive::Poly { layer, .. } => layer.clone(),
|
||||
};
|
||||
if !layer_allowed(&layer, opts) { continue; }
|
||||
let colour = opts.palette.colour(&layer);
|
||||
out.entry(layer).or_default().push(Draw::Fp(LayerCall::Primitive {
|
||||
prim: p, tf, palette_colour: colour,
|
||||
}));
|
||||
}
|
||||
|
||||
if opts.draw_pads {
|
||||
for pad in &fp.pads {
|
||||
for layer in &pad.layers {
|
||||
for expanded in expand_wildcard_layer(layer) {
|
||||
if !layer_allowed(&expanded, opts) { continue; }
|
||||
let colour = opts.palette.colour(&expanded);
|
||||
out.entry(expanded).or_default().push(Draw::Fp(LayerCall::Pad {
|
||||
pad, fp_tf: tf, palette_colour: colour,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if opts.draw_text {
|
||||
for text in &fp.texts {
|
||||
use crate::parse::FpTextKind;
|
||||
let skip = match text.kind {
|
||||
FpTextKind::Reference => !opts.include_reference_text,
|
||||
FpTextKind::Value => !opts.include_value_text,
|
||||
FpTextKind::User => false,
|
||||
};
|
||||
if skip { continue; }
|
||||
if text.effects.hide && !opts.include_hidden_text { continue; }
|
||||
if !layer_allowed(&text.layer, opts) { continue; }
|
||||
out.entry(text.layer.clone()).or_default().push(Draw::FpText { text, tf });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn layer_allowed(layer: &str, opts: &RenderOptions) -> bool {
|
||||
match &opts.layer_filter {
|
||||
Some(filter) => filter.iter().any(|l| l == layer),
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn expand_wildcard_layer(layer: &str) -> Vec<String> {
|
||||
let wildcards: &[&str] = match layer {
|
||||
"*.Cu" => &["F.Cu", "In1.Cu", "In2.Cu", "In3.Cu", "In4.Cu", "B.Cu"],
|
||||
"*.Mask" => &["F.Mask", "B.Mask"],
|
||||
"*.Paste" => &["F.Paste", "B.Paste"],
|
||||
"*.SilkS" | "*.Silkscreen" => &["F.SilkS", "B.SilkS"],
|
||||
"*.Fab" => &["F.Fab", "B.Fab"],
|
||||
"*.CrtYd" | "*.Courtyard" => &["F.CrtYd", "B.CrtYd"],
|
||||
"F&B.Cu" => &["F.Cu", "B.Cu"],
|
||||
_ => return vec![layer.to_string()],
|
||||
};
|
||||
wildcards.iter().map(|s| s.to_string()).collect()
|
||||
}
|
||||
|
||||
fn item_layer(item: &Item) -> &str {
|
||||
match item {
|
||||
Item::Segment(s) => &s.layer,
|
||||
Item::Arc(a) => &a.layer,
|
||||
Item::GrLine(g) => &g.layer,
|
||||
Item::GrArc(g) => &g.layer,
|
||||
Item::GrRect(g) => &g.layer,
|
||||
Item::GrCircle(g) => &g.layer,
|
||||
Item::GrPoly(g) => &g.layer,
|
||||
Item::GrText(t) => &t.layer,
|
||||
Item::Zone(z) => &z.layer,
|
||||
Item::Dimension(d) => &d.layer,
|
||||
Item::Via(_) => "*.via",
|
||||
Item::Footprint(f) => &f.reference_layer,
|
||||
}
|
||||
}
|
||||
|
||||
fn item_uuid(item: &Item) -> Option<&str> {
|
||||
match item {
|
||||
Item::Segment(s) => s.uuid.as_deref(),
|
||||
Item::Arc(a) => a.uuid.as_deref(),
|
||||
Item::Via(v) => v.uuid.as_deref(),
|
||||
Item::GrLine(g) => g.uuid.as_deref(),
|
||||
Item::GrArc(g) => g.uuid.as_deref(),
|
||||
Item::GrRect(g) => g.uuid.as_deref(),
|
||||
Item::GrCircle(g) => g.uuid.as_deref(),
|
||||
Item::GrPoly(g) => g.uuid.as_deref(),
|
||||
Item::GrText(t) => t.uuid.as_deref(),
|
||||
Item::Zone(z) => z.uuid.as_deref(),
|
||||
Item::Dimension(d) => d.uuid.as_deref(),
|
||||
Item::Footprint(f) => f.uuid.as_deref(),
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_item(item: &Item, colour: Rgba, sink: &mut dyn Sink, bounds: &mut BBox) {
|
||||
let tf = Transform::IDENTITY;
|
||||
match item {
|
||||
Item::Segment(s) => prim::stroke_segment(s.start, s.end, s.width_mm, colour, &tf, sink, bounds),
|
||||
Item::Arc(a) => prim::stroke_arc(a.start, a.mid, a.end, a.width_mm, colour, &tf, sink, bounds),
|
||||
Item::GrLine(g) => prim::stroke_segment(g.start, g.end, g.width_mm, colour, &tf, sink, bounds),
|
||||
Item::GrArc(g) => prim::stroke_arc(g.start, g.mid, g.end, g.width_mm, colour, &tf, sink, bounds),
|
||||
Item::GrRect(g) => prim::rect(g.start, g.end, g.width_mm, g.fill, colour, &tf, sink, bounds),
|
||||
Item::GrCircle(g) => prim::circle(g.centre, g.end, g.width_mm, g.fill, colour, &tf, sink, bounds),
|
||||
Item::GrPoly(g) => prim::poly(&g.points, g.width_mm, g.fill, colour, &tf, sink, bounds),
|
||||
Item::Via(_)
|
||||
| Item::Footprint(_)
|
||||
| Item::GrText(_)
|
||||
| Item::Zone(_)
|
||||
| Item::Dimension(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_via(v: &crate::parse::Via, palette: &Palette, sink: &mut dyn Sink, bounds: &mut BBox) {
|
||||
let outer = palette.via_body();
|
||||
let drill = palette.via_drill();
|
||||
let ro = v.size_mm * 0.5;
|
||||
let rd = v.drill_mm * 0.5;
|
||||
sink.circle(v.at, ro, &Style::fill_only(outer));
|
||||
sink.circle(v.at, rd, &Style::fill_only(drill));
|
||||
bounds.expand_point(crate::geom::Point::new(v.at.x - ro, v.at.y - ro));
|
||||
bounds.expand_point(crate::geom::Point::new(v.at.x + ro, v.at.y + ro));
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
use crate::layer::Rgba;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Palette {
|
||||
pub default: Rgba,
|
||||
pub overrides: Vec<(String, Rgba)>,
|
||||
pub via_body: Rgba,
|
||||
pub via_drill: Rgba,
|
||||
}
|
||||
|
||||
impl Palette {
|
||||
pub fn colour(&self, layer: &str) -> Rgba {
|
||||
self.overrides
|
||||
.iter()
|
||||
.find(|(n, _)| n == layer)
|
||||
.map(|(_, c)| *c)
|
||||
.unwrap_or(self.default)
|
||||
}
|
||||
|
||||
pub fn with_override(mut self, layer: impl Into<String>, colour: Rgba) -> Self {
|
||||
self.overrides.push((layer.into(), colour));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn via_body(&self) -> Rgba { self.via_body }
|
||||
pub fn via_drill(&self) -> Rgba { self.via_drill }
|
||||
}
|
||||
|
||||
impl Default for Palette {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
default: Rgba::opaque(180, 180, 180),
|
||||
overrides: vec![
|
||||
("F.Cu".into(), Rgba::opaque(200, 52, 52)),
|
||||
("B.Cu".into(), Rgba::opaque(77, 127, 196)),
|
||||
("In1.Cu".into(), Rgba::opaque(127, 200, 127)),
|
||||
("In2.Cu".into(), Rgba::opaque(206, 125, 188)),
|
||||
("F.Silkscreen".into(), Rgba::opaque(220, 200, 200)),
|
||||
("B.Silkscreen".into(), Rgba::opaque(200, 220, 220)),
|
||||
("F.SilkS".into(), Rgba::opaque(220, 200, 200)),
|
||||
("B.SilkS".into(), Rgba::opaque(200, 220, 220)),
|
||||
("F.Mask".into(), Rgba::opaque(102, 0, 102)),
|
||||
("B.Mask".into(), Rgba::opaque(102, 0, 102)),
|
||||
("F.Paste".into(), Rgba::opaque(180, 180, 180)),
|
||||
("B.Paste".into(), Rgba::opaque(180, 180, 180)),
|
||||
("F.Fab".into(), Rgba::opaque(150, 115, 60)),
|
||||
("B.Fab".into(), Rgba::opaque(150, 115, 60)),
|
||||
("F.Courtyard".into(), Rgba::opaque(255, 38, 226)),
|
||||
("B.Courtyard".into(), Rgba::opaque(38, 233, 255)),
|
||||
("F.CrtYd".into(), Rgba::opaque(255, 38, 226)),
|
||||
("B.CrtYd".into(), Rgba::opaque(38, 233, 255)),
|
||||
("Edge.Cuts".into(), Rgba::opaque(255, 240, 150)),
|
||||
("Margin".into(), Rgba::opaque(255, 120, 120)),
|
||||
("Dwgs.User".into(), Rgba::opaque(194, 194, 194)),
|
||||
("Cmts.User".into(), Rgba::opaque(89, 148, 220)),
|
||||
],
|
||||
via_body: Rgba::opaque(200, 170, 80),
|
||||
via_drill: Rgba::opaque(30, 30, 30),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
use crate::geom::{Arc, BBox, Point, Transform};
|
||||
use crate::layer::{Rgba, Style};
|
||||
use crate::sink::Sink;
|
||||
|
||||
pub fn stroke_segment(
|
||||
start: Point,
|
||||
end: Point,
|
||||
width_mm: f64,
|
||||
colour: Rgba,
|
||||
tf: &Transform,
|
||||
sink: &mut dyn Sink,
|
||||
bounds: &mut BBox,
|
||||
) {
|
||||
let a = tf.apply(start);
|
||||
let b = tf.apply(end);
|
||||
sink.line(a, b, &Style::stroke_only(colour, width_mm));
|
||||
bounds.expand_point(a);
|
||||
bounds.expand_point(b);
|
||||
}
|
||||
|
||||
pub fn stroke_arc(
|
||||
start: Point,
|
||||
mid: Point,
|
||||
end: Point,
|
||||
width_mm: f64,
|
||||
colour: Rgba,
|
||||
tf: &Transform,
|
||||
sink: &mut dyn Sink,
|
||||
bounds: &mut BBox,
|
||||
) {
|
||||
let a = tf.apply(start);
|
||||
let m = tf.apply(mid);
|
||||
let e = tf.apply(end);
|
||||
sink.arc(Arc::new(a, m, e), &Style::stroke_only(colour, width_mm));
|
||||
bounds.expand_point(a);
|
||||
bounds.expand_point(m);
|
||||
bounds.expand_point(e);
|
||||
}
|
||||
|
||||
pub fn circle(
|
||||
centre: Point,
|
||||
end: Point,
|
||||
width_mm: f64,
|
||||
fill: bool,
|
||||
colour: Rgba,
|
||||
tf: &Transform,
|
||||
sink: &mut dyn Sink,
|
||||
bounds: &mut BBox,
|
||||
) {
|
||||
let c = tf.apply(centre);
|
||||
let e = tf.apply(end);
|
||||
let r = c.distance_to(e);
|
||||
let style = if fill {
|
||||
Style::fill_and_stroke(colour, colour, width_mm)
|
||||
} else {
|
||||
Style::stroke_only(colour, width_mm)
|
||||
};
|
||||
sink.circle(c, r, &style);
|
||||
bounds.expand_point(Point::new(c.x - r, c.y - r));
|
||||
bounds.expand_point(Point::new(c.x + r, c.y + r));
|
||||
}
|
||||
|
||||
pub fn rect(
|
||||
a: Point,
|
||||
b: Point,
|
||||
width_mm: f64,
|
||||
fill: bool,
|
||||
colour: Rgba,
|
||||
tf: &Transform,
|
||||
sink: &mut dyn Sink,
|
||||
bounds: &mut BBox,
|
||||
) {
|
||||
let pts = [
|
||||
tf.apply(Point::new(a.x, a.y)),
|
||||
tf.apply(Point::new(b.x, a.y)),
|
||||
tf.apply(Point::new(b.x, b.y)),
|
||||
tf.apply(Point::new(a.x, b.y)),
|
||||
];
|
||||
let style = if fill {
|
||||
Style::fill_and_stroke(colour, colour, width_mm)
|
||||
} else {
|
||||
Style::stroke_only(colour, width_mm)
|
||||
};
|
||||
if tf.rotation_rad.abs() < 1e-9 && !tf.mirror_x {
|
||||
sink.rect(pts[0], pts[2], &style);
|
||||
} else {
|
||||
sink.polygon(&pts, &style);
|
||||
}
|
||||
for p in &pts {
|
||||
bounds.expand_point(*p);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn poly(
|
||||
points: &[Point],
|
||||
width_mm: f64,
|
||||
fill: bool,
|
||||
colour: Rgba,
|
||||
tf: &Transform,
|
||||
sink: &mut dyn Sink,
|
||||
bounds: &mut BBox,
|
||||
) {
|
||||
let transformed: Vec<Point> = points.iter().map(|p| tf.apply(*p)).collect();
|
||||
let style = if fill {
|
||||
Style::fill_and_stroke(colour, colour, width_mm)
|
||||
} else {
|
||||
Style::stroke_only(colour, width_mm)
|
||||
};
|
||||
sink.polyline(&transformed, fill, &style);
|
||||
for p in &transformed {
|
||||
bounds.expand_point(*p);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
pub mod svg;
|
||||
|
||||
use crate::geom::{Arc, Point};
|
||||
use crate::layer::{LayerName, Rgba, Style};
|
||||
use crate::parse::TextEffects;
|
||||
|
||||
pub trait Sink {
|
||||
fn begin_layer(&mut self, _layer: &LayerName) {}
|
||||
fn end_layer(&mut self, _layer: &LayerName) {}
|
||||
|
||||
fn line(&mut self, a: Point, b: Point, style: &Style);
|
||||
fn arc(&mut self, arc: Arc, style: &Style);
|
||||
fn circle(&mut self, centre: Point, radius: f64, style: &Style);
|
||||
fn polyline(&mut self, points: &[Point], closed: bool, style: &Style);
|
||||
fn polygon(&mut self, points: &[Point], style: &Style);
|
||||
fn rect(&mut self, a: Point, b: Point, style: &Style);
|
||||
|
||||
fn text(
|
||||
&mut self,
|
||||
text: &str,
|
||||
pos: Point,
|
||||
rotation_deg: f64,
|
||||
effects: &TextEffects,
|
||||
colour: Rgba,
|
||||
) {
|
||||
let _ = (text, pos, rotation_deg, effects, colour);
|
||||
}
|
||||
}
|
||||
|
||||
pub use svg::SvgSink;
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
use std::fmt::Write as _;
|
||||
|
||||
use crate::geom::{Arc, BBox, Point};
|
||||
use crate::layer::{LayerName, Rgba, Style};
|
||||
use crate::parse::{JustifyH, JustifyV, TextEffects};
|
||||
use crate::sink::Sink;
|
||||
|
||||
pub struct SvgSink {
|
||||
pub bounds: BBox,
|
||||
pub padding_mm: f64,
|
||||
body: String,
|
||||
active_layer: Option<String>,
|
||||
layer_open: bool,
|
||||
}
|
||||
|
||||
impl SvgSink {
|
||||
pub fn new(bounds: BBox) -> Self {
|
||||
Self {
|
||||
bounds,
|
||||
padding_mm: 0.5,
|
||||
body: String::new(),
|
||||
active_layer: None,
|
||||
layer_open: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish(mut self) -> String {
|
||||
if self.layer_open {
|
||||
self.body.push_str("</g>\n");
|
||||
self.layer_open = false;
|
||||
}
|
||||
let mut bb = self.bounds;
|
||||
bb.inflate(self.padding_mm);
|
||||
let (w, h) = (bb.width().max(0.001), bb.height().max(0.001));
|
||||
let mut out = String::with_capacity(self.body.len() + 256);
|
||||
let _ = write!(
|
||||
out,
|
||||
"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"{:.4} {:.4} {:.4} {:.4}\" width=\"{:.4}mm\" height=\"{:.4}mm\">\n",
|
||||
bb.min_x, bb.min_y, w, h, w, h
|
||||
);
|
||||
out.push_str(&self.body);
|
||||
out.push_str("</svg>\n");
|
||||
out
|
||||
}
|
||||
|
||||
fn write_attrs(&mut self, style: &Style) {
|
||||
match style.stroke {
|
||||
Some(c) => {
|
||||
let _ = write!(self.body, " stroke=\"{}\"", rgb_hex(c));
|
||||
if c.a != 255 {
|
||||
let _ = write!(self.body, " stroke-opacity=\"{:.4}\"", c.a as f64 / 255.0);
|
||||
}
|
||||
let _ = write!(
|
||||
self.body,
|
||||
" stroke-width=\"{:.4}\" stroke-linecap=\"round\" stroke-linejoin=\"round\"",
|
||||
style.stroke_width_mm.max(0.0)
|
||||
);
|
||||
}
|
||||
None => self.body.push_str(" stroke=\"none\""),
|
||||
}
|
||||
match style.fill {
|
||||
Some(c) => {
|
||||
let _ = write!(self.body, " fill=\"{}\"", rgb_hex(c));
|
||||
if c.a != 255 {
|
||||
let _ = write!(self.body, " fill-opacity=\"{:.4}\"", c.a as f64 / 255.0);
|
||||
}
|
||||
}
|
||||
None => self.body.push_str(" fill=\"none\""),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rgb_hex(c: Rgba) -> String {
|
||||
format!("#{:02x}{:02x}{:02x}", c.r, c.g, c.b)
|
||||
}
|
||||
|
||||
impl Sink for SvgSink {
|
||||
fn begin_layer(&mut self, layer: &LayerName) {
|
||||
if self.layer_open {
|
||||
self.body.push_str("</g>\n");
|
||||
}
|
||||
let _ = write!(self.body, "<g data-layer=\"{}\">\n", escape_xml(layer.as_str()));
|
||||
self.active_layer = Some(layer.0.clone());
|
||||
self.layer_open = true;
|
||||
}
|
||||
|
||||
fn end_layer(&mut self, _layer: &LayerName) {
|
||||
if self.layer_open {
|
||||
self.body.push_str("</g>\n");
|
||||
self.layer_open = false;
|
||||
self.active_layer = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn line(&mut self, a: Point, b: Point, style: &Style) {
|
||||
let _ = write!(
|
||||
self.body,
|
||||
"<line x1=\"{:.4}\" y1=\"{:.4}\" x2=\"{:.4}\" y2=\"{:.4}\"",
|
||||
a.x, a.y, b.x, b.y
|
||||
);
|
||||
self.write_attrs(style);
|
||||
self.body.push_str("/>\n");
|
||||
}
|
||||
|
||||
fn arc(&mut self, arc: Arc, style: &Style) {
|
||||
let Some((_, r)) = arc.centre_and_radius() else {
|
||||
self.line(arc.start, arc.end, style);
|
||||
return;
|
||||
};
|
||||
let sweep = if arc.sweep_sign() > 0.0 { 1 } else { 0 };
|
||||
let _ = write!(
|
||||
self.body,
|
||||
"<path d=\"M {:.4} {:.4} A {:.4} {:.4} 0 0 {} {:.4} {:.4}\"",
|
||||
arc.start.x, arc.start.y, r, r, sweep, arc.end.x, arc.end.y
|
||||
);
|
||||
self.write_attrs(style);
|
||||
self.body.push_str("/>\n");
|
||||
}
|
||||
|
||||
fn circle(&mut self, centre: Point, radius: f64, style: &Style) {
|
||||
let _ = write!(
|
||||
self.body,
|
||||
"<circle cx=\"{:.4}\" cy=\"{:.4}\" r=\"{:.4}\"",
|
||||
centre.x, centre.y, radius
|
||||
);
|
||||
self.write_attrs(style);
|
||||
self.body.push_str("/>\n");
|
||||
}
|
||||
|
||||
fn polyline(&mut self, points: &[Point], closed: bool, style: &Style) {
|
||||
if points.is_empty() { return; }
|
||||
let tag = if closed { "polygon" } else { "polyline" };
|
||||
let _ = write!(self.body, "<{tag} points=\"");
|
||||
for (i, p) in points.iter().enumerate() {
|
||||
if i > 0 { self.body.push(' '); }
|
||||
let _ = write!(self.body, "{:.4},{:.4}", p.x, p.y);
|
||||
}
|
||||
self.body.push('"');
|
||||
self.write_attrs(style);
|
||||
self.body.push_str("/>\n");
|
||||
}
|
||||
|
||||
fn polygon(&mut self, points: &[Point], style: &Style) {
|
||||
self.polyline(points, true, style);
|
||||
}
|
||||
|
||||
fn rect(&mut self, a: Point, b: Point, style: &Style) {
|
||||
let (x, w) = if a.x <= b.x { (a.x, b.x - a.x) } else { (b.x, a.x - b.x) };
|
||||
let (y, h) = if a.y <= b.y { (a.y, b.y - a.y) } else { (b.y, a.y - b.y) };
|
||||
let _ = write!(
|
||||
self.body,
|
||||
"<rect x=\"{:.4}\" y=\"{:.4}\" width=\"{:.4}\" height=\"{:.4}\"",
|
||||
x, y, w, h
|
||||
);
|
||||
self.write_attrs(style);
|
||||
self.body.push_str("/>\n");
|
||||
}
|
||||
|
||||
fn text(
|
||||
&mut self,
|
||||
text: &str,
|
||||
pos: Point,
|
||||
rotation_deg: f64,
|
||||
effects: &TextEffects,
|
||||
colour: Rgba,
|
||||
) {
|
||||
if effects.hide || text.is_empty() { return; }
|
||||
let anchor = match effects.justify_h {
|
||||
JustifyH::Left => "start",
|
||||
JustifyH::Centre => "middle",
|
||||
JustifyH::Right => "end",
|
||||
};
|
||||
let baseline = match effects.justify_v {
|
||||
JustifyV::Top => "hanging",
|
||||
JustifyV::Centre => "middle",
|
||||
JustifyV::Bottom => "alphabetic",
|
||||
};
|
||||
let fill_hex = rgb_hex(colour);
|
||||
let alpha = colour.a as f64 / 255.0;
|
||||
let font_size = effects.size_mm.1.max(0.01);
|
||||
let stretch = if effects.size_mm.0 > 0.0 && effects.size_mm.1 > 0.0 {
|
||||
effects.size_mm.0 / effects.size_mm.1
|
||||
} else { 1.0 };
|
||||
|
||||
let escaped = escape_xml(text);
|
||||
let weight = if effects.bold { "bold" } else { "normal" };
|
||||
let style = if effects.italic { "italic" } else { "normal" };
|
||||
|
||||
let rot = rotation_deg;
|
||||
let scale_x = if effects.mirror { -stretch } else { stretch };
|
||||
|
||||
let _ = write!(
|
||||
self.body,
|
||||
"<g transform=\"translate({:.4} {:.4}) rotate({:.4}) scale({:.4} 1)\">\
|
||||
<text x=\"0\" y=\"0\" font-family=\"monospace\" font-size=\"{:.4}\" \
|
||||
text-anchor=\"{}\" dominant-baseline=\"{}\" font-weight=\"{}\" \
|
||||
font-style=\"{}\" fill=\"{}\"",
|
||||
pos.x, pos.y, rot, scale_x, font_size, anchor, baseline, weight, style, fill_hex
|
||||
);
|
||||
if alpha < 1.0 {
|
||||
let _ = write!(self.body, " fill-opacity=\"{:.4}\"", alpha);
|
||||
}
|
||||
let _ = write!(self.body, ">{}</text></g>\n", escaped);
|
||||
}
|
||||
}
|
||||
|
||||
fn escape_xml(s: &str) -> String {
|
||||
let mut out = String::with_capacity(s.len());
|
||||
for ch in s.chars() {
|
||||
match ch {
|
||||
'<' => out.push_str("<"),
|
||||
'>' => out.push_str(">"),
|
||||
'&' => out.push_str("&"),
|
||||
'"' => out.push_str("""),
|
||||
_ => out.push(ch),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
Loading…
Reference in New Issue