225 lines
6.9 KiB
Rust
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, ¶ms)?;
|
|
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);
|
|
}
|
|
}
|