use crate::canvas::{Fill, Item, Paint, Stroke};
use crate::scene::{Point, Segment};
use crate::tex::Token;
use crate::Error;
use super::arrows::{Curvilinear, EndTips, TipsMode, Widths};
use super::path::{valid_segment, MAX_COORDINATE};
use super::Engine;
use crate::PT;
impl Engine {
pub fn counts_for_size(&self) -> bool {
!self.canvas_frozen && !self.scopes.iter().any(|s| s.clipped)
}
pub fn use_path(&mut self, segments: Vec<Segment>, command: &str) -> Result<(), Error> {
if !segments.iter().all(valid_segment) {
self.warn(format!("{command}: coordinate is not finite or beyond ±{MAX_COORDINATE} cm (division by zero?): path ignored"));
return Ok(());
}
self.use_path_here(segments)
}
fn use_path_here(&mut self, segments: Vec<Segment>) -> Result<(), Error> {
let mut segs = segments;
if let Some(Token::Cs(n)) = self.st.use_saved_path.take() {
if let Some(saved) = self.saved_paths.get(&*n) {
segs = saved.clone();
}
}
if let Some(Token::Cs(n)) = self.st.save_path.take() {
self.saved_paths.insert(n.to_string(), segs.clone());
}
if !self.st.nonlinear.is_empty() {
segs = self.nonlinear_path(&segs)?;
}
if self.st.rounded > 0.0 {
segs = round_corners(&segs, self.st.rounded);
}
let mode = self.st.mode.clone();
if mode.bounding_box {
self.canvas.include_path(&segs);
self.canvas_frozen = true;
} else if self.counts_for_size() {
self.canvas.include_path(&segs);
if mode.draw {
let w = match self.st.double {
Some(_) => 2.0 * self.st.line_width + self.st.double_distance,
None => self.st.line_width,
};
let mut path = crate::canvas::Canvas::default();
path.include_path(&segs);
if let (Some(lo), Some(hi)) = (path.min, path.max) {
self.canvas.include(Point::new(lo.x - w / 2.0, lo.y - w / 2.0));
self.canvas.include(Point::new(hi.x + w / 2.0, hi.y + w / 2.0));
}
}
}
let drawn = segs.iter().any(|s| !matches!(s, Segment::MoveTo(_)));
let functional = if mode.shade && drawn && !mode.pattern { self.functional_cells(&segs) } else { None };
let pattern = mode.pattern || (mode.fill && self.st.fill_pattern && self.st.fill_color.is_none());
let mode = super::state::Mode { shade: mode.shade && functional.is_none(), pattern, ..mode };
let fill = if mode.fill || mode.pattern || mode.shade {
let paint = if mode.pattern {
let name = self.st.pattern_name.clone().unwrap_or_else(|| "north east lines".into());
let color = self.st.pattern_color.unwrap_or(crate::color::Color::BLACK);
self.pattern_paint(&name, color).unwrap_or(Paint::Pattern { name, color })
} else if mode.shade {
Paint::Shading(self.shading_for(&segs))
} else {
Paint::Solid(self.st.fill_paint_color())
};
Some(Fill { paint, opacity: self.st.fill_opacity, even_odd: self.st.even_odd })
} else {
None
};
let solid_under = if mode.pattern && mode.fill && self.st.fill_color.is_some() {
Some(Fill { paint: Paint::Solid(self.st.fill_paint_color()), opacity: self.st.fill_opacity, even_odd: self.st.even_odd })
} else {
None
};
let stroke = mode.draw.then(|| self.stroke());
let fading = match self.st.path_fading.clone().filter(|_| mode.fade) {
Some(f) if self.picture_fadings.contains_key(&f) => self.picture_fading_for(&f, &segs, self.st.fit_fading),
Some(f) => self.fading_for(&f, &segs).map(|mask| Item::Fade { mask }),
None => None,
};
let faded = drawn && (fill.is_some() || stroke.is_some()) && fading.is_some();
let blended = drawn && (fill.is_some() || stroke.is_some()) && self.st.blend.is_some();
if blended {
self.canvas.push(Item::Blend { mode: self.st.blend.clone().unwrap_or_default() });
}
if faded {
self.canvas.push(fading.expect("checked"));
}
let tips_only = stroke.is_none() && fill.is_none() && !mode.clip && matches!(self.st.tips_mode, TipsMode::True | TipsMode::Proper);
let tips = if !segs.is_empty() && !mode.clip && (stroke.is_some() || tips_only) { self.arrow_ends(&mut segs) } else { Tips::default() };
if let Some(cells) = functional {
self.canvas.push(Item::Clip { segments: segs.clone(), even_odd: self.st.even_odd });
self.canvas.items.extend(cells);
self.canvas.push(Item::End);
}
if drawn && (fill.is_some() || stroke.is_some()) {
if let Some(under) = solid_under {
self.canvas.push(Item::Path { segments: segs.clone(), fill: Some(under), stroke: None });
}
match (self.st.double, &stroke) {
(Some(core), Some(s)) => {
let outer = Stroke { width: 2.0 * s.width + self.st.double_distance, ..s.clone() };
let inner = Stroke { width: self.st.double_distance, color: core, ..s.clone() };
self.canvas.push(Item::Path { segments: segs.clone(), fill, stroke: Some(outer) });
self.canvas.push(Item::Path { segments: segs.clone(), fill: None, stroke: Some(inner) });
}
(None, Some(s)) if self.st.inner_line.0 > 0.0 => {
let inner = Stroke { width: self.st.inner_line.0, color: self.st.inner_line.1.unwrap_or(crate::color::Color::WHITE), ..s.clone() };
self.canvas.push(Item::Path { segments: segs.clone(), fill, stroke: Some(s.clone()) });
self.canvas.push(Item::Path { segments: segs.clone(), fill: None, stroke: Some(inner) });
}
_ => self.canvas.push(Item::Path { segments: segs.clone(), fill, stroke }),
}
}
self.push_tips(tips);
if faded {
self.canvas.push(Item::End);
}
if blended {
self.canvas.push(Item::End);
}
if mode.clip {
self.open_in_scope(Item::Clip { segments: segs, even_odd: self.st.even_odd });
if let Some(s) = self.scopes.last_mut() {
s.clipped = true;
}
}
Ok(())
}
pub(super) fn fading_for(&mut self, name: &str, segs: &[Segment]) -> Option<crate::canvas::Shading> {
let mut c = crate::canvas::Canvas::default();
c.include_path(segs);
let (lo, hi) = (c.min?, c.max?);
let center = lo.lerp(hi, 0.5);
let fit = crate::scene::Transform::translation(center.x, center.y)
.compose(&crate::scene::Transform::scaling(((hi.x - lo.x) / 50.0).max(1e-9), ((hi.y - lo.y) / 50.0).max(1e-9)))
.compose(&self.st.fading_transform);
if let Some(sh) = library_fading(name, &fit) {
return Some(sh);
}
match self.fadings.get(name).cloned() {
Some(mut sh) => {
let (crate::canvas::Shading::Axial { transform, .. } | crate::canvas::Shading::Radial { transform, .. }) = &mut sh;
*transform = fit.compose(transform);
Some(sh)
}
None => {
self.warn(format!("fading '{name}' not supported: drawn without it"));
None
}
}
}
pub fn set_blend_mode(&mut self, mode: &str) {
let mode = mode.trim().replace(' ', "-");
self.st.blend = (mode != "normal" && !mode.is_empty()).then_some(mode);
}
fn functional_cells(&mut self, segs: &[Segment]) -> Option<Vec<Item>> {
if self.basic_shadings.contains_key(&self.st.shading) {
return None;
}
let name = self.st.shading.clone();
let f = self.functional_shading(&name)?;
let mut c = crate::canvas::Canvas::default();
c.include_path(segs);
let (lo, hi) = (c.min?, c.max?);
Some(f.cells(&f.fit(lo, hi, self.st.shade_angle)))
}
pub fn shading_of(&self, segs: &[Segment]) -> crate::canvas::Shading {
self.shading_for(segs)
}
fn shading_for(&self, segs: &[Segment]) -> crate::canvas::Shading {
if let Some(sh) = self.basic_shading_for(&self.st.shading, segs, self.st.shade_angle) {
return sh;
}
let mut c = crate::canvas::Canvas::default();
c.include_path(segs);
let (lo, hi) = (c.min.unwrap_or_default(), c.max.unwrap_or_default());
let (w, h) = (hi.x - lo.x, hi.y - lo.y);
let a = self.st.shade_angle;
let (s, co) = a.to_radians().sin_cos();
let (abs_s, abs_c) = (s.abs(), co.abs());
let sx = (w * abs_c + h * abs_s) / (abs_c + abs_s) / 50.0;
let sy = (w * abs_s + h * abs_c) / (abs_c + abs_s) / 50.0;
let center = lo.lerp(hi, 0.5);
let transform = crate::scene::Transform::translation(center.x, center.y)
.compose(&crate::scene::Transform::rotation(a))
.compose(&crate::scene::Transform::scaling(sx.max(1e-9), sy.max(1e-9)));
let st = &self.st;
let blend = |x: crate::color::Color, y: crate::color::Color, t: f64| crate::color::Color {
r: x.r * t + y.r * (1.0 - t),
g: x.g * t + y.g * (1.0 - t),
b: x.b * t + y.b * (1.0 - t),
};
let (white, black) = (crate::color::Color::WHITE, crate::color::Color::BLACK);
match st.shading.as_str() {
"ball" => crate::canvas::Shading::Radial {
transform,
focus: Point::new(-10.0, 10.0),
stops: vec![
(0.0, blend(st.ball, white, 0.15)),
(9.0 / 50.0, blend(st.ball, white, 0.75)),
(18.0 / 50.0, blend(st.ball, black, 0.7)),
(0.5, blend(st.ball, black, 0.5)),
(1.0, black),
],
},
"radial" => crate::canvas::Shading::Radial { transform, focus: Point::default(), stops: vec![(0.0, st.radial_inner), (0.5, st.radial_outer), (1.0, st.radial_outer)] },
_ => crate::canvas::Shading::Axial {
transform,
stops: vec![(0.0, st.axis_bottom), (0.25, st.axis_bottom), (0.5, st.axis_middle), (0.75, st.axis_top), (1.0, st.axis_top)],
},
}
}
fn arrow_ends(&mut self, segs: &mut [Segment]) -> Tips {
let mut out = Tips::default();
if segs.iter().any(|s| matches!(s, Segment::Close)) || self.st.tips_mode == TipsMode::Never {
return out;
}
if matches!(self.st.tips_mode, TipsMode::Proper | TipsMode::OnProperDraw) && !proper(segs) {
return out;
}
let s = self.stroke();
let widths = match self.st.double {
Some(_) => Widths { line: (2.0 * s.width + self.st.double_distance) / PT, inner: self.st.double_distance / PT },
None => Widths { line: s.width / PT, inner: 0.0 },
};
let user = self.st.tips.clone();
let mut warn = Vec::new();
let (end_spec, start_spec) = (self.st.end_arrow.clone().unwrap_or_default(), self.st.start_arrow.clone().unwrap_or_default());
let (shorten_end, shorten_start) = (self.st.shorten_end / PT, self.st.shorten_start / PT);
let end = EndTips::new(self, &end_spec, false, &user, widths, shorten_end, &mut warn);
let start = EndTips::new(self, &start_spec, true, &user, widths, shorten_start, &mut warn);
for w in warn {
self.warn(w);
}
let mut at_end = None;
if !end.is_empty() || end.shortening != 0.0 {
if let Some((k, movable, last, reference)) = path_end(segs) {
let curve = match (segs[k], end.flexed()) {
(Segment::CurveTo(c1, c2, _), true) => point_of(&segs[k - 1]).map(|prev| Curvilinear::new([last, c2, c1, prev])),
_ => None,
};
if movable && end.shortening != 0.0 {
match &curve {
Some(c) => {
let [p, b, a, _] = c.after(c.time(end.shortening));
segs[k] = Segment::CurveTo(a, b, p);
}
None => {
let p = toward(last, reference, end.shortening * PT);
if let Segment::LineTo(q) | Segment::CurveTo(_, _, q) = &mut segs[k] {
*q = p;
}
}
}
}
at_end = Some(match curve {
Some(c) => Along::Curve(c),
None => Along::Line(last, reference),
});
}
}
let mut at_start = None;
if !start.is_empty() || start.shortening != 0.0 {
if let Some((m, movable, first, reference)) = path_start(segs) {
let curve = match (segs.get(m + 1), start.flexed()) {
(Some(&Segment::CurveTo(c1, c2, p)), true) => Some(Curvilinear::new([first, c1, c2, p])),
_ => None,
};
if movable && start.shortening != 0.0 {
match &curve {
Some(c) => {
let [p, a, b, q] = c.after(c.time(start.shortening));
segs[m] = Segment::MoveTo(p);
segs[m + 1] = Segment::CurveTo(a, b, q);
}
None => segs[m] = Segment::MoveTo(toward(first, reference, start.shortening * PT)),
}
}
at_start = Some(match curve {
Some(c) => Along::Curve(c),
None => Along::Line(first, reference),
});
}
}
let stroke = Stroke { width: widths.line * PT, ..s };
let fill_opacity = self.st.fill_opacity;
for (tips, at) in [(&start, at_start), (&end, at_end)] {
let hull = match at {
Some(Along::Line(p, from)) => tips.draw(p, from, &stroke, fill_opacity, &mut out.items),
Some(Along::Curve(c)) => tips.draw_on_curve(&c, &stroke, fill_opacity, &mut out.items),
None => continue,
};
out.hull.extend(hull);
}
out
}
fn push_tips(&mut self, tips: Tips) {
if self.counts_for_size() {
for p in tips.hull {
self.canvas.include(p);
}
}
self.canvas.items.extend(tips.items);
}
}
enum Along {
Line(Point, Point),
Curve(Curvilinear),
}
#[derive(Default)]
struct Tips {
items: Vec<Item>,
hull: Vec<Point>,
}
fn toward(p: Point, q: Point, distance: f64) -> Point {
let d = q.minus(p);
let u = if d.norm() > 1e-12 { d.scale(1.0 / d.norm()) } else { Point::new(0.0, 1.0) };
p.plus(u.scale(distance))
}
fn point_of(s: &Segment) -> Option<Point> {
match s {
Segment::MoveTo(p) | Segment::LineTo(p) | Segment::CurveTo(_, _, p) => Some(*p),
Segment::Close => None,
}
}
fn last_subpath(segs: &[Segment]) -> Option<usize> {
segs.iter().rposition(|s| matches!(s, Segment::MoveTo(_)))
}
fn path_end(segs: &[Segment]) -> Option<(usize, bool, Point, Point)> {
let k = segs.len().checked_sub(1)?;
match segs[k] {
Segment::MoveTo(p) => Some((k, false, p, p)),
Segment::LineTo(last) => Some((k, true, last, point_of(&segs[k.checked_sub(1)?])?)),
Segment::CurveTo(c1, c2, last) => {
let r = if c2 != last {
c2
} else if c1 != last {
c1
} else {
point_of(&segs[k.checked_sub(1)?])?
};
Some((k, true, last, r))
}
Segment::Close => None,
}
}
fn path_start(segs: &[Segment]) -> Option<(usize, bool, Point, Point)> {
let m = last_subpath(segs)?;
let first = point_of(&segs[m])?;
match segs.get(m + 1) {
None => Some((m, false, first, first)),
Some(&Segment::LineTo(p)) => Some((m, true, first, p)),
Some(&Segment::CurveTo(c1, c2, p)) => Some((
m,
true,
first,
if c1 != first {
c1
} else if c2 != first {
c2
} else {
p
},
)),
_ => None,
}
}
fn proper(segs: &[Segment]) -> bool {
let last = segs.iter().rposition(|s| matches!(s, Segment::MoveTo(_))).unwrap_or(0);
let points: Vec<Point> = segs[last..]
.iter()
.flat_map(|s| match *s {
Segment::MoveTo(p) | Segment::LineTo(p) => vec![p],
Segment::CurveTo(a, b, p) => vec![a, b, p],
Segment::Close => vec![],
})
.collect();
points.iter().any(|p| Some(p) != points.first())
}
pub fn round_corners(segs: &[Segment], r: f64) -> Vec<Segment> {
let mut points: Vec<Vec<Point>> = Vec::new();
let mut closed: Vec<bool> = Vec::new();
for s in segs {
match s {
Segment::MoveTo(p) => {
points.push(vec![*p]);
closed.push(false);
}
Segment::LineTo(p) => {
if let Some(v) = points.last_mut() {
v.push(*p);
}
}
Segment::Close => {
if let Some(f) = closed.last_mut() {
*f = true;
}
}
Segment::CurveTo(..) => return segs.to_vec(),
}
}
let mut out = Vec::new();
for (pts, is_closed) in points.iter().zip(closed) {
let mut p: Vec<Point> = pts.clone();
if is_closed && p.len() > 2 && p.first().map(|a| a.minus(*p.last().unwrap()).norm() <= 1e-9).unwrap_or(false) {
p.pop();
}
let n = p.len();
if n < 3 {
out.push(Segment::MoveTo(p[0]));
for q in &p[1..] {
out.push(Segment::LineTo(*q));
}
if is_closed {
out.push(Segment::Close);
}
continue;
}
let corner = |k: usize| -> (Point, Point, Point) {
let prev = p[(k + n - 1) % n];
let here = p[k];
let next = p[(k + 1) % n];
let d1 = prev.minus(here);
let d2 = next.minus(here);
let rr = r.min(d1.norm() / 2.0).min(d2.norm() / 2.0);
(here.plus(d1.scale(rr / d1.norm().max(1e-12))), here, here.plus(d2.scale(rr / d2.norm().max(1e-12))))
};
let indices: Vec<usize> = if is_closed { (0..n).collect() } else { (1..n - 1).collect() };
if is_closed {
out.push(Segment::MoveTo(corner(0).2));
} else {
out.push(Segment::MoveTo(p[0]));
}
for &k in indices.iter().skip(if is_closed { 1 } else { 0 }) {
let (a, v, b) = corner(k);
out.push(Segment::LineTo(a));
out.push(Segment::CurveTo(a.lerp(v, 0.5523), b.lerp(v, 0.5523), b));
}
if is_closed {
let (a, v, b) = corner(0);
out.push(Segment::LineTo(a));
out.push(Segment::CurveTo(a.lerp(v, 0.5523), b.lerp(v, 0.5523), b));
out.push(Segment::Close);
} else {
out.push(Segment::LineTo(p[n - 1]));
}
}
out
}
pub fn library_fading(name: &str, fit: &crate::scene::Transform) -> Option<crate::canvas::Shading> {
let fit = *fit;
let (white, black) = (crate::color::Color::WHITE, crate::color::Color::BLACK);
let axial = |angle: f64, from: crate::color::Color, to: crate::color::Color| crate::canvas::Shading::Axial {
transform: fit.compose(&crate::scene::Transform::rotation(angle)),
stops: vec![(0.0, from), (0.25, from), (0.75, to), (1.0, to)],
};
let circle =
|edge: f64| crate::canvas::Shading::Radial { transform: fit, focus: Point::default(), stops: vec![(0.0, white), (edge / 50.0, white), (0.5, black), (1.0, black)] };
Some(match name {
"north" => axial(0.0, white, black),
"south" => axial(0.0, black, white),
"east" => axial(-90.0, white, black),
"west" => axial(-90.0, black, white),
"circle with fuzzy edge 10 percent" => circle(22.5),
"circle with fuzzy edge 15 percent" => circle(21.25),
"circle with fuzzy edge 20 percent" => circle(20.0),
"fuzzy ring 15 percent" => crate::canvas::Shading::Radial {
transform: fit,
focus: Point::default(),
stops: vec![(0.0, black), (21.25 / 50.0, black), (23.125 / 50.0, white), (0.5, black), (1.0, black)],
},
_ => return None,
})
}