app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Drive your own KiCad from an AI, in the background: open every editor, read the whole board as data (the EDA Bridge Standard board model), stack-up, part heights, netlists, place, route, pour, DRC and exports.
master
John Lauer
1.0.61: Every Konnect v0.13.0 capability, 211 verbs (was 133; docs/konnect-gap.md maps all 228 Konnect tools). Schemati…
baad156
14d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600
//! SVG artwork to filled polygons in millimetres, in pure Rust: paths (every command,
//! curves and arcs flattened), rect, circle, ellipse, polygon and polyline, nested group
//! transforms, the root viewBox and its physical width/height. Holes (a subpath inside
//! another, by containment depth, which is what both fill rules give for ordinary logo
//! art) are joined to their outline by a zero-width cut, the way KiCad's own fractured
//! polygons are, because a KiCad `gr_poly` has no holes.
use quick_xml::events::Event;
use quick_xml::Reader;
pub type Pt = (f64, f64);
/// 2D affine matrix [a b c d e f]: x' = a x + c y + e, y' = b x + d y + f.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Mat(pub [f64; 6]);
impl Mat {
pub const ID: Mat = Mat([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
pub fn mul(&self, o: &Mat) -> Mat {
let [a, b, c, d, e, f] = self.0;
let [a2, b2, c2, d2, e2, f2] = o.0;
Mat([a * a2 + c * b2, b * a2 + d * b2, a * c2 + c * d2, b * c2 + d * d2, a * e2 + c * f2 + e, b * e2 + d * f2 + f])
}
pub fn apply(&self, p: Pt) -> Pt {
let [a, b, c, d, e, f] = self.0;
(a * p.0 + c * p.1 + e, b * p.0 + d * p.1 + f)
}
fn scale_factor(&self) -> f64 {
let [a, b, c, d, _, _] = self.0;
((a * d - b * c).abs()).sqrt().max(1e-12)
}
}
fn numbers(s: &str) -> Vec<f64> {
let mut out = Vec::new();
let b = s.as_bytes();
let mut i = 0;
while i < b.len() {
let c = b[i] as char;
if c.is_ascii_digit() || c == '-' || c == '+' || c == '.' {
let start = i;
let mut seen_dot = c == '.';
let mut seen_e = false;
i += 1;
while i < b.len() {
let d = b[i] as char;
if d.is_ascii_digit() {
i += 1;
} else if d == '.' && !seen_dot && !seen_e {
seen_dot = true;
i += 1;
} else if (d == 'e' || d == 'E') && !seen_e {
seen_e = true;
i += 1;
if i < b.len() && (b[i] == b'-' || b[i] == b'+') {
i += 1;
}
} else {
break;
}
}
if let Ok(v) = s[start..i].parse::<f64>() {
out.push(v);
}
} else {
i += 1;
}
}
out
}
/// An SVG `transform` attribute.
pub fn parse_transform(s: &str) -> Mat {
let mut m = Mat::ID;
let mut rest = s;
while let Some(open) = rest.find('(') {
let name = rest[..open].trim().trim_start_matches(',').trim();
let Some(close) = rest[open..].find(')') else { break };
let args = numbers(&rest[open + 1..open + close]);
let t = match (name, args.as_slice()) {
("matrix", [a, b, c, d, e, f]) => Mat([*a, *b, *c, *d, *e, *f]),
("translate", [x]) => Mat([1.0, 0.0, 0.0, 1.0, *x, 0.0]),
("translate", [x, y]) => Mat([1.0, 0.0, 0.0, 1.0, *x, *y]),
("scale", [s]) => Mat([*s, 0.0, 0.0, *s, 0.0, 0.0]),
("scale", [sx, sy]) => Mat([*sx, 0.0, 0.0, *sy, 0.0, 0.0]),
("rotate", [a]) => {
let (s, c) = a.to_radians().sin_cos();
Mat([c, s, -s, c, 0.0, 0.0])
}
("rotate", [a, cx, cy]) => {
let (s, c) = a.to_radians().sin_cos();
Mat([1.0, 0.0, 0.0, 1.0, *cx, *cy]).mul(&Mat([c, s, -s, c, 0.0, 0.0])).mul(&Mat([1.0, 0.0, 0.0, 1.0, -cx, -cy]))
}
("skewX", [a]) => Mat([1.0, 0.0, a.to_radians().tan(), 1.0, 0.0, 0.0]),
("skewY", [a]) => Mat([1.0, a.to_radians().tan(), 0.0, 1.0, 0.0, 0.0]),
_ => Mat::ID,
};
m = m.mul(&t);
rest = &rest[open + close + 1..];
}
m
}
fn steps_for(len: f64, tol: f64) -> usize {
((len / tol).sqrt().ceil() as usize).clamp(4, 96)
}
fn cubic(p0: Pt, p1: Pt, p2: Pt, p3: Pt, tol: f64, out: &mut Vec<Pt>) {
let len = dist(p0, p1) + dist(p1, p2) + dist(p2, p3);
let n = steps_for(len, tol);
for i in 1..=n {
let t = i as f64 / n as f64;
let u = 1.0 - t;
let (a, b, c, d) = (u * u * u, 3.0 * u * u * t, 3.0 * u * t * t, t * t * t);
out.push((a * p0.0 + b * p1.0 + c * p2.0 + d * p3.0, a * p0.1 + b * p1.1 + c * p2.1 + d * p3.1));
}
}
fn quad(p0: Pt, p1: Pt, p2: Pt, tol: f64, out: &mut Vec<Pt>) {
let n = steps_for(dist(p0, p1) + dist(p1, p2), tol);
for i in 1..=n {
let t = i as f64 / n as f64;
let u = 1.0 - t;
out.push((u * u * p0.0 + 2.0 * u * t * p1.0 + t * t * p2.0, u * u * p0.1 + 2.0 * u * t * p1.1 + t * t * p2.1));
}
}
fn dist(a: Pt, b: Pt) -> f64 {
((a.0 - b.0).powi(2) + (a.1 - b.1).powi(2)).sqrt()
}
/// SVG elliptical arc (endpoint parameterisation, F.6.5 of the SVG spec), flattened.
#[allow(clippy::too_many_arguments)]
fn arc(p0: Pt, rx: f64, ry: f64, phi_deg: f64, large: bool, sweep: bool, p1: Pt, tol: f64, out: &mut Vec<Pt>) {
let (mut rx, mut ry) = (rx.abs(), ry.abs());
if rx < 1e-12 || ry < 1e-12 || dist(p0, p1) < 1e-12 {
out.push(p1);
return;
}
let (s, c) = phi_deg.to_radians().sin_cos();
let (dx, dy) = ((p0.0 - p1.0) / 2.0, (p0.1 - p1.1) / 2.0);
let (x1, y1) = (c * dx + s * dy, -s * dx + c * dy);
let lam = (x1 * x1) / (rx * rx) + (y1 * y1) / (ry * ry);
if lam > 1.0 {
rx *= lam.sqrt();
ry *= lam.sqrt();
}
let num = rx * rx * ry * ry - rx * rx * y1 * y1 - ry * ry * x1 * x1;
let den = rx * rx * y1 * y1 + ry * ry * x1 * x1;
let mut k = (num / den).max(0.0).sqrt();
if large == sweep {
k = -k;
}
let (cx1, cy1) = (k * rx * y1 / ry, -k * ry * x1 / rx);
let (cx, cy) = (c * cx1 - s * cy1 + (p0.0 + p1.0) / 2.0, s * cx1 + c * cy1 + (p0.1 + p1.1) / 2.0);
let ang = |ux: f64, uy: f64, vx: f64, vy: f64| {
let d = (ux * vx + uy * vy) / ((ux * ux + uy * uy).sqrt() * (vx * vx + vy * vy).sqrt());
let a = d.clamp(-1.0, 1.0).acos();
if ux * vy - uy * vx < 0.0 { -a } else { a }
};
let t1 = ang(1.0, 0.0, (x1 - cx1) / rx, (y1 - cy1) / ry);
let mut dt = ang((x1 - cx1) / rx, (y1 - cy1) / ry, (-x1 - cx1) / rx, (-y1 - cy1) / ry);
if !sweep && dt > 0.0 {
dt -= 2.0 * std::f64::consts::PI;
} else if sweep && dt < 0.0 {
dt += 2.0 * std::f64::consts::PI;
}
let n = steps_for(rx.max(ry) * dt.abs(), tol).max((dt.abs() / 0.2).ceil() as usize).min(180);
for i in 1..=n {
let t = t1 + dt * i as f64 / n as f64;
let (st, ct) = t.sin_cos();
out.push((c * rx * ct - s * ry * st + cx, s * rx * ct + c * ry * st + cy));
}
if let Some(l) = out.last_mut() {
*l = p1;
}
}
/// A path's `d` as closed rings (every subpath is closed for filling).
pub fn path_rings(d: &str, tol: f64) -> Vec<Vec<Pt>> {
let mut rings: Vec<Vec<Pt>> = Vec::new();
let mut cur: Vec<Pt> = Vec::new();
let (mut p, mut start) = ((0.0, 0.0), (0.0, 0.0));
let mut last_ctrl: Option<(char, Pt)> = None;
// tokenise into commands and their numbers
let mut cmds: Vec<(char, Vec<f64>)> = Vec::new();
let mut buf = String::new();
let mut cmd: Option<char> = None;
for ch in d.chars() {
if ch.is_ascii_alphabetic() && ch != 'e' && ch != 'E' {
if let Some(c) = cmd {
cmds.push((c, numbers(&buf)));
}
buf.clear();
cmd = Some(ch);
} else {
buf.push(ch);
}
}
if let Some(c) = cmd {
cmds.push((c, numbers(&buf)));
}
let flush = |cur: &mut Vec<Pt>, rings: &mut Vec<Vec<Pt>>| {
if cur.len() >= 3 {
rings.push(std::mem::take(cur));
} else {
cur.clear();
}
};
for (c, args) in cmds {
let rel = c.is_ascii_lowercase();
let base = |p: Pt, x: f64, y: f64| if rel { (p.0 + x, p.1 + y) } else { (x, y) };
match c.to_ascii_uppercase() {
'M' => {
flush(&mut cur, &mut rings);
for (i, xy) in args.chunks(2).enumerate() {
if xy.len() < 2 {
break;
}
p = base(p, xy[0], xy[1]);
if i == 0 {
start = p;
}
cur.push(p);
}
last_ctrl = None;
}
'L' => {
for xy in args.chunks(2).filter(|c| c.len() == 2) {
p = base(p, xy[0], xy[1]);
cur.push(p);
}
last_ctrl = None;
}
'H' => {
for x in &args {
p = (if rel { p.0 + x } else { *x }, p.1);
cur.push(p);
}
last_ctrl = None;
}
'V' => {
for y in &args {
p = (p.0, if rel { p.1 + y } else { *y });
cur.push(p);
}
last_ctrl = None;
}
'C' => {
for a in args.chunks(6).filter(|c| c.len() == 6) {
let (c1, c2, e) = (base(p, a[0], a[1]), base(p, a[2], a[3]), base(p, a[4], a[5]));
cubic(p, c1, c2, e, tol, &mut cur);
last_ctrl = Some(('C', c2));
p = e;
}
}
'S' => {
for a in args.chunks(4).filter(|c| c.len() == 4) {
let c1 = match last_ctrl {
Some(('C', q)) => (2.0 * p.0 - q.0, 2.0 * p.1 - q.1),
_ => p,
};
let (c2, e) = (base(p, a[0], a[1]), base(p, a[2], a[3]));
cubic(p, c1, c2, e, tol, &mut cur);
last_ctrl = Some(('C', c2));
p = e;
}
}
'Q' => {
for a in args.chunks(4).filter(|c| c.len() == 4) {
let (c1, e) = (base(p, a[0], a[1]), base(p, a[2], a[3]));
quad(p, c1, e, tol, &mut cur);
last_ctrl = Some(('Q', c1));
p = e;
}
}
'T' => {
for a in args.chunks(2).filter(|c| c.len() == 2) {
let c1 = match last_ctrl {
Some(('Q', q)) => (2.0 * p.0 - q.0, 2.0 * p.1 - q.1),
_ => p,
};
let e = base(p, a[0], a[1]);
quad(p, c1, e, tol, &mut cur);
last_ctrl = Some(('Q', c1));
p = e;
}
}
'A' => {
for a in args.chunks(7).filter(|c| c.len() == 7) {
let e = base(p, a[5], a[6]);
arc(p, a[0], a[1], a[2], a[3] != 0.0, a[4] != 0.0, e, tol, &mut cur);
p = e;
}
last_ctrl = None;
}
'Z' => {
flush(&mut cur, &mut rings);
p = start;
cur.push(p);
last_ctrl = None;
}
_ => {}
}
}
flush(&mut cur, &mut rings);
for r in rings.iter_mut() {
dedup_ring(r);
}
rings.retain(|r| r.len() >= 3 && area(r).abs() > 1e-12);
rings
}
fn dedup_ring(r: &mut Vec<Pt>) {
r.dedup_by(|a, b| dist(*a, *b) < 1e-9);
while r.len() > 1 && dist(r[0], *r.last().unwrap()) < 1e-9 {
r.pop();
}
}
pub fn area(r: &[Pt]) -> f64 {
let mut s = 0.0;
for i in 0..r.len() {
let (a, b) = (r[i], r[(i + 1) % r.len()]);
s += a.0 * b.1 - b.0 * a.1;
}
s / 2.0
}
fn inside(p: Pt, r: &[Pt]) -> bool {
let mut c = false;
let mut j = r.len() - 1;
for i in 0..r.len() {
let (a, b) = (r[i], r[j]);
if (a.1 > p.1) != (b.1 > p.1) && p.0 < (b.0 - a.0) * (p.1 - a.1) / (b.1 - a.1) + a.0 {
c = !c;
}
j = i;
}
c
}
fn ellipse_ring(cx: f64, cy: f64, rx: f64, ry: f64, tol: f64) -> Vec<Pt> {
let n = steps_for(2.0 * std::f64::consts::PI * rx.max(ry), tol).max(24).min(180);
(0..n).map(|i| {
let t = 2.0 * std::f64::consts::PI * i as f64 / n as f64;
(cx + rx * t.cos(), cy + ry * t.sin())
}).collect()
}
fn unit_mm(v: &str) -> Option<f64> {
let v = v.trim();
let num: String = v.chars().take_while(|c| c.is_ascii_digit() || *c == '.' || *c == '-' || *c == 'e' || *c == '+').collect();
let x: f64 = num.parse().ok()?;
let unit = v[num.len()..].trim();
Some(x * match unit {
"mm" => 1.0,
"cm" => 10.0,
"in" => 25.4,
"pt" => 25.4 / 72.0,
"pc" => 25.4 / 6.0,
"" | "px" => 25.4 / 96.0,
_ => return None,
})
}
/// The parsed artwork: filled rings in SVG user units with the user-unit to mm factor
/// and the artwork's bounding box.
#[derive(Debug, Clone)]
pub struct Art {
pub rings: Vec<Vec<Pt>>,
pub mm_per_unit: f64,
pub skipped: Vec<String>,
}
fn attr(e: &quick_xml::events::BytesStart, name: &str) -> Option<String> {
e.attributes().flatten().find(|a| a.key.as_ref() == name.as_bytes()).and_then(|a| a.unescape_value().ok().map(|v| v.to_string()))
}
fn style_prop(style: &str, key: &str) -> Option<String> {
style.split(';').find_map(|kv| {
let (k, v) = kv.split_once(':')?;
(k.trim() == key).then(|| v.trim().to_string())
})
}
/// Parse SVG text into filled rings (user units, root viewBox applied).
pub fn parse_svg(text: &str, tol_mm: f64) -> Result<Art, String> {
let mut reader = Reader::from_str(text);
let mut stack: Vec<(Mat, bool)> = vec![(Mat::ID, true)];
let mut rings = Vec::new();
let mut skipped = Vec::new();
let mut mm_per_unit = 25.4 / 96.0;
let mut seen_root = false;
let mut defs_depth = 0usize;
loop {
let ev = reader.read_event().map_err(|e| format!("SVG parse error: {e}"))?;
let (e, empty) = match &ev {
Event::Start(e) => (e.clone(), false),
Event::Empty(e) => (e.clone(), true),
Event::End(e) => {
let name = String::from_utf8_lossy(e.name().as_ref()).to_string();
if name == "defs" || name == "clipPath" || name == "mask" || name == "symbol" {
defs_depth = defs_depth.saturating_sub(1);
}
if stack.len() > 1 {
stack.pop();
}
continue;
}
Event::Eof => break,
_ => continue,
};
let name = String::from_utf8_lossy(e.local_name().as_ref()).to_string();
let (parent, parent_fill) = *stack.last().unwrap();
let mut m = parent;
if let Some(t) = attr(&e, "transform") {
m = m.mul(&parse_transform(&t));
}
let style = attr(&e, "style").unwrap_or_default();
let fill = attr(&e, "fill").or_else(|| style_prop(&style, "fill"));
let filled = match fill.as_deref() {
Some("none") | Some("transparent") => false,
Some(_) => true,
None => parent_fill,
};
if name == "svg" && !seen_root {
seen_root = true;
let vb = attr(&e, "viewBox").map(|v| numbers(&v)).filter(|v| v.len() == 4 && v[2] > 0.0 && v[3] > 0.0);
let w = attr(&e, "width").and_then(|w| unit_mm(&w));
let h = attr(&e, "height").and_then(|h| unit_mm(&h));
if let Some(vb) = &vb {
m = m.mul(&Mat([1.0, 0.0, 0.0, 1.0, -vb[0], -vb[1]]));
mm_per_unit = match (w, h) {
(Some(w), _) => w / vb[2],
(None, Some(h)) => h / vb[3],
_ => 25.4 / 96.0,
};
} else if let Some(w) = w {
let raw = attr(&e, "width").and_then(|w| numbers(&w).first().copied()).unwrap_or(0.0);
if raw > 0.0 {
mm_per_unit = w / raw;
}
}
}
if matches!(name.as_str(), "defs" | "clipPath" | "mask" | "symbol") && !empty {
defs_depth += 1;
}
let tol = tol_mm / mm_per_unit.max(1e-9) / m.scale_factor();
let num = |k: &str| attr(&e, k).and_then(|v| numbers(&v).first().copied());
let mut local: Vec<Vec<Pt>> = Vec::new();
if defs_depth == 0 && filled {
match name.as_str() {
"path" => local = path_rings(&attr(&e, "d").unwrap_or_default(), tol),
"rect" => {
let (x, y, w, h) = (num("x").unwrap_or(0.0), num("y").unwrap_or(0.0), num("width").unwrap_or(0.0), num("height").unwrap_or(0.0));
let rx = num("rx").or(num("ry")).unwrap_or(0.0).min(w / 2.0);
let ry = num("ry").or(num("rx")).unwrap_or(0.0).min(h / 2.0);
if w > 0.0 && h > 0.0 {
if rx > 0.0 && ry > 0.0 {
let d = format!("M{} {} H{} A{rx} {ry} 0 0 1 {} {} V{} A{rx} {ry} 0 0 1 {} {} H{} A{rx} {ry} 0 0 1 {} {} V{} A{rx} {ry} 0 0 1 {} {} Z",
x + rx, y, x + w - rx, x + w, y + ry, y + h - ry, x + w - rx, y + h, x + rx, x, y + h - ry, y + ry, x + rx, y);
local = path_rings(&d, tol);
} else {
local.push(vec![(x, y), (x + w, y), (x + w, y + h), (x, y + h)]);
}
}
}
"circle" => {
let r = num("r").unwrap_or(0.0);
if r > 0.0 {
local.push(ellipse_ring(num("cx").unwrap_or(0.0), num("cy").unwrap_or(0.0), r, r, tol));
}
}
"ellipse" => {
let (rx, ry) = (num("rx").unwrap_or(0.0), num("ry").unwrap_or(0.0));
if rx > 0.0 && ry > 0.0 {
local.push(ellipse_ring(num("cx").unwrap_or(0.0), num("cy").unwrap_or(0.0), rx, ry, tol));
}
}
"polygon" | "polyline" => {
let v = numbers(&attr(&e, "points").unwrap_or_default());
let r: Vec<Pt> = v.chunks(2).filter(|c| c.len() == 2).map(|c| (c[0], c[1])).collect();
if r.len() >= 3 {
local.push(r);
}
}
"text" | "image" | "use" => skipped.push(format!("<{name}> is not imported (convert text to paths first)")),
_ => {}
}
}
for r in local {
rings.push(r.into_iter().map(|p| m.apply(p)).collect::<Vec<_>>());
}
if !empty {
stack.push((m, filled));
}
}
if !seen_root {
return Err("not an SVG document (no <svg> element)".into());
}
Ok(Art { rings, mm_per_unit, skipped })
}
/// Group rings into filled outlines with their holes (by containment depth) and fracture
/// every hole into its outline. Returns simple polygons.
pub fn fill_polygons(rings: &[Vec<Pt>]) -> Vec<Vec<Pt>> {
let n = rings.len();
let depth: Vec<usize> = (0..n).map(|i| (0..n).filter(|&j| j != i && area(&rings[j]).abs() > area(&rings[i]).abs() && inside(rings[i][0], &rings[j])).count()).collect();
let mut out = Vec::new();
for i in 0..n {
if depth[i] % 2 == 1 {
continue;
}
let mut outer = rings[i].clone();
if area(&outer) < 0.0 {
outer.reverse();
}
let mut holes: Vec<Vec<Pt>> = (0..n)
.filter(|&j| depth[j] == depth[i] + 1 && inside(rings[j][0], &rings[i]))
.map(|j| {
let mut h = rings[j].clone();
if area(&h) > 0.0 {
h.reverse();
}
h
})
.collect();
holes.sort_by(|a, b| {
let mx = |r: &Vec<Pt>| r.iter().map(|p| p.0).fold(f64::MIN, f64::max);
mx(b).partial_cmp(&mx(a)).unwrap_or(std::cmp::Ordering::Equal)
});
for h in &holes {
outer = bridge(&outer, h, &holes);
}
out.push(outer);
}
out
}
fn segs_cross(a: Pt, b: Pt, c: Pt, d: Pt) -> bool {
let o = |p: Pt, q: Pt, r: Pt| (q.0 - p.0) * (r.1 - p.1) - (q.1 - p.1) * (r.0 - p.0);
let (d1, d2, d3, d4) = (o(c, d, a), o(c, d, b), o(a, b, c), o(a, b, d));
((d1 > 1e-12 && d2 < -1e-12) || (d1 < -1e-12 && d2 > 1e-12)) && ((d3 > 1e-12 && d4 < -1e-12) || (d3 < -1e-12 && d4 > 1e-12))
}
/// Join `hole` into `outer` through the closest mutually visible vertex pair.
fn bridge(outer: &[Pt], hole: &[Pt], all_holes: &[Vec<Pt>]) -> Vec<Pt> {
let hi = (0..hole.len()).max_by(|&a, &b| hole[a].0.partial_cmp(&hole[b].0).unwrap_or(std::cmp::Ordering::Equal)).unwrap_or(0);
let m = hole[hi];
let visible = |p: Pt| -> bool {
let edges = |r: &[Pt]| (0..r.len()).map(move |i| (r[i], r[(i + 1) % r.len()])).collect::<Vec<_>>();
edges(outer).iter().chain(all_holes.iter().flat_map(|h| edges(h)).collect::<Vec<_>>().iter()).all(|(a, b)| !segs_cross(m, p, *a, *b))
};
let mut order: Vec<usize> = (0..outer.len()).collect();
order.sort_by(|&a, &b| dist(outer[a], m).partial_cmp(&dist(outer[b], m)).unwrap_or(std::cmp::Ordering::Equal));
let oi = order.iter().copied().find(|&i| visible(outer[i])).unwrap_or(order[0]);
let mut r = Vec::with_capacity(outer.len() + hole.len() + 2);
r.extend_from_slice(&outer[..=oi]);
for k in 0..=hole.len() {
r.push(hole[(hi + k) % hole.len()]);
}
r.push(outer[oi]);
r.extend_from_slice(&outer[oi + 1..]);
r
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_square_with_a_hole_becomes_one_fractured_polygon_in_mm() {
let svg = r#"<svg xmlns="http://www.w3.org/2000/svg" width="10mm" height="10mm" viewBox="0 0 100 100">
<path d="M0 0 H100 V100 H0 Z M25 25 v50 h50 v-50 z" fill-rule="evenodd"/>
<g transform="translate(200,0)"><circle cx="10" cy="10" r="5"/></g>
<rect x="0" y="0" width="5" height="5" fill="none"/>
<text>hi</text></svg>"#;
let art = parse_svg(svg, 0.05).unwrap();
assert!((art.mm_per_unit - 0.1).abs() < 1e-12);
assert_eq!(art.rings.len(), 3);
assert!(art.skipped.iter().any(|s| s.contains("text")));
let polys = fill_polygons(&art.rings);
assert_eq!(polys.len(), 2, "the square (with its hole joined) and the circle");
let sq = polys.iter().find(|p| p.len() < 20).unwrap();
assert!((area(sq).abs() - (10000.0 - 2500.0)).abs() < 1e-6, "{}", area(sq));
let circle = polys.iter().find(|p| p.len() >= 20).unwrap();
assert!(circle.iter().all(|p| p.0 > 200.0));
}
#[test]
fn curves_and_arcs_flatten_to_their_ends() {
let r = path_rings("M0 0 C10 0 10 10 0 10 Q-5 5 0 0 Z", 0.01);
assert_eq!(r.len(), 1);
let r = path_rings("M0 0 A5 5 0 0 1 10 0 L10 1 L0 1 Z", 0.01);
let top = r[0].iter().map(|p| p.1).fold(f64::MAX, f64::min);
assert!((top + 5.0).abs() < 0.05, "a half circle above the chord reaches y=-5, got {top}");
assert_eq!(parse_transform("rotate(90)").apply((1.0, 0.0)).1.round(), 1.0);
}
}