use super::markers::{emit_marker, line_inset, marker_anchor};
use super::rules::{PAINT_PROPS, RuleSet};
use super::values::{attr_or_var, dasharray_value, escape_xml, format_value, num, wrap_font};
use crate::Options;
use crate::layout::{Airwire, RoutedText, RoutedWire};
use crate::resolve::{AttrMap, MarkerKind, VarTable};
use std::fmt::Write;
pub fn radius_cap(w: &RoutedWire, vars: &VarTable) -> f64 {
w.attrs
.number("clearance")
.or_else(|| vars.get("clearance").and_then(|e| e.value.as_number()))
.unwrap_or(16.0)
}
pub fn render_wire(
out: &mut String,
w: &RoutedWire,
targets: &[f64],
vars: &VarTable,
ruleset: &RuleSet,
opts: &Options,
) {
if w.path.len() < 2 {
return;
}
let stroke = attr_or_var(&w.attrs, "stroke", "stroke", vars, opts);
let thickness = w.attrs.number("thickness").unwrap_or(1.0);
let mut wire_classes = vec!["lini-wire".to_string()];
wire_classes.extend(w.applied_styles.iter().map(|s| format!("lini-style-{}", s)));
let mut decls: Vec<(&str, String)> = Vec::new();
for (lini, css) in PAINT_PROPS {
let Some(v) = w.attrs.get(lini) else {
continue;
};
let formatted = format_value(v, vars, opts);
if ruleset.provided(&wire_classes, css) != Some(formatted.as_str()) {
decls.push((css, formatted));
}
}
if w.attrs.get("line").is_some() {
let dash = dasharray_value(&w.attrs, thickness);
let value = if dash.is_empty() {
"none".to_string()
} else {
dash
};
if ruleset.provided(&wire_classes, "stroke-dasharray") != Some(value.as_str()) {
decls.push(("stroke-dasharray", value));
}
}
let style_attr = if decls.is_empty() {
String::new()
} else {
let body: Vec<String> = decls.iter().map(|(p, v)| format!("{}: {}", p, v)).collect();
format!(r#" style="{}""#, body.join("; "))
};
writeln!(
out,
r#" <g class="{}"{} data-from="{}" data-to="{}">"#,
wire_classes.join(" "),
style_attr,
escape_xml(&w.data_from),
escape_xml(&w.data_to),
)
.unwrap();
let drawn = shorten_for_markers(&w.path, &w.markers, thickness);
let d = rounded_d(&drawn, targets);
writeln!(out, r#" <path d="{d}"/>"#).unwrap();
if w.markers.start != MarkerKind::None
&& let Some((tip, dir)) = marker_anchor(w.path[1], w.path[0], false)
{
emit_marker(out, " ", w.markers.start, tip, dir, &stroke, thickness);
}
if w.markers.end != MarkerKind::None {
let n = w.path.len();
if let Some((tip, dir)) = marker_anchor(w.path[n - 2], w.path[n - 1], false) {
emit_marker(out, " ", w.markers.end, tip, dir, &stroke, thickness);
}
}
for t in &w.texts {
render_wire_text(out, t, vars, opts);
}
out.push_str(" </g>\n");
}
pub fn render_airwire(out: &mut String, a: &Airwire, vars: &VarTable, opts: &Options) {
let none = AttrMap::default();
let stroke = attr_or_var(&none, "stroke", "airwire", vars, opts);
let bg = attr_or_var(&none, "fill", "bg", vars, opts);
writeln!(
out,
r#" <g class="lini-airwire" data-from="{}" data-to="{}">"#,
escape_xml(&a.data_from),
escape_xml(&a.data_to),
)
.unwrap();
writeln!(
out,
r#" <path d="M {} {} L {} {}" fill="none" stroke="{stroke}" stroke-width="1.5" stroke-dasharray="6,4"/>"#,
num(a.from.0),
num(a.from.1),
num(a.to.0),
num(a.to.1),
)
.unwrap();
let (mx, my) = ((a.from.0 + a.to.0) / 2.0, (a.from.1 + a.to.1) / 2.0);
writeln!(
out,
r#" <path d="M {} {} L {} {} L {} {} Z" fill="{bg}" stroke="{stroke}" stroke-width="1.5" stroke-linejoin="round"/>"#,
num(mx),
num(my - 6.5),
num(mx + 7.0),
num(my + 5.5),
num(mx - 7.0),
num(my + 5.5),
)
.unwrap();
writeln!(
out,
r#" <path d="M {mx} {} L {mx} {}" stroke="{stroke}" stroke-width="1.6" stroke-linecap="round"/>"#,
num(my - 2.5),
num(my + 1.0),
mx = num(mx),
)
.unwrap();
writeln!(
out,
r#" <circle cx="{}" cy="{}" r="0.9" fill="{stroke}"/>"#,
num(mx),
num(my + 3.6),
)
.unwrap();
out.push_str(" </g>\n");
}
fn rounded_d(pts: &[(f64, f64)], targets: &[f64]) -> String {
let mut d = format!("M {} {}", num(pts[0].0), num(pts[0].1));
for i in 1..pts.len() - 1 {
let (a, b, c) = (pts[i - 1], pts[i], pts[i + 1]);
let (in_dx, in_dy) = (b.0 - a.0, b.1 - a.1);
let (out_dx, out_dy) = (c.0 - b.0, c.1 - b.1);
let in_len = in_dx.abs() + in_dy.abs();
let out_len = out_dx.abs() + out_dy.abs();
let cap = targets.get(i - 1).copied().unwrap_or(0.0);
let r = cap.min(in_len / 2.0).min(out_len / 2.0);
let cross = in_dx * out_dy - in_dy * out_dx;
if r < 0.5 || cross == 0.0 {
write!(d, " L {} {}", num(b.0), num(b.1)).unwrap();
continue;
}
let enter = (b.0 - in_dx / in_len * r, b.1 - in_dy / in_len * r);
let exit = (b.0 + out_dx / out_len * r, b.1 + out_dy / out_len * r);
let sweep = if cross > 0.0 { 1 } else { 0 };
write!(
d,
" L {} {} A {} {} 0 0 {} {} {}",
num(enter.0),
num(enter.1),
num(r),
num(r),
sweep,
num(exit.0),
num(exit.1),
)
.unwrap();
}
let last = pts[pts.len() - 1];
write!(d, " L {} {}", num(last.0), num(last.1)).unwrap();
d
}
struct Corner {
wire: usize,
slot: usize,
quad: (i8, i8),
diag: f64,
proj: f64,
ceil: f64,
cap: f64,
}
pub fn fillet_targets(polys: &[&[(f64, f64)]], caps: &[f64]) -> Vec<Vec<f64>> {
const EPS: f64 = 1e-6;
let mut out: Vec<Vec<f64>> = polys
.iter()
.map(|p| vec![0.0; p.len().saturating_sub(2)])
.collect();
let mut corners: Vec<Corner> = Vec::new();
for (wi, poly) in polys.iter().enumerate() {
for k in 1..poly.len().saturating_sub(1) {
let (a, v, b) = (poly[k - 1], poly[k], poly[k + 1]);
let (ix, iy) = (v.0 - a.0, v.1 - a.1);
let (ox, oy) = (b.0 - v.0, b.1 - v.1);
if ix * oy - iy * ox == 0.0 {
continue; }
let unit = |x: f64, y: f64| {
let l = x.abs() + y.abs();
(x / l, y / l)
};
let (ux, uy) = unit(ix, iy);
let (wx, wy) = unit(ox, oy);
let quad = ((wx - ux).signum() as i8, (wy - uy).signum() as i8);
let in_len = ix.abs() + iy.abs();
let out_len = ox.abs() + oy.abs();
let mut ceil = (in_len / 2.0).min(out_len / 2.0);
for (wj, other) in polys.iter().enumerate() {
if wj == wi {
continue;
}
for s in other.windows(2) {
for leg in [[a, v], [v, b]] {
if let Some(at) = crate::layout::cross(&leg, s) {
let t = (at.0 - v.0).abs() + (at.1 - v.1).abs();
ceil = ceil.min(t);
}
}
}
}
corners.push(Corner {
wire: wi,
slot: k - 1,
quad,
diag: if quad.0 as f64 * quad.1 as f64 > 0.0 {
v.0 - v.1
} else {
v.0 + v.1
},
proj: v.0 * quad.0 as f64 + v.1 * quad.1 as f64,
ceil,
cap: caps[wi],
});
}
}
corners.sort_by(|a, b| {
a.quad
.cmp(&b.quad)
.then(a.diag.total_cmp(&b.diag))
.then(a.wire.cmp(&b.wire))
.then(a.slot.cmp(&b.slot))
});
let mut i = 0;
while i < corners.len() {
let mut j = i + 1;
while j < corners.len()
&& corners[j].quad == corners[i].quad
&& (corners[j].diag - corners[j - 1].diag).abs() <= EPS
{
j += 1;
}
let mut cluster: Vec<&Corner> = corners[i..j].iter().collect();
cluster.sort_by(|a, b| {
b.proj
.total_cmp(&a.proj)
.then(a.wire.cmp(&b.wire))
.then(a.slot.cmp(&b.slot))
});
let mut prev: Option<(&Corner, f64)> = None;
for c in cluster {
let r = match prev {
Some((p, pr)) if (p.proj - c.proj) / 2.0 <= 2.0 * c.cap.max(p.cap) + EPS => {
(pr + (p.proj - c.proj) / 2.0).min(c.ceil)
}
_ => c.cap.min(c.ceil),
};
out[c.wire][c.slot] = r;
prev = Some((c, r));
}
i = j;
}
out
}
fn shorten_for_markers(
path: &[(f64, f64)],
markers: &crate::resolve::Markers,
thickness: f64,
) -> Vec<(f64, f64)> {
let mut p = path.to_vec();
if p.len() < 2 {
return p;
}
if markers.end != MarkerKind::None {
let n = p.len();
if let Some(q) = pulled_back(p[n - 2], p[n - 1], line_inset(markers.end, thickness)) {
p[n - 1] = q;
}
}
if markers.start != MarkerKind::None
&& let Some(q) = pulled_back(p[1], p[0], line_inset(markers.start, thickness))
{
p[0] = q;
}
p
}
fn pulled_back(inner: (f64, f64), endpoint: (f64, f64), amount: f64) -> Option<(f64, f64)> {
let (dx, dy) = (endpoint.0 - inner.0, endpoint.1 - inner.1);
let len = (dx * dx + dy * dy).sqrt();
if len <= amount + 0.5 {
return None;
}
Some((
endpoint.0 - dx / len * amount,
endpoint.1 - dy / len * amount,
))
}
fn render_wire_text(out: &mut String, t: &RoutedText, vars: &VarTable, opts: &Options) {
let size = t.attrs.number("text-size").unwrap_or(11.0);
let fill = if let Some(v) = t.attrs.get("fill") {
format_value(v, vars, opts)
} else if let Some(v) = t.attrs.get("color") {
format_value(v, vars, opts)
} else {
"currentColor".to_string()
};
let font = attr_or_var(&t.attrs, "font", "font", vars, opts);
let halo = attr_or_var(&t.attrs, "halo", "bg", vars, opts);
let halo_w = (size * 0.4).max(2.0);
let (x, y) = t.position;
writeln!(
out,
r#" <text x="{}" y="{}" text-anchor="middle" dominant-baseline="central" font-size="{}" font-family={} fill="{}" paint-order="stroke" stroke="{}" stroke-width="{}" stroke-linejoin="round">{}</text>"#,
num(x),
num(y),
num(size),
wrap_font(&font),
fill,
halo,
num(halo_w),
escape_xml(&t.content),
)
.unwrap();
}
#[cfg(test)]
mod tests {
use super::fillet_targets;
fn ell(v: (f64, f64), len: f64) -> Vec<(f64, f64)> {
vec![(v.0 - len, v.1), v, (v.0, v.1 + len)]
}
#[test]
fn nested_corners_round_concentrically() {
let (a, b, c) = (
ell((0.0, 0.0), 100.0),
ell((8.0, -8.0), 100.0),
ell((16.0, -16.0), 100.0),
);
let t = fillet_targets(&[&a, &b, &c], &[8.0; 3]);
assert_eq!((t[0][0], t[1][0], t[2][0]), (8.0, 16.0, 24.0));
}
#[test]
fn opposite_travel_still_nests() {
let a = ell((0.0, 0.0), 100.0);
let b = vec![(8.0, 92.0), (8.0, -8.0), (-92.0, -8.0)];
let t = fillet_targets(&[&a, &b], &[8.0; 2]);
assert_eq!((t[0][0], t[1][0]), (8.0, 16.0));
}
#[test]
fn a_far_corner_on_the_same_diagonal_is_not_nested() {
let (a, b) = (ell((0.0, 0.0), 100.0), ell((80.0, -80.0), 100.0));
let t = fillet_targets(&[&a, &b], &[8.0; 2]);
assert_eq!((t[0][0], t[1][0]), (8.0, 8.0));
}
#[test]
fn a_crossing_on_a_leg_caps_the_radius() {
let a = ell((0.0, 0.0), 100.0);
let b = vec![(-5.0, -50.0), (-5.0, 50.0)];
let t = fillet_targets(&[&a, &b], &[8.0; 2]);
assert_eq!(t[0][0], 5.0);
}
#[test]
fn float_dust_on_the_diagonal_never_reorders_a_nest() {
let corner =
|v: (f64, f64), down: f64| vec![(-5.775000000000006, v.1), v, (v.0, v.1 + down)];
let outer = corner((-78.22500000000001, -64.6), 52.1);
let inner = corner((-62.22500000000001, -48.6), 81.7);
let middle = corner((-70.22500000000001, -56.6), 156.0);
let t = fillet_targets(&[&outer, &inner, &middle], &[8.0; 3]);
for (got, want) in [(t[1][0], 8.0), (t[2][0], 16.0), (t[0][0], 24.0)] {
assert!((got - want).abs() < 1e-9, "{got} != {want}");
}
}
#[test]
fn short_legs_cap_a_nested_radius_without_unnesting_the_rest() {
let (a, b, c) = (
ell((0.0, 0.0), 100.0),
vec![(-92.0, -8.0), (8.0, -8.0), (8.0, 12.0)],
ell((16.0, -16.0), 100.0),
);
let t = fillet_targets(&[&a, &b, &c], &[8.0; 3]);
assert_eq!((t[0][0], t[1][0], t[2][0]), (8.0, 10.0, 18.0));
}
}