use crate::model::{Blend, CutSource, Paint};
use super::Piece;
const MAX_STEPS: usize = 16;
const STEP: f64 = 12.0;
type Rgba = [f64; 4];
pub fn trace_pieces(
span_px: &[[f64; 2]],
sources: &[CutSource],
paint: &Paint,
) -> Result<Vec<Piece>, String> {
if sources.len() != span_px.len() {
return Err(format!(
"the cut has {} point(s) and {} source(s): the paint cannot be laid on it",
span_px.len(),
sources.len()
));
}
let mut colours: Vec<Option<Rgba>> = Vec::with_capacity(sources.len());
for (at, source) in sources.iter().enumerate() {
colours.push(point_colour(at, source, paint)?);
}
let mut pieces: Vec<Piece> = Vec::new();
for at in 0..span_px.len().saturating_sub(1) {
let (Some(from), Some(to)) = (colours[at], colours[at + 1]) else {
continue;
};
let (p, q) = (span_px[at], span_px[at + 1]);
match paint.blend {
Blend::Net => {
if hex_of(from) == hex_of(to) && from[3] == to[3] {
push(&mut pieces, vec![p, q], from);
} else {
let middle = [(p[0] + q[0]) / 2.0, (p[1] + q[1]) / 2.0];
push(&mut pieces, vec![p, middle], from);
push(&mut pieces, vec![middle, q], to);
}
}
Blend::Smooth => {
let spread = (0..4).map(|k| (from[k] - to[k]).abs()).fold(0.0_f64, f64::max);
let steps = ((spread / STEP).ceil() as usize).clamp(1, MAX_STEPS);
for step in 0..steps {
let (t0, t1) = (step as f64 / steps as f64, (step + 1) as f64 / steps as f64);
let at_t = |t: f64| [p[0] + (q[0] - p[0]) * t, p[1] + (q[1] - p[1]) * t];
let colour = mix(from, to, (t0 + t1) / 2.0);
push(&mut pieces, vec![at_t(t0), at_t(t1)], colour);
}
}
}
}
Ok(pieces)
}
fn push(pieces: &mut Vec<Piece>, points: Vec<[f64; 2]>, colour: Rgba) {
let (hex, opacity) = (hex_of(colour), colour[3] / 255.0);
if let Some(last) = pieces.last_mut()
&& last.colour == hex
&& last.opacity == opacity
&& last.points.last() == points.first()
{
last.points.extend(points.into_iter().skip(1));
return;
}
pieces.push(Piece {
points,
colour: hex,
opacity,
});
}
fn point_colour(at: usize, source: &CutSource, paint: &Paint) -> Result<Option<Rgba>, String> {
if !source.t.is_finite() || !(-1e-9..=1.0 + 1e-9).contains(&source.t) {
return Err(format!(
"point {at}: t = {} is not a place on its edge (0 to 1)",
source.t
));
}
let t = source.t.clamp(0.0, 1.0);
let node = |number: u32| -> Result<Rgba, String> {
let text = paint.nodes.get(number as usize).ok_or_else(|| {
format!(
"point {at}: its edge names node {number}, and the paint holds {} node(s) - the paint and the cut do not number the same nodes",
paint.nodes.len()
)
})?;
parse(text).map_err(|why| format!("point {at}: the colour of node {number}: {why}"))
};
let (a, b) = (node(source.a)?, node(source.b)?);
let leans_on_a = t < 1.0;
let leans_on_b = t > 0.0;
if (leans_on_a && a[3] == 0.0) || (leans_on_b && b[3] == 0.0) {
return Ok(None);
}
Ok(Some(match paint.blend {
Blend::Smooth => mix(a, b, t),
Blend::Net => {
if t < 0.5 {
a
} else {
b
}
}
}))
}
fn mix(a: Rgba, b: Rgba, t: f64) -> Rgba {
[0, 1, 2, 3].map(|k| a[k] + (b[k] - a[k]) * t)
}
fn parse(text: &str) -> Result<Rgba, String> {
let digits = text
.strip_prefix('#')
.ok_or_else(|| format!("`{text}` is not `#rrggbbaa`"))?;
if !(digits.len() == 8 || digits.len() == 6) || !digits.is_ascii() {
return Err(format!("`{text}` is not `#rrggbbaa`"));
}
let channel = |at: usize| u8::from_str_radix(&digits[at..at + 2], 16).map(f64::from);
let read = |at: usize| channel(at).map_err(|_| format!("`{text}` is not `#rrggbbaa`"));
let alpha = if digits.len() == 8 { read(6)? } else { 255.0 };
Ok([read(0)?, read(2)?, read(4)?, alpha])
}
fn hex_of(colour: Rgba) -> String {
let byte = |value: f64| value.round().clamp(0.0, 255.0) as u8;
format!(
"#{:02x}{:02x}{:02x}",
byte(colour[0]),
byte(colour[1]),
byte(colour[2])
)
}