Cord/crates/cord-expr/src/ngon.rs

225 lines
6.9 KiB
Rust

use cord_trig::ir::{NodeId, TrigOp};
use crate::parser::ExprParser;
impl<'a> ExprParser<'a> {
pub(crate) fn parse_ngon(&mut self, n: u32, args: &[NodeId]) -> Result<NodeId, String> {
if n < 3 {
return Err(format!("{n}-gon: need at least 3 sides"));
}
let (is_reg, rest) = if !args.is_empty() {
if let TrigOp::Const(v) = self.graph.nodes[args[0] as usize] {
if v.is_nan() { (true, &args[1..]) } else { (false, args) }
} else {
(false, args)
}
} else {
(false, args)
};
if is_reg {
let side = if rest.is_empty() {
self.graph.push(TrigOp::Const(1.0))
} else if rest.len() == 1 {
rest[0]
} else {
return Err(format!("{n}-gon(reg[, side])"));
};
return self.build_regular_ngon(n, side);
}
if args.len() == 1 {
return self.build_regular_ngon(n, args[0]);
}
let expected = 2 * n as usize - 3;
if args.len() != expected {
return Err(format!(
"{n}-gon: expected 1 (side), reg, or {expected} (s,a,s,...) params, got {}",
args.len()
));
}
let mut params = Vec::with_capacity(expected);
for (i, &arg) in args.iter().enumerate() {
match self.graph.nodes.get(arg as usize) {
Some(TrigOp::Const(v)) => params.push(*v),
_ => return Err(format!("{n}-gon: param {} must be a constant", i + 1)),
}
}
let vertices = construct_polygon_sas(n, &params)?;
self.build_polygon_sdf(&vertices)
}
fn build_regular_ngon(&mut self, n: u32, side: NodeId) -> Result<NodeId, String> {
let ix = self.get_x();
let iy = self.get_y();
let inv_2tan = 1.0 / (2.0 * (std::f64::consts::PI / n as f64).tan());
let scale = self.graph.push(TrigOp::Const(inv_2tan));
let inradius = self.graph.push(TrigOp::Mul(side, scale));
let mut result: Option<NodeId> = None;
for i in 0..n {
let angle = 2.0 * std::f64::consts::PI * i as f64 / n as f64;
let cx = self.graph.push(TrigOp::Const(angle.cos()));
let cy = self.graph.push(TrigOp::Const(angle.sin()));
let dx = self.graph.push(TrigOp::Mul(ix, cx));
let dy = self.graph.push(TrigOp::Mul(iy, cy));
let dot = self.graph.push(TrigOp::Add(dx, dy));
let edge = self.graph.push(TrigOp::Sub(dot, inradius));
result = Some(match result {
None => edge,
Some(prev) => self.graph.push(TrigOp::Max(prev, edge)),
});
}
Ok(result.unwrap())
}
fn build_polygon_sdf(&mut self, vertices: &[(f64, f64)]) -> Result<NodeId, String> {
let n = vertices.len();
let ix = self.get_x();
let iy = self.get_y();
let mut result: Option<NodeId> = None;
for i in 0..n {
let j = (i + 1) % n;
let (x0, y0) = vertices[i];
let (x1, y1) = vertices[j];
let dx = x1 - x0;
let dy = y1 - y0;
let len = (dx * dx + dy * dy).sqrt();
if len < 1e-15 { continue; }
let nx = dy / len;
let ny = -dx / len;
let offset = nx * x0 + ny * y0;
let cnx = self.graph.push(TrigOp::Const(nx));
let cny = self.graph.push(TrigOp::Const(ny));
let cd = self.graph.push(TrigOp::Const(offset));
let dot_x = self.graph.push(TrigOp::Mul(ix, cnx));
let dot_y = self.graph.push(TrigOp::Mul(iy, cny));
let dot = self.graph.push(TrigOp::Add(dot_x, dot_y));
let dist = self.graph.push(TrigOp::Sub(dot, cd));
result = Some(match result {
None => dist,
Some(prev) => self.graph.push(TrigOp::Max(prev, dist)),
});
}
result.ok_or_else(|| "degenerate polygon".into())
}
}
fn construct_polygon_sas(n: u32, params: &[f64]) -> Result<Vec<(f64, f64)>, String> {
use std::f64::consts::PI;
let mut vertices = Vec::with_capacity(n as usize);
let mut x = 0.0_f64;
let mut y = 0.0_f64;
let mut heading = 0.0_f64;
vertices.push((x, y));
for i in 0..(n as usize - 1) {
let side = params[i * 2];
if side <= 0.0 {
return Err(format!("side {} must be positive", i + 1));
}
x += side * heading.cos();
y += side * heading.sin();
vertices.push((x, y));
if i * 2 + 1 < params.len() {
let interior = params[i * 2 + 1];
heading += PI - interior;
}
}
let cx: f64 = vertices.iter().map(|v| v.0).sum::<f64>() / n as f64;
let cy: f64 = vertices.iter().map(|v| v.1).sum::<f64>() / n as f64;
for v in &mut vertices {
v.0 -= cx;
v.1 -= cy;
}
let mut area2 = 0.0;
for i in 0..vertices.len() {
let j = (i + 1) % vertices.len();
area2 += vertices[i].0 * vertices[j].1 - vertices[j].0 * vertices[i].1;
}
if area2 < 0.0 {
vertices.reverse();
}
Ok(vertices)
}
#[cfg(test)]
mod tests {
use crate::parse_expr;
use cord_trig::eval::evaluate;
#[test]
fn ngon_square() {
let g = parse_expr("4-gon(2)").unwrap();
assert!((evaluate(&g, 1.0, 0.0, 0.0) - 0.0).abs() < 1e-6);
assert!((evaluate(&g, 0.0, 0.0, 0.0) - -1.0).abs() < 1e-6);
}
#[test]
fn ngon_function_syntax() {
let g = parse_expr("ngon(4, 2)").unwrap();
assert!((evaluate(&g, 1.0, 0.0, 0.0) - 0.0).abs() < 1e-6);
}
#[test]
fn ngon_reg_keyword() {
let g = parse_expr("4-gon(reg, 2)").unwrap();
assert!((evaluate(&g, 1.0, 0.0, 0.0) - 0.0).abs() < 1e-6);
}
#[test]
fn ngon_reg_default_side() {
let g = parse_expr("4-gon(reg)").unwrap();
assert!((evaluate(&g, 0.5, 0.0, 0.0) - 0.0).abs() < 1e-6);
assert!((evaluate(&g, 0.0, 0.0, 0.0) - -0.5).abs() < 1e-6);
}
#[test]
fn ngon_reg_default_triangle() {
let g = parse_expr("3-gon(reg)").unwrap();
assert!(evaluate(&g, 0.0, 0.0, 0.0) < 0.0);
}
#[test]
fn ngon_sas_equilateral() {
use std::f64::consts::PI;
let src = format!("3-gon(2, {}, 2)", PI / 3.0);
let g = parse_expr(&src).unwrap();
assert!(evaluate(&g, 0.0, 0.0, 0.0) < 0.0);
}
#[test]
fn ngon_sas_right_triangle() {
use std::f64::consts::FRAC_PI_2;
let src = format!("3-gon(3, {}, 4)", FRAC_PI_2);
let g = parse_expr(&src).unwrap();
assert!(evaluate(&g, 0.0, 0.0, 0.0) < 0.0);
}
#[test]
fn ngon_sas_square() {
use std::f64::consts::FRAC_PI_2;
let src = format!("4-gon(2, {0}, 2, {0}, 2)", FRAC_PI_2);
let g = parse_expr(&src).unwrap();
assert!(evaluate(&g, 0.0, 0.0, 0.0) < 0.0);
}
}