pub mod surfaces;
use crate::model::{Blend, Joint, JointKind, Paint};
#[derive(Clone, Debug, PartialEq)]
pub struct Piece {
pub points: Vec<[f64; 2]>,
pub colour: String,
pub opacity: f64,
}
pub const SMOOTH_STEP_PX: f64 = 2.0;
pub const SMOOTH_MOST: usize = 64;
const ABSENT_BELOW: u8 = 128;
type Rgba = [u8; 4];
fn parse_colour(text: &str) -> Option<Rgba> {
let hex = text.strip_prefix('#')?;
if !(hex.len() == 6 || hex.len() == 8) || !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
return None;
}
let byte = |i: usize| u8::from_str_radix(&hex[i..i + 2], 16).ok();
Some([
byte(0)?,
byte(2)?,
byte(4)?,
if hex.len() == 8 { byte(6)? } else { 255 },
])
}
fn hex_of(rgb: [f64; 3]) -> String {
let c = |v: f64| v.round().clamp(0.0, 255.0) as u8;
format!("#{:02x}{:02x}{:02x}", c(rgb[0]), c(rgb[1]), c(rgb[2]))
}
fn mix(a: Rgba, b: Rgba, t: f64) -> [f64; 3] {
[0, 1, 2].map(|i| a[i] as f64 + (b[i] as f64 - a[i] as f64) * t)
}
struct Read {
nodes: Vec<Rgba>,
joint: Rgba,
}
fn read(nodes: usize, paint: &Paint, joints: &Joint) -> Result<Read, String> {
if paint.nodes.len() != nodes {
return Err(format!(
"the paint holds {} node colours but the well has {nodes} nodes - they must match one to one",
paint.nodes.len()
));
}
let mut colours = Vec::with_capacity(nodes);
for (k, text) in paint.nodes.iter().enumerate() {
colours
.push(parse_colour(text).ok_or_else(|| format!("node {k}'s colour {text:?} is not #rrggbbaa"))?);
}
let mut joint = [0, 0, 0, 255];
if joints.kind != JointKind::None {
joint = parse_colour(&joints.colour)
.ok_or_else(|| format!("the joint colour {:?} is not #rrggbb", joints.colour))?;
if !joints.px.is_finite() || joints.px < 0.0 {
return Err(format!("the joint length {} px is not a length", joints.px));
}
if !joints.alpha.is_finite() {
return Err(format!("the joint alpha {} is not a number", joints.alpha));
}
}
Ok(Read {
nodes: colours,
joint,
})
}
pub fn check(nodes: usize, paint: &Paint, joints: &Joint) -> Result<(), String> {
read(nodes, paint, joints).map(|_| ())
}
fn at(a: [f64; 2], b: [f64; 2], t: f64) -> [f64; 2] {
if t <= 0.0 {
a
} else if t >= 1.0 {
b
} else {
[a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]
}
}
pub fn well_pieces(path_px: &[[f64; 2]], paint: &Paint, joints: &Joint) -> Result<Vec<Piece>, String> {
let Read {
nodes: colours,
joint,
} = read(path_px.len(), paint, joints)?;
let count = path_px.len();
if count < 2 {
return Err(format!("a well of {count} node(s) has no stretch to paint"));
}
if let Some(k) = path_px
.iter()
.position(|p| !(p[0].is_finite() && p[1].is_finite()))
{
return Err(format!("node {k} of the well has no position on the drawing"));
}
let absent = |k: usize| colours[k][3] < ABSENT_BELOW;
let flagged = |k: usize| -> bool {
if k == 0 || k + 1 >= count {
return false;
}
match joints.kind {
JointKind::None => false,
JointKind::Nodes => true,
JointKind::Changes => colours[k] != colours[k - 1],
}
};
let half = joints.px / 2.0;
let alpha = joints.alpha.clamp(0.0, 1.0);
let mut arc = vec![0.0; count];
for k in 1..count {
let (a, b) = (path_px[k - 1], path_px[k]);
arc[k] = arc[k - 1] + ((b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2)).sqrt();
}
let mut spans: Vec<(f64, f64)> = Vec::new();
if joints.px > 0.0 {
let flags: Vec<usize> = (0..count).filter(|&k| flagged(k)).collect();
for (at, &c) in flags.iter().enumerate() {
let before = if at == 0 { 0 } else { flags[at - 1] };
let after = flags.get(at + 1).copied().unwrap_or(count - 1);
if arc[c] - arc[before] >= 2.0 * joints.px {
spans.push((arc[c] - half, arc[c]));
}
if arc[after] - arc[c] >= 2.0 * joints.px {
spans.push((arc[c], arc[c] + half));
}
}
}
let in_joint = |s: f64| spans.iter().any(|&(lo, hi)| s > lo && s < hi);
let mut out: Vec<Piece> = Vec::new();
let mut push = |points: Vec<[f64; 2]>, colour: String| {
if let Some(last) = out.last_mut()
&& last.colour == colour
&& last.points.last() == points.first()
{
last.points.extend(points.into_iter().skip(1));
} else {
out.push(Piece {
points,
colour,
opacity: 1.0,
});
}
};
for k in 0..count - 1 {
if absent(k) {
continue;
}
let (a, b) = (path_px[k], path_px[k + 1]);
let len = ((b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2)).sqrt();
if len <= 1e-9 {
continue;
}
let from = colours[k];
let to = if paint.blend == Blend::Smooth && !absent(k + 1) {
colours[k + 1]
} else {
from
};
let (s0, s1) = (arc[k], arc[k + 1]);
let mut cuts = vec![s0, s1];
for &(lo, hi) in &spans {
for edge in [lo, hi] {
if edge > s0 && edge < s1 {
cuts.push(edge);
}
}
}
cuts.sort_by(f64::total_cmp);
cuts.dedup_by(|x, y| (*x - *y).abs() < 1e-9);
for pair in cuts.windows(2) {
let (u0, u1) = ((pair[0] - s0) / len, (pair[1] - s0) / len);
if in_joint((pair[0] + pair[1]) / 2.0) {
if alpha > 0.0 {
let under = if (u0 + u1) / 2.0 < 0.5 { from } else { to };
push(vec![at(a, b, u0), at(a, b, u1)], hex_of(mix(under, joint, alpha)));
}
} else if to == from {
push(vec![at(a, b, u0), at(a, b, u1)], hex_of(mix(from, from, 0.0)));
} else {
let n = (((u1 - u0) * len / SMOOTH_STEP_PX).ceil() as usize).clamp(1, SMOOTH_MOST);
for j in 0..n {
let (v0, v1) = (
u0 + (u1 - u0) * j as f64 / n as f64,
u0 + (u1 - u0) * (j + 1) as f64 / n as f64,
);
push(
vec![at(a, b, v0), at(a, b, v1)],
hex_of(mix(from, to, (v0 + v1) / 2.0)),
);
}
}
}
}
Ok(out)
}
pub fn over(colour: &str, opacity: f64, under: &str) -> String {
if opacity >= 1.0 {
return colour.to_string();
}
match (parse_colour(colour), parse_colour(under)) {
(Some(top), Some(bottom)) => hex_of(mix(bottom, top, opacity.clamp(0.0, 1.0))),
_ => colour.to_string(),
}
}