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 { 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 { 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 = 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 { let n = vertices.len(); let ix = self.get_x(); let iy = self.get_y(); let mut result: Option = 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, 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::() / n as f64; let cy: f64 = vertices.iter().map(|v| v.1).sum::() / 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); } }