use nom::{
IResult, Parser,
branch::alt,
bytes::complete::{tag, take_until, take_while, take_while_m_n, take_while1},
character::complete::{char as c_char, multispace1},
combinator::{cut, map, opt, verify},
error::{Error, ErrorKind},
multi::many0,
number::complete::double,
sequence::{delimited, preceded, terminated},
};
use crate::error::ShapeError;
use crate::ops::{
AttachCase, AttachSelector, Axis, CompFaceCase, CompTarget, FaceSelector, OffsetCase,
OffsetSelector, RoofCase, RoofConfig, RoofFaceSelector, RoofType, ShapeOp, SplitSize,
SplitSlot,
};
use crate::scope::{Quat, Vec3};
const MAX_SPLIT_SLOTS: usize = 256;
const MAX_COMP_CASES: usize = 32;
const MAX_OPS: usize = 1024;
const MAX_IDENTIFIER_LEN: usize = 64;
pub const MAX_VARIANTS: usize = 64;
fn space_or_comment<'a, E: nom::error::ParseError<&'a str>>(
input: &'a str,
) -> IResult<&'a str, (), E> {
let comment = alt((
preceded(tag("/*"), terminated(take_until("*/"), tag("*/"))),
preceded(tag("//"), take_while(|c: char| c != '\n' && c != '\r')),
));
let mut p = many0(alt((map(multispace1, |_| ()), map(comment, |_| ()))));
p.parse(input).map(|(i, _)| (i, ()))
}
fn ws<'a, F, O, E: nom::error::ParseError<&'a str>>(
inner: F,
) -> impl Parser<&'a str, Output = O, Error = E>
where
F: Parser<&'a str, Output = O, Error = E>,
{
delimited(space_or_comment, inner, space_or_comment)
}
fn finite_float(input: &str) -> IResult<&str, f64> {
verify(double, |x: &f64| x.is_finite()).parse(input)
}
fn is_ident_start(c: char) -> bool {
c.is_alphabetic() || c == '_'
}
fn is_ident_char(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
fn identifier(input: &str) -> IResult<&str, &str> {
let original = input;
let (input, s) = take_while1(is_ident_char).parse(input)?;
if !s.chars().next().map(is_ident_start).unwrap_or(false) {
return Err(nom::Err::Error(Error::new(original, ErrorKind::Alpha)));
}
if s.len() > MAX_IDENTIFIER_LEN {
return Err(nom::Err::Failure(Error::new(input, ErrorKind::TooLarge)));
}
Ok((input, s))
}
fn rule_name(input: &str) -> IResult<&str, String> {
alt((
map(
delimited(
c_char('"'),
take_while_m_n(0, MAX_IDENTIFIER_LEN, |c: char| c != '"'),
c_char('"'),
),
|s: &str| s.to_string(),
),
map(ws(identifier), |s: &str| s.to_string()),
))
.parse(input)
}
fn parse_axis(input: &str) -> IResult<&str, Axis> {
alt((
map(tag("X"), |_| Axis::X),
map(tag("Y"), |_| Axis::Y),
map(tag("Z"), |_| Axis::Z),
))
.parse(input)
}
fn parse_vec3(input: &str) -> IResult<&str, Vec3> {
let (input, _) = ws(c_char('(')).parse(input)?;
let (input, x) = ws(finite_float).parse(input)?;
let (input, _) = ws(c_char(',')).parse(input)?;
let (input, y) = ws(finite_float).parse(input)?;
let (input, _) = ws(c_char(',')).parse(input)?;
let (input, z) = ws(finite_float).parse(input)?;
let (input, _) = ws(c_char(')')).parse(input)?;
Ok((input, Vec3::new(x, y, z)))
}
fn parse_quat(input: &str) -> IResult<&str, Quat> {
let (input, _) = ws(c_char('(')).parse(input)?;
let (input, w) = ws(finite_float).parse(input)?;
let (input, _) = ws(c_char(',')).parse(input)?;
let (input, x) = ws(finite_float).parse(input)?;
let (input, _) = ws(c_char(',')).parse(input)?;
let (input, y) = ws(finite_float).parse(input)?;
let (input, _) = ws(c_char(',')).parse(input)?;
let (input, z) = ws(finite_float).parse(input)?;
let (input, _) = ws(c_char(')')).parse(input)?;
let len_sq = x * x + y * y + z * z + w * w;
if !len_sq.is_finite() || len_sq < 1e-12 {
return Err(nom::Err::Failure(Error::new(input, ErrorKind::Verify)));
}
Ok((input, Quat::from_xyzw(x, y, z, w).normalize()))
}
fn parse_split_size(input: &str) -> IResult<&str, SplitSize> {
alt((
map(preceded(c_char('~'), ws(finite_float)), SplitSize::Floating),
map(
preceded(c_char('\''), ws(finite_float)),
SplitSize::Relative,
),
map(ws(finite_float), SplitSize::Absolute),
))
.parse(input)
}
fn parse_split_slot(input: &str) -> IResult<&str, SplitSlot> {
let (input, size) = ws(parse_split_size).parse(input)?;
let (input, _) = ws(c_char(':')).parse(input)?;
let (input, rule) = ws(rule_name).parse(input)?;
Ok((input, SplitSlot { size, rule }))
}
fn parse_comp_face_case(input: &str) -> IResult<&str, CompFaceCase> {
let (input, sel_str) = ws(identifier).parse(input)?;
let selector = FaceSelector::parse(sel_str)
.ok_or_else(|| nom::Err::Failure(Error::new(input, ErrorKind::Tag)))?;
let (input, _) = ws(c_char(':')).parse(input)?;
let (input, rule) = ws(rule_name).parse(input)?;
Ok((input, CompFaceCase { selector, rule }))
}
fn parse_extrude(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = tag("Extrude").parse(input)?;
let (input, h) = cut(delimited(
ws(c_char('(')),
ws(finite_float),
ws(c_char(')')),
))
.parse(input)?;
if h <= 0.0 {
return Err(nom::Err::Failure(Error::new(input, ErrorKind::Verify)));
}
Ok((input, ShapeOp::Extrude(h)))
}
fn parse_taper(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = tag("Taper").parse(input)?;
let (input, amount) = cut(delimited(
ws(c_char('(')),
ws(finite_float),
ws(c_char(')')),
))
.parse(input)?;
if !(0.0..=1.0).contains(&amount) {
return Err(nom::Err::Failure(Error::new(input, ErrorKind::Verify)));
}
Ok((input, ShapeOp::Taper(amount)))
}
fn parse_rotate(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = tag("Rotate").parse(input)?;
let (input, q) = cut(ws(parse_quat)).parse(input)?;
Ok((input, ShapeOp::Rotate(q)))
}
fn parse_translate(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = tag("Translate").parse(input)?;
let (input, v) = cut(ws(parse_vec3)).parse(input)?;
Ok((input, ShapeOp::Translate(v)))
}
fn parse_scale(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = tag("Scale").parse(input)?;
let (input, v) = cut(ws(parse_vec3)).parse(input)?;
if v.x <= 0.0 || v.y <= 0.0 || v.z <= 0.0 {
return Err(nom::Err::Failure(Error::new(input, ErrorKind::Verify)));
}
Ok((input, ShapeOp::Scale(v)))
}
fn parse_split_snap(input: &str) -> IResult<&str, Option<crate::ops::SnapBinding>> {
let Ok((after_comma, _)) = ws::<_, _, Error<&str>>(c_char(',')).parse(input) else {
return Ok((input, None));
};
let Ok((after_eq, _)) = (
ws::<_, _, Error<&str>>(tag("snap")),
ws::<_, _, Error<&str>>(c_char('=')),
)
.parse(after_comma)
else {
return Ok((input, None));
};
let (after_label, label) = cut(ws(rule_name)).parse(after_eq)?;
let mut remaining = after_label;
let mut tolerance: Option<f64> = None;
if let Ok((after_c, _)) = ws::<_, _, Error<&str>>(c_char(',')).parse(remaining)
&& let Ok((after_t_eq, _)) = (
ws::<_, _, Error<&str>>(tag("tol")),
ws::<_, _, Error<&str>>(c_char('=')),
)
.parse(after_c)
{
let (after_v, v) = cut(ws(finite_float)).parse(after_t_eq)?;
if v < 0.0 {
return Err(nom::Err::Failure(Error::new(after_v, ErrorKind::Verify)));
}
tolerance = Some(v);
remaining = after_v;
}
Ok((
remaining,
Some(crate::ops::SnapBinding { label, tolerance }),
))
}
fn parse_split(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = tag("Split").parse(input)?;
let (input, _) = cut(ws(c_char('('))).parse(input)?;
let (input, axis) = cut(ws(parse_axis)).parse(input)?;
let (input, snap) = parse_split_snap(input)?;
let (input, _) = cut(ws(c_char(')'))).parse(input)?;
let (input, _) = cut(ws(c_char('{'))).parse(input)?;
let (mut remaining, first) = cut(ws(parse_split_slot)).parse(input)?;
let mut slots = vec![first];
loop {
if slots.len() >= MAX_SPLIT_SLOTS {
return Err(nom::Err::Failure(Error::new(
remaining,
ErrorKind::TooLarge,
)));
}
let Ok((after_sep, _)) = ws::<_, _, Error<&str>>(c_char('|')).parse(remaining) else {
break;
};
match ws(parse_split_slot).parse(after_sep) {
Ok((after_item, slot)) => {
slots.push(slot);
remaining = after_item;
}
Err(nom::Err::Failure(e)) => return Err(nom::Err::Failure(e)),
Err(_) => {
remaining = after_sep; break;
}
}
}
let (remaining, _) = ws(c_char('}')).parse(remaining)?;
Ok((remaining, ShapeOp::Split { axis, slots, snap }))
}
fn parse_reg_snap(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = tag("RegSnap").parse(input)?;
let (input, _) = cut(ws(c_char('('))).parse(input)?;
let (input, label) = cut(ws(rule_name)).parse(input)?;
let (input, _) = cut(ws(c_char(')'))).parse(input)?;
Ok((input, ShapeOp::RegSnap(label)))
}
fn parse_if_clear(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = tag("IfClear").parse(input)?;
let (input, _) = cut(ws(c_char('{'))).parse(input)?;
let (input, rule) = cut(ws(rule_name)).parse(input)?;
let (input, _) = cut(ws(c_char('}'))).parse(input)?;
Ok((input, ShapeOp::IfClear { rule }))
}
fn parse_if_occluded(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = tag("IfOccluded").parse(input)?;
let (input, _) = cut(ws(c_char('{'))).parse(input)?;
let (input, rule) = cut(ws(rule_name)).parse(input)?;
let (input, _) = cut(ws(c_char('}'))).parse(input)?;
Ok((input, ShapeOp::IfOccluded { rule }))
}
const MAX_REPEAT_TILE_SIZES: usize = 256;
fn parse_repeat_tile_sizes(input: &str) -> IResult<&str, Vec<f64>> {
if let Ok((rest, _)) = ws::<_, _, Error<&str>>(c_char('[')).parse(input) {
let (rest, first) = cut(ws(finite_float)).parse(rest)?;
if first <= 0.0 {
return Err(nom::Err::Failure(Error::new(rest, ErrorKind::Verify)));
}
let mut sizes = vec![first];
let mut remaining = rest;
loop {
if sizes.len() >= MAX_REPEAT_TILE_SIZES {
return Err(nom::Err::Failure(Error::new(
remaining,
ErrorKind::TooLarge,
)));
}
let Ok((after_comma, _)) = ws::<_, _, Error<&str>>(c_char(',')).parse(remaining) else {
break;
};
let (after_num, v) = cut(ws(finite_float)).parse(after_comma)?;
if v <= 0.0 {
return Err(nom::Err::Failure(Error::new(after_num, ErrorKind::Verify)));
}
sizes.push(v);
remaining = after_num;
}
let (remaining, _) = cut(ws(c_char(']'))).parse(remaining)?;
return Ok((remaining, sizes));
}
let (rest, v) = ws(finite_float).parse(input)?;
if v <= 0.0 {
return Err(nom::Err::Failure(Error::new(rest, ErrorKind::Verify)));
}
Ok((rest, vec![v]))
}
fn parse_repeat(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = tag("Repeat").parse(input)?;
let (input, _) = cut(ws(c_char('('))).parse(input)?;
let (input, axis) = cut(ws(parse_axis)).parse(input)?;
let (input, _) = cut(ws(c_char(','))).parse(input)?;
let (input, tile_sizes) = cut(parse_repeat_tile_sizes).parse(input)?;
let (input, _) = cut(ws(c_char(')'))).parse(input)?;
let (input, _) = cut(ws(c_char('{'))).parse(input)?;
let (input, rule) = cut(ws(rule_name)).parse(input)?;
let (input, _) = cut(ws(c_char('}'))).parse(input)?;
Ok((
input,
ShapeOp::Repeat {
axis,
tile_sizes,
rule,
},
))
}
fn parse_comp(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = tag("Comp").parse(input)?;
let (input, _) = cut(ws(c_char('('))).parse(input)?;
let (input, kind) = cut(ws(tag("Faces"))).parse(input)?;
let _ = kind; let (input, _) = cut(ws(c_char(')'))).parse(input)?;
let (input, _) = cut(ws(c_char('{'))).parse(input)?;
let (mut remaining, first) = cut(ws(parse_comp_face_case)).parse(input)?;
let mut cases = vec![first];
loop {
if cases.len() >= MAX_COMP_CASES {
return Err(nom::Err::Failure(Error::new(
remaining,
ErrorKind::TooLarge,
)));
}
let Ok((after_sep, _)) = ws::<_, _, Error<&str>>(c_char('|')).parse(remaining) else {
break;
};
match ws(parse_comp_face_case).parse(after_sep) {
Ok((after_item, case)) => {
cases.push(case);
remaining = after_item;
}
Err(nom::Err::Failure(e)) => return Err(nom::Err::Failure(e)),
Err(_) => {
remaining = after_sep; break;
}
}
}
let (remaining, _) = ws(c_char('}')).parse(remaining)?;
Ok((remaining, ShapeOp::Comp(CompTarget::Faces(cases))))
}
fn parse_instance(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = tag("I").parse(input)?;
let (input, mesh_id) =
cut(delimited(ws(c_char('(')), ws(rule_name), ws(c_char(')')))).parse(input)?;
Ok((input, ShapeOp::I(mesh_id)))
}
const MAX_POLYGON_VERTICES: usize = 256;
fn parse_vec2(input: &str) -> IResult<&str, glam::DVec2> {
let (input, _) = ws(c_char('(')).parse(input)?;
let (input, x) = ws(finite_float).parse(input)?;
let (input, _) = ws(c_char(',')).parse(input)?;
let (input, y) = ws(finite_float).parse(input)?;
let (input, _) = ws(c_char(')')).parse(input)?;
Ok((input, glam::DVec2::new(x, y)))
}
fn parse_polygon(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = tag("Polygon").parse(input)?;
let (input, _) = cut(ws(c_char('('))).parse(input)?;
let (input, first) = cut(ws(parse_vec2)).parse(input)?;
let mut verts = vec![first];
let mut remaining = input;
loop {
if verts.len() >= MAX_POLYGON_VERTICES {
return Err(nom::Err::Failure(Error::new(
remaining,
ErrorKind::TooLarge,
)));
}
let Ok((after_comma, _)) = ws::<_, _, Error<&str>>(c_char(',')).parse(remaining) else {
break;
};
let (after_pt, pt) = cut(ws(parse_vec2)).parse(after_comma)?;
verts.push(pt);
remaining = after_pt;
}
let (remaining, _) = cut(ws(c_char(')'))).parse(remaining)?;
if verts.len() < 3 {
return Err(nom::Err::Failure(Error::new(remaining, ErrorKind::Verify)));
}
Ok((remaining, ShapeOp::Polygon(verts)))
}
fn parse_mat(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = tag("Mat").parse(input)?;
let (input, _) = cut(ws(c_char('('))).parse(input)?;
let (input, mat_id) = cut(ws(rule_name)).parse(input)?;
let (input, density) = opt(preceded(ws(c_char(',')), ws(finite_float))).parse(input)?;
let (input, _) = cut(ws(c_char(')'))).parse(input)?;
if let Some(d) = density
&& d <= 0.0
{
return Err(nom::Err::Failure(Error::new(input, ErrorKind::Verify)));
}
let material = crate::model::Material {
id: mat_id,
density,
};
Ok((input, ShapeOp::Mat(material)))
}
fn parse_align_target(input: &str) -> IResult<&str, Vec3> {
let (input, _) = opt(terminated(tag("World"), c_char('.'))).parse(input)?;
let (input, name) = identifier.parse(input)?;
let v = match name {
"Up" => Vec3::new(0.0, 1.0, 0.0),
"Down" => Vec3::new(0.0, -1.0, 0.0),
"Right" => Vec3::new(1.0, 0.0, 0.0),
"Left" => Vec3::new(-1.0, 0.0, 0.0),
"Forward" => Vec3::new(0.0, 0.0, -1.0),
"Back" => Vec3::new(0.0, 0.0, 1.0),
_ => return Err(nom::Err::Failure(Error::new(input, ErrorKind::Tag))),
};
Ok((input, v))
}
fn parse_align(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = tag("Align").parse(input)?;
let (input, _) = cut(ws(c_char('('))).parse(input)?;
let (input, local_axis) = cut(ws(parse_axis)).parse(input)?;
let (input, _) = cut(ws(c_char(','))).parse(input)?;
let (input, target) = cut(ws(parse_align_target)).parse(input)?;
let (input, _) = cut(ws(c_char(')'))).parse(input)?;
Ok((input, ShapeOp::Align { local_axis, target }))
}
fn parse_offset_case(input: &str) -> IResult<&str, OffsetCase> {
let (input, sel_str) = ws(identifier).parse(input)?;
let selector = OffsetSelector::parse(sel_str)
.ok_or_else(|| nom::Err::Failure(Error::new(input, ErrorKind::Tag)))?;
let (input, _) = ws(c_char(':')).parse(input)?;
let (input, rule) = ws(rule_name).parse(input)?;
Ok((input, OffsetCase { selector, rule }))
}
fn parse_offset(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = tag("Offset").parse(input)?;
let (input, distance) = cut(delimited(
ws(c_char('(')),
ws(finite_float),
ws(c_char(')')),
))
.parse(input)?;
let (input, _) = cut(ws(c_char('{'))).parse(input)?;
let (mut remaining, first) = cut(ws(parse_offset_case)).parse(input)?;
let mut cases = vec![first];
loop {
if cases.len() >= MAX_COMP_CASES {
return Err(nom::Err::Failure(Error::new(
remaining,
ErrorKind::TooLarge,
)));
}
let Ok((after_sep, _)) = ws::<_, _, Error<&str>>(c_char('|')).parse(remaining) else {
break;
};
match ws(parse_offset_case).parse(after_sep) {
Ok((after_item, case)) => {
cases.push(case);
remaining = after_item;
}
Err(nom::Err::Failure(e)) => return Err(nom::Err::Failure(e)),
Err(_) => {
remaining = after_sep;
break;
}
}
}
let (remaining, _) = ws(c_char('}')).parse(remaining)?;
Ok((remaining, ShapeOp::Offset { distance, cases }))
}
fn parse_roof_case(input: &str) -> IResult<&str, RoofCase> {
let (input, sel_str) = ws(identifier).parse(input)?;
let selector = RoofFaceSelector::parse(sel_str)
.ok_or_else(|| nom::Err::Failure(Error::new(input, ErrorKind::Tag)))?;
let (input, _) = ws(c_char(':')).parse(input)?;
let (input, rule) = ws(rule_name).parse(input)?;
Ok((input, RoofCase { selector, rule }))
}
fn parse_named_float<'a>(key: &'static str, input: &'a str) -> IResult<&'a str, Option<f64>> {
let result: IResult<&str, (&str, f64)> =
(ws(tag(key)), preceded(ws(c_char('=')), ws(finite_float))).parse(input);
match result {
Ok((rest, (_, v))) => Ok((rest, Some(v))),
Err(_) => Ok((input, None)),
}
}
#[allow(clippy::type_complexity)]
fn parse_roof_named_params(
input: &str,
) -> IResult<
&str,
(
Option<f64>,
Option<f64>,
Option<f64>,
Option<f64>,
Option<f64>,
),
> {
let mut remaining = input;
let mut overhang: Option<f64> = None;
let mut offset: Option<f64> = None;
let mut tier: Option<f64> = None;
let mut fascia: Option<f64> = None;
let mut secondary: Option<f64> = None;
loop {
let Ok((after_comma, _)) = ws::<_, _, Error<&str>>(c_char(',')).parse(remaining) else {
break;
};
let mut matched = false;
for (key, slot) in [
("overhang", &mut overhang),
("offset", &mut offset),
("tier", &mut tier),
("fascia", &mut fascia),
("secondary", &mut secondary),
] {
if let Ok((rest, Some(v))) = parse_named_float(key, after_comma) {
*slot = Some(v);
remaining = rest;
matched = true;
break;
}
}
if !matched {
break;
}
}
Ok((remaining, (overhang, offset, tier, fascia, secondary)))
}
fn roof_type_uses_secondary_pitch(rt: RoofType) -> bool {
matches!(rt, RoofType::Gambrel | RoofType::Mansard)
}
fn parse_roof(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = tag("Roof").parse(input)?;
let (input, _) = cut(ws(c_char('('))).parse(input)?;
let (input, type_str) = cut(ws(identifier)).parse(input)?;
let roof_type = RoofType::parse(type_str)
.ok_or_else(|| nom::Err::Failure(Error::new(input, ErrorKind::Tag)))?;
let (input, _) = cut(ws(c_char(','))).parse(input)?;
let (input, pitch) = cut(ws(finite_float)).parse(input)?;
if pitch <= 0.0 || pitch >= 90.0 {
return Err(nom::Err::Failure(Error::new(input, ErrorKind::Verify)));
}
let uses_secondary = roof_type_uses_secondary_pitch(roof_type);
let mut secondary_pitch: Option<f64> = None;
let mut overhang = 0.0_f64;
let (input, second_positional) =
opt(preceded(ws(c_char(',')), ws(finite_float))).parse(input)?;
if let Some(v) = second_positional {
if uses_secondary {
secondary_pitch = Some(v);
} else {
if v < 0.0 {
return Err(nom::Err::Failure(Error::new(input, ErrorKind::Verify)));
}
overhang = v;
}
}
let (input, (ov_named, offset_named, tier_named, fascia_named, sec_named)) =
parse_roof_named_params(input)?;
if let Some(v) = ov_named {
if v < 0.0 {
return Err(nom::Err::Failure(Error::new(input, ErrorKind::Verify)));
}
overhang = v;
}
if let Some(v) = sec_named {
secondary_pitch = Some(v);
}
let (input, _) = cut(ws(c_char(')'))).parse(input)?;
let config = RoofConfig {
roof_type,
pitch,
secondary_pitch,
overhang,
ridge_offset: offset_named.unwrap_or(0.5),
fascia_depth: fascia_named.unwrap_or(0.0),
tier_height: tier_named,
};
let (input, _) = cut(ws(c_char('{'))).parse(input)?;
let (mut remaining, first) = cut(ws(parse_roof_case)).parse(input)?;
let mut cases = vec![first];
loop {
if cases.len() >= MAX_COMP_CASES {
return Err(nom::Err::Failure(Error::new(
remaining,
ErrorKind::TooLarge,
)));
}
let Ok((after_sep, _)) = ws::<_, _, Error<&str>>(c_char('|')).parse(remaining) else {
break;
};
match ws(parse_roof_case).parse(after_sep) {
Ok((after_item, case)) => {
cases.push(case);
remaining = after_item;
}
Err(nom::Err::Failure(e)) => return Err(nom::Err::Failure(e)),
Err(_) => {
remaining = after_sep;
break;
}
}
}
let (remaining, _) = ws(c_char('}')).parse(remaining)?;
Ok((remaining, ShapeOp::Roof { config, cases }))
}
fn parse_attach_case(input: &str) -> IResult<&str, AttachCase> {
let (input, sel_str) = ws(identifier).parse(input)?;
let selector = AttachSelector::parse(sel_str)
.ok_or_else(|| nom::Err::Failure(Error::new(input, ErrorKind::Tag)))?;
let (input, _) = ws(c_char(':')).parse(input)?;
let (input, rule) = ws(rule_name).parse(input)?;
Ok((input, AttachCase { selector, rule }))
}
fn parse_world_axis(input: &str) -> IResult<&str, Vec3> {
let (input, name) = ws(identifier).parse(input)?;
let v = match name {
"Up" | "up" => Vec3::Y,
"Down" | "down" => Vec3::NEG_Y,
"Right" | "right" => Vec3::X,
"Left" | "left" => Vec3::NEG_X,
"Forward" | "forward" => Vec3::NEG_Z,
"Back" | "back" => Vec3::Z,
_ => return Err(nom::Err::Failure(Error::new(input, ErrorKind::Tag))),
};
Ok((input, v))
}
fn parse_attach(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = tag("Attach").parse(input)?;
let (input, _) = cut(ws(c_char('('))).parse(input)?;
let (input, world_axis) = cut(ws(parse_world_axis)).parse(input)?;
let (input, _) = cut(ws(c_char(')'))).parse(input)?;
let (input, _) = cut(ws(c_char('{'))).parse(input)?;
let (mut remaining, first) = cut(ws(parse_attach_case)).parse(input)?;
let mut cases = vec![first];
loop {
if cases.len() >= MAX_COMP_CASES {
return Err(nom::Err::Failure(Error::new(
remaining,
ErrorKind::TooLarge,
)));
}
let Ok((after_sep, _)) = ws::<_, _, Error<&str>>(c_char('|')).parse(remaining) else {
break;
};
match ws(parse_attach_case).parse(after_sep) {
Ok((after_item, case)) => {
cases.push(case);
remaining = after_item;
}
Err(nom::Err::Failure(e)) => return Err(nom::Err::Failure(e)),
Err(_) => {
remaining = after_sep;
break;
}
}
}
let (remaining, _) = ws(c_char('}')).parse(remaining)?;
Ok((remaining, ShapeOp::Attach { world_axis, cases }))
}
fn parse_rule_ref(input: &str) -> IResult<&str, ShapeOp> {
map(ws(rule_name), ShapeOp::Rule).parse(input)
}
fn parse_op(input: &str) -> IResult<&str, ShapeOp> {
alt((
parse_extrude,
parse_taper,
parse_rotate,
parse_translate,
parse_scale,
parse_split,
parse_repeat,
parse_comp,
parse_if_clear,
parse_if_occluded,
parse_instance,
parse_mat,
parse_align,
parse_offset,
parse_roof,
parse_attach,
parse_polygon,
parse_reg_snap,
parse_rule_ref,
))
.parse(input)
}
fn is_terminating_op(op: &ShapeOp) -> bool {
matches!(
op,
ShapeOp::I(_)
| ShapeOp::Rule(_)
| ShapeOp::Split { .. }
| ShapeOp::Comp(_)
| ShapeOp::Repeat { .. }
| ShapeOp::Offset { .. }
| ShapeOp::Roof { .. }
| ShapeOp::Attach { .. }
)
}
pub fn parse_ops(input: &str) -> Result<Vec<ShapeOp>, ShapeError> {
let mut remaining = input;
let mut ops = Vec::new();
loop {
let (next, _) = space_or_comment::<Error<&str>>(remaining)
.map_err(|e| ShapeError::ParseError(e.to_string()))?;
remaining = next;
if remaining.is_empty() {
break;
}
if ops.len() >= MAX_OPS {
return Err(ShapeError::CapacityOverflow);
}
let (next, op) = parse_op(remaining).map_err(|e| ShapeError::ParseError(e.to_string()))?;
let terminates = is_terminating_op(&op);
ops.push(op);
remaining = next;
if terminates {
let (after_ws, _) = space_or_comment::<Error<&str>>(remaining)
.map_err(|e| ShapeError::ParseError(e.to_string()))?;
if !after_ws.is_empty() {
return Err(ShapeError::ParseError(
"unreachable operations after terminal or branching op".to_string(),
));
}
break;
}
}
Ok(ops)
}
fn split_top_level_pipe(s: &str) -> Result<Vec<&str>, ShapeError> {
let mut depth: i32 = 0;
let mut in_string = false;
let mut in_block_comment = false;
let mut in_line_comment = false;
let mut start = 0;
let mut parts = Vec::new();
let mut iter = s.char_indices().peekable();
while let Some((byte_pos, c)) = iter.next() {
if in_line_comment {
if c == '\n' || c == '\r' {
in_line_comment = false;
}
continue;
}
if in_block_comment {
if c == '*' && matches!(iter.peek(), Some((_, '/'))) {
iter.next(); in_block_comment = false;
}
continue;
}
if in_string {
if c == '"' {
in_string = false;
}
continue;
}
if c == '/' {
match iter.peek() {
Some(&(_, '/')) => {
iter.next();
in_line_comment = true;
continue;
}
Some(&(_, '*')) => {
iter.next();
in_block_comment = true;
continue;
}
_ => {}
}
}
match c {
'"' => in_string = true,
'{' => depth += 1,
'}' => depth -= 1,
'|' if depth == 0 => {
if parts.len() + 2 > MAX_VARIANTS {
return Err(ShapeError::CapacityOverflow);
}
parts.push(s[start..byte_pos].trim());
start = byte_pos + c.len_utf8();
}
_ => {}
}
}
parts.push(s[start..].trim());
Ok(parts)
}
fn parse_weight_prefix(input: &str) -> IResult<&str, f64> {
let (input, w) = ws(double).parse(input)?;
let (input, _) = ws(c_char('%')).parse(input)?;
Ok((input, w))
}
fn try_parse_weight(input: &str) -> Option<(f64, &str)> {
let trimmed = input.trim_start();
match parse_weight_prefix(trimmed) {
Ok((rest, w)) if w.is_finite() && w >= 0.0 => Some((w / 100.0, rest)),
_ => None,
}
}
#[derive(Debug, Clone)]
pub struct GrammarRule {
pub name: String,
pub variants: Vec<(f64, Vec<ShapeOp>)>,
}
impl GrammarRule {
pub fn ops(&self) -> &[ShapeOp] {
self.variants
.first()
.map(|(_, ops)| ops.as_slice())
.unwrap_or(&[])
}
}
pub fn parse_rule(input: &str) -> Result<GrammarRule, ShapeError> {
let (remaining, _) = space_or_comment::<Error<&str>>(input)
.map_err(|e| ShapeError::ParseError(e.to_string()))?;
let (remaining, name) = ws(identifier)
.parse(remaining)
.map_err(|e| ShapeError::ParseError(e.to_string()))?;
let (remaining, _) = ws(tag::<_, _, Error<&str>>("-->"))
.parse(remaining)
.map_err(|e| ShapeError::ParseError(e.to_string()))?;
let parts = split_top_level_pipe(remaining)?;
let variants = if parts.len() > 1 {
parts
.iter()
.map(|part| match try_parse_weight(part) {
Some((weight, rest)) => Ok((weight, parse_ops(rest)?)),
None => Err(ShapeError::ParseError(format!(
"stochastic rule alternative missing 'weight%' prefix: {:?}",
part
))),
})
.collect::<Result<Vec<_>, _>>()?
} else {
let part = parts[0];
match try_parse_weight(part) {
Some((weight, rest)) => vec![(weight, parse_ops(rest)?)],
None => vec![(1.0, parse_ops(remaining)?)],
}
};
Ok(GrammarRule {
name: name.to_string(),
variants,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ops::{Axis, ShapeOp, SplitSize};
#[test]
fn test_parse_extrude() {
let ops = parse_ops("Extrude(10)").unwrap();
assert_eq!(ops.len(), 1);
assert_eq!(ops[0], ShapeOp::Extrude(10.0));
}
#[test]
fn test_parse_taper() {
let ops = parse_ops("Taper(0.5)").unwrap();
assert_eq!(ops[0], ShapeOp::Taper(0.5));
}
#[test]
fn test_parse_split_y() {
let ops = parse_ops("Split(Y) { ~1.0: Floor | ~1.0: Floor | 2.0: Roof }").unwrap();
assert_eq!(ops.len(), 1);
let ShapeOp::Split { axis, slots, .. } = &ops[0] else {
panic!("expected Split");
};
assert_eq!(*axis, Axis::Y);
assert_eq!(slots.len(), 3);
assert_eq!(slots[0].size, SplitSize::Floating(1.0));
assert_eq!(slots[0].rule, "Floor");
assert_eq!(slots[2].size, SplitSize::Absolute(2.0));
assert_eq!(slots[2].rule, "Roof");
}
#[test]
fn test_parse_split_relative() {
let ops = parse_ops("Split(X) { '0.3: Left | '0.7: Right }").unwrap();
let ShapeOp::Split { slots, .. } = &ops[0] else {
panic!("expected Split");
};
assert_eq!(slots[0].size, SplitSize::Relative(0.3));
assert_eq!(slots[1].size, SplitSize::Relative(0.7));
}
#[test]
fn test_parse_comp_faces() {
let ops =
parse_ops("Comp(Faces) { Top: Roof | Side: Facade | Bottom: Foundation }").unwrap();
assert_eq!(ops.len(), 1);
let ShapeOp::Comp(CompTarget::Faces(cases)) = &ops[0] else {
panic!("expected Comp(Faces)");
};
assert_eq!(cases.len(), 3);
assert_eq!(cases[0].selector, FaceSelector::Top);
assert_eq!(cases[0].rule, "Roof");
}
#[test]
fn test_parse_instance() {
let ops = parse_ops(r#"I("Window")"#).unwrap();
assert_eq!(ops[0], ShapeOp::I("Window".to_string()));
}
#[test]
fn test_parse_mat() {
let ops = parse_ops(r#"Mat("Brick")"#).unwrap();
assert_eq!(ops[0], ShapeOp::Mat(crate::model::Material::new("Brick")));
let ops2 = parse_ops("Mat(Stone)").unwrap();
assert_eq!(ops2[0], ShapeOp::Mat(crate::model::Material::new("Stone")));
}
#[test]
fn test_parse_mat_with_density() {
let ops = parse_ops(r#"Mat("Brick", 1800)"#).unwrap();
assert_eq!(
ops[0],
ShapeOp::Mat(crate::model::Material::with_density("Brick", 1800.0))
);
}
#[test]
fn test_parse_mat_zero_density_rejected() {
assert!(parse_ops("Mat(Stone, 0)").is_err());
}
#[test]
fn test_parse_mat_negative_density_rejected() {
assert!(parse_ops("Mat(Stone, -5)").is_err());
}
#[test]
fn test_parse_rule_ref() {
let ops = parse_ops("Floor").unwrap();
assert_eq!(ops[0], ShapeOp::Rule("Floor".to_string()));
}
#[test]
fn test_parse_multiple_ops() {
let ops =
parse_ops(r#"Extrude(10) Split(Y) { 2.0: Ground | ~1.0: Upper | 3.0: Roof }"#).unwrap();
assert_eq!(ops.len(), 2);
}
#[test]
fn test_parse_grammar_rule_deterministic() {
let rule = parse_rule("Lot --> Extrude(10) Split(Y) { ~1: Floor | 2: Roof }").unwrap();
assert_eq!(rule.name, "Lot");
assert_eq!(rule.variants.len(), 1);
assert!((rule.variants[0].0 - 1.0).abs() < 1e-9);
assert_eq!(rule.variants[0].1.len(), 2);
}
#[test]
fn test_parse_grammar_rule_stochastic() {
let rule = parse_rule("Facade --> 70% BrickWall | 30% GlassCurtain").unwrap();
assert_eq!(rule.name, "Facade");
assert_eq!(rule.variants.len(), 2);
assert!((rule.variants[0].0 - 0.70).abs() < 1e-9);
assert_eq!(
rule.variants[0].1,
vec![ShapeOp::Rule("BrickWall".to_string())]
);
assert!((rule.variants[1].0 - 0.30).abs() < 1e-9);
assert_eq!(
rule.variants[1].1,
vec![ShapeOp::Rule("GlassCurtain".to_string())]
);
}
#[test]
fn test_parse_stochastic_with_complex_ops() {
let rule = parse_rule("R --> 50% Split(X) { ~1: A | ~1: B } | 50% I(Solid)").unwrap();
assert_eq!(rule.variants.len(), 2);
assert_eq!(rule.variants[0].1.len(), 1); assert_eq!(rule.variants[1].1.len(), 1); }
#[test]
fn test_extrude_zero_rejected() {
assert!(parse_ops("Extrude(0)").is_err());
}
#[test]
fn test_taper_out_of_range_rejected() {
assert!(parse_ops("Taper(1.5)").is_err());
}
#[test]
fn test_comments_ignored() {
let ops = parse_ops("// comment\nExtrude(5) /* block */ Taper(0.2)").unwrap();
assert_eq!(ops.len(), 2);
}
#[test]
fn test_grammar_rule_ops_empty_variants_returns_empty() {
let rule = GrammarRule {
name: "Empty".to_string(),
variants: vec![],
};
assert_eq!(rule.ops(), &[] as &[ShapeOp]);
}
#[test]
fn test_rotate_overflow_components_rejected() {
assert!(parse_ops("Rotate(1e160, 0, 0, 0)").is_err());
assert!(parse_ops("Rotate(1, 1e200, 0, 0)").is_err());
}
#[test]
fn test_pipe_in_quoted_mesh_id_not_split() {
let rule = parse_rule(r#"Lot --> I("Wall|Door")"#).unwrap();
assert_eq!(rule.variants.len(), 1);
assert_eq!(
rule.variants[0].1,
vec![ShapeOp::I("Wall|Door".to_string())]
);
}
#[test]
fn test_block_comment_with_brace_does_not_confuse_depth() {
let rule = parse_rule("Facade --> 50% Extrude(10) /* { */ | 50% I(Solid)").unwrap();
assert_eq!(rule.variants.len(), 2);
assert!((rule.variants[0].0 - 0.50).abs() < 1e-9);
assert!((rule.variants[1].0 - 0.50).abs() < 1e-9);
}
#[test]
fn test_ops_after_instance_rejected() {
assert!(parse_ops(r#"I("Wall") Scale(2, 2, 2)"#).is_err());
}
#[test]
fn test_ops_after_rule_ref_rejected() {
assert!(parse_ops("Floor Scale(2, 2, 2)").is_err());
}
#[test]
fn test_ops_after_split_rejected() {
assert!(parse_ops("Split(Y) { ~1: A | ~1: B } Scale(1, 2, 1)").is_err());
}
#[test]
fn test_too_many_variants_rejected() {
let variants: Vec<String> = (0..=MAX_VARIANTS).map(|i| format!("1% I(M{i})")).collect();
let rule_str = format!("R --> {}", variants.join(" | "));
assert!(matches!(
parse_rule(&rule_str),
Err(ShapeError::CapacityOverflow)
));
}
#[test]
fn test_max_variants_boundary_accepted() {
let variants: Vec<String> = (0..MAX_VARIANTS).map(|i| format!("1% I(M{i})")).collect();
let rule_str = format!("R --> {}", variants.join(" | "));
let rule = parse_rule(&rule_str).unwrap();
assert_eq!(rule.variants.len(), MAX_VARIANTS);
}
#[test]
fn test_translate_wrong_arg_count_rejected() {
assert!(parse_ops("Translate(1.0, 2.0)").is_err());
}
#[test]
fn test_extrude_missing_arg_rejected() {
assert!(parse_ops("Extrude()").is_err());
}
#[test]
fn test_split_missing_brace_rejected() {
assert!(parse_ops("Split(Y)").is_err());
}
#[test]
fn test_single_variant_with_weight_prefix() {
let rule = parse_rule("Lot --> 100% Extrude(10)").unwrap();
assert_eq!(rule.variants.len(), 1);
assert!((rule.variants[0].0 - 1.0).abs() < 1e-9);
assert_eq!(rule.variants[0].1, vec![ShapeOp::Extrude(10.0)]);
}
#[test]
fn test_scale_negative_rejected() {
assert!(parse_ops("Scale(-1, 1, 1)").is_err());
}
#[test]
fn test_scale_zero_rejected() {
assert!(parse_ops("Scale(0, 1, 1)").is_err());
}
#[test]
fn test_scale_positive_accepted() {
let ops = parse_ops("Scale(0.5, 2, 1)").unwrap();
assert_eq!(ops.len(), 1);
}
#[test]
fn test_parse_align_basic() {
let ops = parse_ops("Align(Y, Up)").unwrap();
assert_eq!(ops.len(), 1);
let ShapeOp::Align { local_axis, target } = &ops[0] else {
panic!("expected Align");
};
assert_eq!(*local_axis, Axis::Y);
assert!((*target - crate::scope::Vec3::new(0.0, 1.0, 0.0)).length() < 1e-9);
}
#[test]
fn test_parse_align_world_prefix() {
let ops = parse_ops("Align(Z, World.Forward)").unwrap();
let ShapeOp::Align { local_axis, target } = &ops[0] else {
panic!("expected Align");
};
assert_eq!(*local_axis, Axis::Z);
assert!((*target - crate::scope::Vec3::new(0.0, 0.0, -1.0)).length() < 1e-9);
}
#[test]
fn test_parse_align_unknown_target_rejected() {
assert!(parse_ops("Align(Y, Sideways)").is_err());
}
#[test]
fn test_parse_offset_basic() {
let ops = parse_ops("Offset(-0.2) { Inside: Glass | Border: Frame }").unwrap();
assert_eq!(ops.len(), 1);
let ShapeOp::Offset { distance, cases } = &ops[0] else {
panic!("expected Offset");
};
assert!((*distance - (-0.2)).abs() < 1e-9);
assert_eq!(cases.len(), 2);
assert_eq!(cases[0].selector, OffsetSelector::Inside);
assert_eq!(cases[0].rule, "Glass");
assert_eq!(cases[1].selector, OffsetSelector::Border);
assert_eq!(cases[1].rule, "Frame");
}
#[test]
fn test_parse_offset_is_terminating() {
assert!(parse_ops("Offset(-0.1) { Inside: A } Scale(1, 2, 1)").is_err());
}
#[test]
fn test_parse_roof_gable_no_overhang() {
let ops = parse_ops("Roof(Gable, 30) { Slope: Tiles | GableEnd: Bricks }").unwrap();
assert_eq!(ops.len(), 1);
let ShapeOp::Roof { config, cases } = &ops[0] else {
panic!("expected Roof");
};
assert_eq!(config.roof_type, RoofType::Gable);
assert!((config.pitch - 30.0).abs() < 1e-9);
assert!(config.overhang.abs() < 1e-9);
assert_eq!(cases.len(), 2);
assert_eq!(cases[0].selector, RoofFaceSelector::Slope);
assert_eq!(cases[1].selector, RoofFaceSelector::GableEnd);
}
#[test]
fn test_parse_roof_hip_with_overhang() {
let ops = parse_ops("Roof(Hip, 45, 0.5) { Slope: Tiles }").unwrap();
let ShapeOp::Roof { config, .. } = &ops[0] else {
panic!("expected Roof");
};
assert_eq!(config.roof_type, RoofType::Hip);
assert!((config.pitch - 45.0).abs() < 1e-9);
assert!((config.overhang - 0.5).abs() < 1e-9);
}
#[test]
fn test_parse_roof_angle_out_of_range_rejected() {
assert!(parse_ops("Roof(Gable, 0) { Slope: Tiles }").is_err());
assert!(parse_ops("Roof(Gable, 90) { Slope: Tiles }").is_err());
}
#[test]
fn test_parse_roof_is_terminating() {
assert!(parse_ops("Roof(Shed, 30) { Slope: Tiles } Scale(1, 2, 1)").is_err());
}
}