use super::super::ir::{Bbox, PlacedNode};
use super::geometry::{self, P, PathSeg, Subpath, arc_center, dist, n};
use crate::error::Error;
use crate::ledger::consts::{BREAK_GAP, CENTER_MARK_OVERHANG};
use crate::resolve::{ResolvedInst, ResolvedValue};
use crate::span::Span;
use super::Segment;
mod clip;
mod viewmap;
#[cfg(test)]
mod tests;
use clip::clip_out;
pub use viewmap::ViewMap;
use viewmap::{build_map, displace, forward};
#[derive(Debug)]
pub struct CutEdge {
pub horizontal: bool,
pub t: f64,
pub lo: f64,
pub hi: f64,
}
struct Group {
a: f64,
b: f64,
vertical: bool,
}
pub(super) fn apply(
inst: &ResolvedInst,
subs: &mut Vec<Subpath>,
scale: f64,
span: Span,
) -> Result<(ViewMap, Vec<CutEdge>), Error> {
let Some(value) = inst.attrs.get("break") else {
return Ok((ViewMap::default(), Vec::new()));
};
let model = geometry::geometry_bbox(&geometry::to_d(subs));
let groups = parse(value, model, scale, span)?;
let view = build_map(&groups, span)?;
let mut cuts = Vec::with_capacity(groups.len() * 2);
for g in &groups {
let (kept, xa, xb) = clip_out(std::mem::take(subs), g.vertical, g.a, g.b, span)?;
*subs = kept;
for (station, crossings) in [(g.a, xa), (g.b, xb)] {
let (Some(lo), Some(hi)) = (
crossings.iter().copied().min_by(f64::total_cmp),
crossings.iter().copied().max_by(f64::total_cmp),
) else {
return Err(Error::at(
span,
format!("'break' at {} misses the profile", n(station / scale)),
));
};
let t = if g.vertical {
forward(&view.y, station)
} else {
forward(&view.x, station)
};
let across = if g.vertical { &view.x } else { &view.y };
cuts.push(CutEdge {
horizontal: g.vertical,
t,
lo: forward(across, lo),
hi: forward(across, hi),
});
}
}
displace(subs, &view);
Ok((view, cuts))
}
fn parse(value: &ResolvedValue, model: Bbox, scale: f64, span: Span) -> Result<Vec<Group>, Error> {
let bad = || {
Error::at(
span,
"'break' takes two stations 'a b' — a < b — and an optional x-axis / y-axis",
)
};
let longer_is_y = model.h() > model.w();
let one = |v: &ResolvedValue| -> Result<Group, Error> {
let ResolvedValue::Tuple(items) = v else {
return Err(bad());
};
let (nums, rest) = match items.as_slice() {
[a, b] => ([a, b], None),
[a, b, axis] => ([a, b], Some(axis)),
_ => return Err(bad()),
};
let (Some(a), Some(b)) = (nums[0].as_number(), nums[1].as_number()) else {
return Err(bad());
};
if a >= b {
return Err(bad());
}
let vertical = match rest {
None => longer_is_y,
Some(ResolvedValue::Ident(s)) if s == "x-axis" => false,
Some(ResolvedValue::Ident(s)) if s == "y-axis" => true,
Some(_) => return Err(bad()),
};
Ok(Group {
a: a * scale,
b: b * scale,
vertical,
})
};
match value {
ResolvedValue::List(items) => items.iter().map(one).collect(),
v => Ok(vec![one(v)?]),
}
}
pub(in crate::layout) fn fill_chrome(children: &mut [PlacedNode], cuts: &[CutEdge]) {
for c in children.iter_mut() {
let Some(ResolvedValue::Tuple(items)) = c.attrs.get("chrome") else {
continue;
};
let [ResolvedValue::Ident(k), ResolvedValue::Number(idx)] = items.as_slice() else {
continue;
};
if k != "break" {
continue;
}
let Some(cut) = cuts.get(*idx as usize) else {
continue;
};
let half = c.attrs.number("stroke-width").unwrap_or(0.0) / 2.0;
let pt = |t: f64, s: f64| if cut.horizontal { (s, t) } else { (t, s) };
let pts = jogged(cut, pt);
let value = ResolvedValue::List(
pts.iter()
.map(|p| {
ResolvedValue::Tuple(vec![
ResolvedValue::Number(p.0),
ResolvedValue::Number(p.1),
])
})
.collect(),
);
c.attrs.insert("points", value);
c.bbox = Bbox::from_points(&pts).inflate(half);
}
}
fn jogged(cut: &CutEdge, pt: impl Fn(f64, f64) -> P) -> Vec<P> {
let m = (cut.lo + cut.hi) / 2.0;
let h = cut.hi - cut.lo;
let jog = (h * 0.28).min(9.0);
let amp = (h * 0.2).min(4.5);
vec![
pt(cut.t, cut.lo - CENTER_MARK_OVERHANG),
pt(cut.t, m - jog),
pt(cut.t + amp, m - jog * 0.15),
pt(cut.t - amp, m + jog * 0.15),
pt(cut.t, m + jog),
pt(cut.t, cut.hi + CENTER_MARK_OVERHANG),
]
}