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::expr::{Expr, MAX_EXPR_NODES, parse_expr};
use crate::ops::{
AttachCase, AttachSelector, Axis, CarveCase, CarveSelector, CompFaceCase, CompTarget,
FaceSelector, FitCandidate, OffsetCase, OffsetSelector, RoofCase, RoofFaceSelector, RoofSpec,
RoofType, RuleCall, RuleVariant, ShapeOp, SplitEntry, SplitSize, SplitSlot, VariantSelector,
};
use crate::scope::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;
pub const MAX_RULE_ARGS: usize = 16;
pub(crate) 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 == '_'
}
pub(crate) 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 keyword<'a>(
kw: &'static str,
) -> impl FnMut(&'a str) -> IResult<&'a str, &'a str, Error<&'a str>> {
move |input: &'a str| {
let (rest, m) = tag::<_, _, Error<&str>>(kw).parse(input)?;
if rest.chars().next().is_some_and(is_ident_char) {
return Err(nom::Err::Error(Error::new(input, ErrorKind::Tag)));
}
Ok((rest, m))
}
}
fn arg_expr(input: &str) -> IResult<&str, Expr> {
let (rest, e) = parse_expr(input)?;
if e.node_count() > MAX_EXPR_NODES {
return Err(nom::Err::Failure(Error::new(input, ErrorKind::TooLarge)));
}
Ok((rest, e))
}
fn parse_rule_call(input: &str) -> IResult<&str, RuleCall> {
let (input, name) = rule_name(input)?;
let Ok((mut rem, _)) = ws::<_, _, Error<&str>>(c_char('(')).parse(input) else {
return Ok((input, RuleCall::new(name)));
};
let mut args = Vec::new();
if let Ok((after, _)) = ws::<_, _, Error<&str>>(c_char(')')).parse(rem) {
return Ok((after, RuleCall::with_args(name, args)));
}
loop {
let (after_arg, arg) = arg_expr(rem)?;
args.push(arg);
if args.len() > MAX_RULE_ARGS {
return Err(nom::Err::Failure(Error::new(
after_arg,
ErrorKind::TooLarge,
)));
}
if let Ok((after, _)) = ws::<_, _, Error<&str>>(c_char(',')).parse(after_arg) {
rem = after;
continue;
}
let (after, _) = cut(ws(c_char(')'))).parse(after_arg)?;
rem = after;
break;
}
Ok((rem, RuleCall::with_args(name, args)))
}
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_expr3(input: &str) -> IResult<&str, [Expr; 3]> {
let (input, _) = ws(c_char('(')).parse(input)?;
let (input, x) = arg_expr(input)?;
let (input, _) = ws(c_char(',')).parse(input)?;
let (input, y) = arg_expr(input)?;
let (input, _) = ws(c_char(',')).parse(input)?;
let (input, z) = arg_expr(input)?;
let (input, _) = ws(c_char(')')).parse(input)?;
Ok((input, [x, y, z]))
}
fn parse_expr4(input: &str) -> IResult<&str, [Expr; 4]> {
let (input, _) = ws(c_char('(')).parse(input)?;
let (input, w) = arg_expr(input)?;
let (input, _) = ws(c_char(',')).parse(input)?;
let (input, x) = arg_expr(input)?;
let (input, _) = ws(c_char(',')).parse(input)?;
let (input, y) = arg_expr(input)?;
let (input, _) = ws(c_char(',')).parse(input)?;
let (input, z) = arg_expr(input)?;
let (input, _) = ws(c_char(')')).parse(input)?;
if let (Some(wv), Some(xv), Some(yv), Some(zv)) =
(w.as_lit(), x.as_lit(), y.as_lit(), z.as_lit())
{
let len_sq = xv * xv + yv * yv + zv * zv + wv * wv;
if !len_sq.is_finite() || len_sq < 1e-12 {
return Err(nom::Err::Failure(Error::new(input, ErrorKind::Verify)));
}
}
Ok((input, [w, x, y, z]))
}
fn parse_split_size(input: &str) -> IResult<&str, SplitSize> {
if let Ok((rest, _)) = c_char::<_, Error<&str>>('~').parse(input) {
let (rest, e) = arg_expr(rest)?;
return Ok((rest, SplitSize::Floating(e)));
}
if let Ok((rest, _)) = c_char::<_, Error<&str>>('\'').parse(input) {
let (rest, e) = arg_expr(rest)?;
return Ok((rest, SplitSize::Relative(e)));
}
let (rest, e) = arg_expr(input)?;
Ok((rest, SplitSize::Absolute(e)))
}
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(parse_rule_call).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(parse_rule_call).parse(input)?;
Ok((input, CompFaceCase { selector, rule }))
}
fn parse_extrude(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = keyword("Extrude")(input)?;
let (input, h) = cut(delimited(ws(c_char('(')), arg_expr, ws(c_char(')')))).parse(input)?;
if let Some(v) = h.as_lit()
&& v <= 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, _) = keyword("Taper")(input)?;
let (input, amount) =
cut(delimited(ws(c_char('(')), arg_expr, ws(c_char(')')))).parse(input)?;
if let Some(v) = amount.as_lit()
&& !(0.0..=1.0).contains(&v)
{
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, _) = keyword("Rotate")(input)?;
let (input, q) = cut(ws(parse_expr4)).parse(input)?;
Ok((input, ShapeOp::Rotate(q)))
}
fn parse_translate(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = keyword("Translate")(input)?;
let (input, v) = cut(ws(parse_expr3)).parse(input)?;
Ok((input, ShapeOp::Translate(v)))
}
fn parse_scale(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = keyword("Scale")(input)?;
let (input, v) = cut(ws(parse_expr3)).parse(input)?;
for c in &v {
if let Some(lit) = c.as_lit()
&& lit <= 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_group(input: &str) -> IResult<&str, Vec<SplitSlot>> {
let (input, _) = 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;
};
let (after_item, slot) = cut(ws(parse_split_slot)).parse(after_sep)?;
slots.push(slot);
remaining = after_item;
}
let (remaining, _) = cut(ws(c_char('}'))).parse(remaining)?;
let (remaining, _) = cut(ws(c_char('*'))).parse(remaining)?;
Ok((remaining, slots))
}
fn parse_split_entry(input: &str) -> IResult<&str, SplitEntry> {
if input.trim_start().starts_with('{') {
let (rest, group) = parse_split_group(input)?;
return Ok((rest, SplitEntry::Group(group)));
}
let (rest, slot) = parse_split_slot(input)?;
Ok((rest, SplitEntry::Slot(slot)))
}
fn parse_split(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = keyword("Split")(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_entry)).parse(input)?;
let mut entries = vec![first];
loop {
if entries.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_entry).parse(after_sep) {
Ok((after_item, entry)) => {
entries.push(entry);
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)?;
if entries
.iter()
.filter(|e| matches!(e, SplitEntry::Group(_)))
.count()
> 1
{
return Err(nom::Err::Failure(Error::new(remaining, ErrorKind::Verify)));
}
Ok((
remaining,
ShapeOp::Split {
axis,
entries,
snap,
},
))
}
fn parse_split_area(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = keyword("SplitArea")(input)?;
let (input, _) = cut(ws(c_char('('))).parse(input)?;
let (input, axis) = cut(ws(parse_axis)).parse(input)?;
if axis == Axis::Y {
return Err(nom::Err::Failure(Error::new(input, ErrorKind::Verify)));
}
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;
};
let (after_item, slot) = cut(ws(parse_split_slot)).parse(after_sep)?;
slots.push(slot);
remaining = after_item;
}
let (remaining, _) = ws(c_char('}')).parse(remaining)?;
Ok((remaining, ShapeOp::SplitArea { axis, slots }))
}
fn parse_fit(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = keyword("Fit")(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, _) = cut(ws(c_char('{'))).parse(input)?;
let mut candidates = Vec::new();
let mut remaining = input;
loop {
if candidates.len() >= MAX_SPLIT_SLOTS {
return Err(nom::Err::Failure(Error::new(
remaining,
ErrorKind::TooLarge,
)));
}
let (after_min, min_size) = cut(ws(arg_expr)).parse(remaining)?;
let (after_colon, _) = cut(ws(c_char(':'))).parse(after_min)?;
let (after_rule, rule) = cut(ws(parse_rule_call)).parse(after_colon)?;
candidates.push(FitCandidate { min_size, rule });
if let Ok((after_sep, _)) = ws::<_, _, Error<&str>>(c_char('|')).parse(after_rule) {
remaining = after_sep;
continue;
}
remaining = after_rule;
break;
}
let (remaining, _) = cut(ws(c_char('}'))).parse(remaining)?;
Ok((remaining, ShapeOp::Fit { axis, candidates }))
}
fn parse_reg_snap(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = keyword("RegSnap")(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_occlusion_label(input: &str) -> IResult<&str, Option<String>> {
let Ok((rest, _)) = ws::<_, _, Error<&str>>(c_char('(')).parse(input) else {
return Ok((input, None));
};
let (rest, label) = cut(ws(rule_name)).parse(rest)?;
let (rest, _) = cut(ws(c_char(')'))).parse(rest)?;
Ok((rest, Some(label)))
}
fn parse_occlusion_body(input: &str) -> IResult<&str, (RuleCall, Option<String>)> {
let (input, label) = parse_occlusion_label(input)?;
let (input, _) = cut(ws(c_char('{'))).parse(input)?;
let (input, rule) = cut(ws(parse_rule_call)).parse(input)?;
let (input, _) = cut(ws(c_char('}'))).parse(input)?;
Ok((input, (rule, label)))
}
fn parse_if_clear(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = keyword("IfClear")(input)?;
let (input, (rule, label)) = parse_occlusion_body(input)?;
Ok((input, ShapeOp::IfClear { rule, label }))
}
fn parse_if_occluded(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = keyword("IfOccluded")(input)?;
let (input, (rule, label)) = parse_occlusion_body(input)?;
Ok((input, ShapeOp::IfOccluded { rule, label }))
}
fn parse_if_inside(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = keyword("IfInside")(input)?;
let (input, (rule, label)) = parse_occlusion_body(input)?;
Ok((input, ShapeOp::IfInside { rule, label }))
}
fn parse_if_touches(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = keyword("IfTouches")(input)?;
let (input, (rule, label)) = parse_occlusion_body(input)?;
Ok((input, ShapeOp::IfTouches { rule, label }))
}
fn parse_label(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = keyword("Label")(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::Label(label)))
}
fn parse_pick(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = keyword("Pick")(input)?;
let (input, _) = cut(ws(c_char('('))).parse(input)?;
let (input, key) = cut(ws(rule_name)).parse(input)?;
let (input, _) = cut(ws(c_char(')'))).parse(input)?;
let (input, _) = cut(ws(c_char('{'))).parse(input)?;
let mut choices = Vec::new();
let mut remaining = input;
loop {
if choices.len() >= MAX_VARIANTS {
return Err(nom::Err::Failure(Error::new(
remaining,
ErrorKind::TooLarge,
)));
}
let (after_w, w) = cut(ws(parse_weight_prefix)).parse(remaining)?;
if !w.is_finite() || w < 0.0 {
return Err(nom::Err::Failure(Error::new(after_w, ErrorKind::Verify)));
}
let (after_rule, rule) = cut(ws(parse_rule_call)).parse(after_w)?;
choices.push((w / 100.0, rule));
if let Ok((after_sep, _)) = ws::<_, _, Error<&str>>(c_char('|')).parse(after_rule) {
remaining = after_sep;
continue;
}
remaining = after_rule;
break;
}
let (remaining, _) = cut(ws(c_char('}'))).parse(remaining)?;
Ok((remaining, ShapeOp::Pick { key, choices }))
}
fn parse_scatter(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = keyword("Scatter")(input)?;
let (input, _) = cut(ws(c_char('('))).parse(input)?;
let (input, kind) = cut(ws(identifier)).parse(input)?;
let volume = match kind {
"Top" | "top" => false,
"Volume" | "volume" => true,
_ => return Err(nom::Err::Failure(Error::new(input, ErrorKind::Tag))),
};
let (input, _) = cut(ws(c_char(','))).parse(input)?;
let (input, count) = cut(arg_expr).parse(input)?;
let (input, _) = cut(ws(c_char(')'))).parse(input)?;
let (input, _) = cut(ws(c_char('{'))).parse(input)?;
let (input, rule) = cut(ws(parse_rule_call)).parse(input)?;
let (input, _) = cut(ws(c_char('}'))).parse(input)?;
Ok((
input,
ShapeOp::Scatter {
volume,
count,
rule,
},
))
}
const MAX_REPEAT_TILE_SIZES: usize = 256;
fn check_tile_lit<'a>(e: &Expr, at: &'a str) -> Result<(), nom::Err<Error<&'a str>>> {
if let Some(v) = e.as_lit()
&& v <= 0.0
{
return Err(nom::Err::Failure(Error::new(at, ErrorKind::Verify)));
}
Ok(())
}
fn parse_repeat_tile_sizes(input: &str) -> IResult<&str, Vec<Expr>> {
if let Ok((rest, _)) = ws::<_, _, Error<&str>>(c_char('[')).parse(input) {
let (rest, first) = cut(arg_expr).parse(rest)?;
check_tile_lit(&first, rest)?;
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(arg_expr).parse(after_comma)?;
check_tile_lit(&v, after_num)?;
sizes.push(v);
remaining = after_num;
}
let (remaining, _) = cut(ws(c_char(']'))).parse(remaining)?;
return Ok((remaining, sizes));
}
let (rest, v) = arg_expr(input)?;
check_tile_lit(&v, rest)?;
Ok((rest, vec![v]))
}
fn parse_repeat(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = keyword("Repeat")(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(parse_rule_call)).parse(input)?;
let (input, _) = cut(ws(c_char('}'))).parse(input)?;
Ok((
input,
ShapeOp::Repeat {
axis,
tile_sizes,
rule,
},
))
}
fn parse_comp_edge_case(input: &str) -> IResult<&str, crate::ops::CompEdgeCase> {
let (input, sel_str) = ws(identifier).parse(input)?;
let selector = crate::ops::EdgeSelector::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(parse_rule_call).parse(input)?;
Ok((input, crate::ops::CompEdgeCase { selector, rule }))
}
fn parse_comp(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = keyword("Comp")(input)?;
let (input, _) = cut(ws(c_char('('))).parse(input)?;
let (input, kind) = cut(ws(identifier)).parse(input)?;
let edges = match kind {
"Faces" | "faces" => false,
"Edges" | "edges" => true,
_ => return Err(nom::Err::Failure(Error::new(input, ErrorKind::Tag))),
};
let (input, _) = cut(ws(c_char(')'))).parse(input)?;
let (input, _) = cut(ws(c_char('{'))).parse(input)?;
if edges {
let (mut remaining, first) = cut(ws(parse_comp_edge_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_edge_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)?;
return Ok((remaining, ShapeOp::Comp(CompTarget::Edges(cases))));
}
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::<_, _, Error<&str>>("I").parse(input)?;
if input.chars().next().is_some_and(is_ident_char) {
return Err(nom::Err::Error(Error::new(input, ErrorKind::Tag)));
}
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, _) = keyword("Polygon")(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, _) = keyword("Mat")(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, _) = keyword("Align")(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(parse_rule_call).parse(input)?;
Ok((input, OffsetCase { selector, rule }))
}
fn parse_offset(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = keyword("Offset")(input)?;
let (input, distance) =
cut(delimited(ws(c_char('(')), arg_expr, 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(parse_rule_call).parse(input)?;
Ok((input, RoofCase { selector, rule }))
}
#[derive(Default)]
struct RoofNamed {
overhang: Option<Expr>,
offset: Option<Expr>,
tier: Option<Expr>,
fascia: Option<Expr>,
secondary: Option<Expr>,
height: Option<Expr>,
ridge: Option<Axis>,
}
fn parse_roof_named_params(input: &str) -> IResult<&str, RoofNamed> {
let mut remaining = input;
let mut named = RoofNamed::default();
loop {
let Ok((after_comma, _)) = ws::<_, _, Error<&str>>(c_char(',')).parse(remaining) else {
break;
};
let Ok((after_eq, (key, _))) = (
ws::<_, _, Error<&str>>(identifier),
ws::<_, _, Error<&str>>(c_char('=')),
)
.parse(after_comma)
else {
break;
};
if key == "ridge" {
let (rest, axis) = cut(ws(parse_axis)).parse(after_eq)?;
if axis == Axis::Y {
return Err(nom::Err::Failure(Error::new(after_eq, ErrorKind::Verify)));
}
named.ridge = Some(axis);
remaining = rest;
continue;
}
let slot = match key {
"overhang" => &mut named.overhang,
"offset" => &mut named.offset,
"tier" => &mut named.tier,
"fascia" => &mut named.fascia,
"secondary" => &mut named.secondary,
"height" => &mut named.height,
_ => break,
};
let (rest, e) = cut(arg_expr).parse(after_eq)?;
*slot = Some(e);
remaining = rest;
}
Ok((remaining, named))
}
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, _) = keyword("Roof")(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 named_ahead = |s: &str| {
(
ws::<_, _, Error<&str>>(identifier),
ws::<_, _, Error<&str>>(c_char('=')),
)
.parse(s)
.is_ok()
};
let mut pitch: Option<Expr> = None;
let mut second_positional: Option<Expr> = None;
let mut remaining = input;
if let Ok((after_comma, _)) = ws::<_, _, Error<&str>>(c_char(',')).parse(remaining)
&& !named_ahead(after_comma)
{
let (rest, p) = cut(arg_expr).parse(after_comma)?;
if let Some(v) = p.as_lit()
&& (v <= 0.0 || v >= 90.0)
{
return Err(nom::Err::Failure(Error::new(rest, ErrorKind::Verify)));
}
pitch = Some(p);
remaining = rest;
if let Ok((after_c2, _)) = ws::<_, _, Error<&str>>(c_char(',')).parse(remaining)
&& !named_ahead(after_c2)
{
let (rest2, v) = cut(arg_expr).parse(after_c2)?;
second_positional = Some(v);
remaining = rest2;
}
}
let uses_secondary = roof_type_uses_secondary_pitch(roof_type);
let mut secondary_pitch: Option<Expr> = None;
let mut overhang: Option<Expr> = None;
if let Some(v) = second_positional {
if uses_secondary {
secondary_pitch = Some(v);
} else {
if let Some(lit) = v.as_lit()
&& lit < 0.0
{
return Err(nom::Err::Failure(Error::new(remaining, ErrorKind::Verify)));
}
overhang = Some(v);
}
}
let (input, named) = parse_roof_named_params(remaining)?;
if let Some(v) = named.overhang {
if let Some(lit) = v.as_lit()
&& lit < 0.0
{
return Err(nom::Err::Failure(Error::new(input, ErrorKind::Verify)));
}
overhang = Some(v);
}
if let Some(v) = named.secondary {
secondary_pitch = Some(v);
}
let (input, _) = cut(ws(c_char(')'))).parse(input)?;
let Some(pitch) = pitch.or_else(|| named.height.is_some().then(|| Expr::lit(45.0))) else {
return Err(nom::Err::Failure(Error::new(input, ErrorKind::Verify)));
};
let spec = RoofSpec {
roof_type,
pitch,
height: named.height,
secondary_pitch,
overhang: overhang.unwrap_or_else(|| Expr::lit(0.0)),
ridge_offset: named.offset.unwrap_or_else(|| Expr::lit(0.5)),
fascia_depth: named.fascia.unwrap_or_else(|| Expr::lit(0.0)),
tier_height: named.tier,
ridge_axis: named.ridge,
};
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 { spec, 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(parse_rule_call).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, _) = keyword("Attach")(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_size(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = keyword("Size")(input)?;
let (input, v) = cut(ws(parse_expr3)).parse(input)?;
for c in &v {
if let Some(lit) = c.as_lit()
&& lit < 0.0
{
return Err(nom::Err::Failure(Error::new(input, ErrorKind::Verify)));
}
}
Ok((input, ShapeOp::Size(v)))
}
fn parse_center(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = keyword("Center")(input)?;
let (input, _) = cut(ws(c_char('('))).parse(input)?;
let (input, mask) = cut(ws(identifier)).parse(input)?;
let (mut x, mut y, mut z) = (false, false, false);
for ch in mask.chars() {
match ch {
'X' | 'x' => x = true,
'Y' | 'y' => y = true,
'Z' | 'z' => z = true,
_ => return Err(nom::Err::Failure(Error::new(input, ErrorKind::Verify))),
}
}
if !(x || y || z) {
return Err(nom::Err::Failure(Error::new(input, ErrorKind::Verify)));
}
let (input, _) = cut(ws(c_char(')'))).parse(input)?;
Ok((input, ShapeOp::Center { x, y, z }))
}
fn parse_mirror(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = keyword("Mirror")(input)?;
let (input, _) = cut(ws(c_char('('))).parse(input)?;
let (input, axis) = cut(ws(parse_axis)).parse(input)?;
if axis != Axis::X {
return Err(nom::Err::Failure(Error::new(input, ErrorKind::Verify)));
}
let (input, _) = cut(ws(c_char(')'))).parse(input)?;
Ok((input, ShapeOp::Mirror))
}
fn parse_carve_case(input: &str) -> IResult<&str, CarveCase> {
let (input, sel_str) = ws(identifier).parse(input)?;
let selector = CarveSelector::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(parse_rule_call).parse(input)?;
Ok((input, CarveCase { selector, rule }))
}
fn parse_carve_cases(input: &str) -> IResult<&str, Vec<CarveCase>> {
let (input, _) = cut(ws(c_char('{'))).parse(input)?;
let (mut remaining, first) = cut(ws(parse_carve_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;
};
let (after_item, case) = cut(ws(parse_carve_case)).parse(after_sep)?;
cases.push(case);
remaining = after_item;
}
let (remaining, _) = cut(ws(c_char('}'))).parse(remaining)?;
Ok((remaining, cases))
}
fn parse_shape_l(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = keyword("ShapeL")(input)?;
let (input, _) = cut(ws(c_char('('))).parse(input)?;
let (input, front) = cut(arg_expr).parse(input)?;
let (input, _) = cut(ws(c_char(','))).parse(input)?;
let (input, side) = cut(arg_expr).parse(input)?;
let (input, _) = cut(ws(c_char(')'))).parse(input)?;
let (input, cases) = parse_carve_cases(input)?;
Ok((input, ShapeOp::ShapeL { front, side, cases }))
}
fn parse_shape_u(input: &str) -> IResult<&str, ShapeOp> {
let (input, _) = keyword("ShapeU")(input)?;
let (input, _) = cut(ws(c_char('('))).parse(input)?;
let (input, front) = cut(arg_expr).parse(input)?;
let (input, _) = cut(ws(c_char(','))).parse(input)?;
let (input, left) = cut(arg_expr).parse(input)?;
let (input, _) = cut(ws(c_char(','))).parse(input)?;
let (input, right) = cut(arg_expr).parse(input)?;
let (input, _) = cut(ws(c_char(')'))).parse(input)?;
let (input, cases) = parse_carve_cases(input)?;
Ok((
input,
ShapeOp::ShapeU {
front,
left,
right,
cases,
},
))
}
fn parse_rule_ref(input: &str) -> IResult<&str, ShapeOp> {
map(ws(parse_rule_call), ShapeOp::Rule).parse(input)
}
fn parse_op(input: &str) -> IResult<&str, ShapeOp> {
alt((
alt((
parse_extrude,
parse_taper,
parse_rotate,
parse_translate,
parse_scale,
parse_split_area,
parse_split,
parse_fit,
parse_shape_l,
parse_shape_u,
parse_size,
parse_center,
parse_mirror,
parse_repeat,
parse_comp,
)),
alt((
parse_if_clear,
parse_if_occluded,
parse_if_inside,
parse_if_touches,
parse_label,
parse_scatter,
parse_pick,
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::SplitArea { .. }
| ShapeOp::Fit { .. }
| ShapeOp::ShapeL { .. }
| ShapeOp::ShapeU { .. }
| ShapeOp::Comp(_)
| ShapeOp::Repeat { .. }
| ShapeOp::Offset { .. }
| ShapeOp::Roof { .. }
| ShapeOp::Attach { .. }
| ShapeOp::Scatter { .. }
| ShapeOp::Pick { .. }
)
}
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 params: Vec<String>,
pub variants: Vec<RuleVariant>,
}
impl GrammarRule {
pub fn ops(&self) -> &[ShapeOp] {
self.variants
.first()
.map(|v| v.ops.as_slice())
.unwrap_or(&[])
}
}
fn try_parse_when(part: &str) -> Option<Result<(Expr, &str), ShapeError>> {
let trimmed = part.trim_start();
let rest = trimmed.strip_prefix("when")?;
if rest.chars().next().is_some_and(is_ident_char) {
return None;
}
let parse = || -> Result<(Expr, &str), ShapeError> {
let (rest2, _) = ws::<_, _, Error<&str>>(c_char('('))
.parse(rest)
.map_err(|e| ShapeError::ParseError(e.to_string()))?;
let (rest3, cond) = arg_expr(rest2).map_err(|e| ShapeError::ParseError(e.to_string()))?;
let (rest4, _) = ws::<_, _, Error<&str>>(c_char(')'))
.parse(rest3)
.map_err(|e| ShapeError::ParseError(e.to_string()))?;
let (rest5, _) = ws::<_, _, Error<&str>>(c_char(':'))
.parse(rest4)
.map_err(|e| ShapeError::ParseError(e.to_string()))?;
Ok((cond, rest5))
};
Some(parse())
}
fn try_parse_else(part: &str) -> Option<&str> {
let trimmed = part.trim_start();
let rest = trimmed.strip_prefix("else")?;
if rest.chars().next().is_some_and(is_ident_char) {
return None;
}
let (rest2, _) = ws::<_, _, Error<&str>>(c_char(':')).parse(rest).ok()?;
Some(rest2)
}
fn parse_param_decl(input: &str) -> Result<(Vec<String>, &str), ShapeError> {
let Ok((rest, _)) = ws::<_, _, Error<&str>>(c_char('(')).parse(input) else {
return Ok((Vec::new(), input));
};
let mut params = Vec::new();
let mut remaining = rest;
if let Ok((after, _)) = ws::<_, _, Error<&str>>(c_char(')')).parse(remaining) {
return Ok((params, after));
}
loop {
let (after, p) = ws(identifier)
.parse(remaining)
.map_err(|e| ShapeError::ParseError(e.to_string()))?;
if params.contains(&p.to_string()) {
return Err(ShapeError::ParseError(format!(
"duplicate rule parameter name: {p}"
)));
}
params.push(p.to_string());
if params.len() > MAX_RULE_ARGS {
return Err(ShapeError::CapacityOverflow);
}
if let Ok((after2, _)) = ws::<_, _, Error<&str>>(c_char(',')).parse(after) {
remaining = after2;
continue;
}
let (after2, _) = ws::<_, _, Error<&str>>(c_char(')'))
.parse(after)
.map_err(|e| ShapeError::ParseError(e.to_string()))?;
return Ok((params, after2));
}
}
#[derive(Debug, Clone)]
pub enum Statement {
Rule(GrammarRule),
Attr {
name: String,
value: f64,
},
Const {
name: String,
value: f64,
},
Style {
name: String,
extends: Option<String>,
overrides: Vec<(String, f64)>,
},
}
fn const_eval(e: &Expr, at: &str) -> Result<f64, ShapeError> {
fn check(e: &Expr) -> bool {
match e {
Expr::Lit(_) => true,
Expr::Var(_) => false,
Expr::Unary(_, a) => check(a),
Expr::Binary(_, a, b) => check(a) && check(b),
Expr::Call(f, args) => *f != crate::expr::Func::Rand && args.iter().all(check),
}
}
if !check(e) {
return Err(ShapeError::ParseError(format!(
"declaration value must be a constant expression (no variables, no rand): {at:?}"
)));
}
let mut rng = rand_pcg::Pcg64::new(0, 0);
let globals = std::collections::HashMap::new();
let mut ctx = crate::expr::EvalCtx {
scope_size: crate::scope::Vec3::ZERO,
split_i: 0.0,
split_n: 1.0,
depth: 0.0,
params: &[],
globals: &globals,
rng: &mut rng,
};
e.eval(&mut ctx)
}
fn parse_style_overrides(input: &str) -> Result<Vec<(String, f64)>, ShapeError> {
let mut out = Vec::new();
let mut remaining = input;
loop {
let (rest, name) = ws::<_, _, Error<&str>>(identifier)
.parse(remaining)
.map_err(|e| ShapeError::ParseError(e.to_string()))?;
let (rest, _) = ws::<_, _, Error<&str>>(c_char('='))
.parse(rest)
.map_err(|e| ShapeError::ParseError(e.to_string()))?;
let (rest, value_expr) =
arg_expr(rest).map_err(|e| ShapeError::ParseError(e.to_string()))?;
out.push((name.to_string(), const_eval(&value_expr, name)?));
if out.len() > MAX_SPLIT_SLOTS {
return Err(ShapeError::CapacityOverflow);
}
if let Ok((rest2, _)) = ws::<_, _, Error<&str>>(c_char(',')).parse(rest) {
remaining = rest2;
continue;
}
let (rest2, _) = ws::<_, _, Error<&str>>(c_char('}'))
.parse(rest)
.map_err(|e| ShapeError::ParseError(e.to_string()))?;
let (rest3, _) = space_or_comment::<Error<&str>>(rest2)
.map_err(|e| ShapeError::ParseError(e.to_string()))?;
if !rest3.is_empty() {
return Err(ShapeError::ParseError(format!(
"trailing input after style block: {rest3:?}"
)));
}
return Ok(out);
}
}
pub fn parse_statement(input: &str) -> Result<Statement, ShapeError> {
let (rest, _) = space_or_comment::<Error<&str>>(input)
.map_err(|e| ShapeError::ParseError(e.to_string()))?;
for (kw, is_attr) in [("attr", true), ("const", false)] {
if let Some(after) = rest.strip_prefix(kw)
&& !after.chars().next().is_some_and(is_ident_char)
{
let (after2, name) = ws::<_, _, Error<&str>>(identifier)
.parse(after)
.map_err(|e| ShapeError::ParseError(e.to_string()))?;
let (after3, _) = ws::<_, _, Error<&str>>(c_char('='))
.parse(after2)
.map_err(|e| ShapeError::ParseError(e.to_string()))?;
let (after4, value_expr) =
arg_expr(after3).map_err(|e| ShapeError::ParseError(e.to_string()))?;
let (after5, _) = space_or_comment::<Error<&str>>(after4)
.map_err(|e| ShapeError::ParseError(e.to_string()))?;
if !after5.is_empty() {
return Err(ShapeError::ParseError(format!(
"trailing input after declaration: {after5:?}"
)));
}
let value = const_eval(&value_expr, name)?;
return Ok(if is_attr {
Statement::Attr {
name: name.to_string(),
value,
}
} else {
Statement::Const {
name: name.to_string(),
value,
}
});
}
}
if let Some(after) = rest.strip_prefix("style")
&& !after.chars().next().is_some_and(is_ident_char)
{
let (after2, name) = ws::<_, _, Error<&str>>(identifier)
.parse(after)
.map_err(|e| ShapeError::ParseError(e.to_string()))?;
let (after3, extends) =
if let Ok((a, _)) = ws::<_, _, Error<&str>>(tag("extends")).parse(after2) {
let (a2, base) = ws::<_, _, Error<&str>>(identifier)
.parse(a)
.map_err(|e| ShapeError::ParseError(e.to_string()))?;
(a2, Some(base.to_string()))
} else {
(after2, None)
};
let (after4, _) = ws::<_, _, Error<&str>>(c_char('{'))
.parse(after3)
.map_err(|e| ShapeError::ParseError(e.to_string()))?;
let overrides = parse_style_overrides(after4)?;
return Ok(Statement::Style {
name: name.to_string(),
extends,
overrides,
});
}
Ok(Statement::Rule(parse_rule(input)?))
}
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 (params, remaining) = parse_param_decl(remaining)?;
let (remaining, _) = ws(tag::<_, _, Error<&str>>("-->"))
.parse(remaining)
.map_err(|e| ShapeError::ParseError(e.to_string()))?;
let parts = split_top_level_pipe(remaining)?;
let mut variants: Vec<RuleVariant> = Vec::with_capacity(parts.len());
let multi = parts.len() > 1;
for (i, part) in parts.iter().enumerate() {
let is_last = i + 1 == parts.len();
if let Some(res) = try_parse_when(part) {
let (cond, rest) = res?;
variants.push(RuleVariant {
selector: VariantSelector::When(cond),
ops: parse_ops(rest)?,
});
continue;
}
if let Some(rest) = try_parse_else(part) {
if !is_last {
return Err(ShapeError::ParseError(
"`else:` must be the last variant".to_string(),
));
}
variants.push(RuleVariant {
selector: VariantSelector::Else,
ops: parse_ops(rest)?,
});
continue;
}
match try_parse_weight(part) {
Some((weight, rest)) => variants.push(RuleVariant {
selector: VariantSelector::Weight(weight),
ops: parse_ops(rest)?,
}),
None if multi => {
return Err(ShapeError::ParseError(format!(
"rule alternative needs a 'weight%', 'when(..):', or 'else:' prefix: {part:?}"
)));
}
None => variants.push(RuleVariant {
selector: VariantSelector::Weight(1.0),
ops: parse_ops(part)?,
}),
}
}
let n_weight = variants
.iter()
.filter(|v| matches!(v.selector, VariantSelector::Weight(_)))
.count();
let n_when = variants
.iter()
.filter(|v| matches!(v.selector, VariantSelector::When(_)))
.count();
let has_else = variants
.iter()
.any(|v| matches!(v.selector, VariantSelector::Else));
if n_weight > 0 && n_when > 0 {
return Err(ShapeError::ParseError(
"rule mixes weighted and guarded variants".to_string(),
));
}
if n_weight > 0 && has_else {
let used: f64 = variants
.iter()
.filter_map(|v| match v.selector {
VariantSelector::Weight(w) => Some(w),
_ => None,
})
.sum();
let remainder = 1.0 - used;
if remainder <= 1e-9 {
return Err(ShapeError::ParseError(format!(
"`else:` has no probability mass left (weights already sum to {:.0}%)",
used * 100.0
)));
}
if let Some(last) = variants.last_mut() {
last.selector = VariantSelector::Weight(remainder);
}
}
Ok(GrammarRule {
name: name.to_string(),
params,
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(Expr::lit(10.0)));
}
#[test]
fn test_parse_taper() {
let ops = parse_ops("Taper(0.5)").unwrap();
assert_eq!(ops[0], ShapeOp::Taper(Expr::lit(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, entries, .. } = &ops[0] else {
panic!("expected Split");
};
let slots: Vec<_> = entries.iter().filter_map(|e| e.as_slot()).collect();
assert_eq!(*axis, Axis::Y);
assert_eq!(slots.len(), 3);
assert_eq!(slots[0].size, SplitSize::float(1.0));
assert_eq!(slots[0].rule, "Floor");
assert_eq!(slots[2].size, SplitSize::abs(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 { entries, .. } = &ops[0] else {
panic!("expected Split");
};
let slots: Vec<_> = entries.iter().filter_map(|e| e.as_slot()).collect();
assert_eq!(slots[0].size, SplitSize::rel(0.3));
assert_eq!(slots[1].size, SplitSize::rel(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".into()));
}
#[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].weight().unwrap() - 1.0).abs() < 1e-9);
assert_eq!(rule.variants[0].ops.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].weight().unwrap() - 0.70).abs() < 1e-9);
assert_eq!(
rule.variants[0].ops,
vec![ShapeOp::Rule("BrickWall".into())]
);
assert!((rule.variants[1].weight().unwrap() - 0.30).abs() < 1e-9);
assert_eq!(
rule.variants[1].ops,
vec![ShapeOp::Rule("GlassCurtain".into())]
);
}
#[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].ops.len(), 1); assert_eq!(rule.variants[1].ops.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(),
params: vec![],
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].ops,
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].weight().unwrap() - 0.50).abs() < 1e-9);
assert!((rule.variants[1].weight().unwrap() - 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].weight().unwrap() - 1.0).abs() < 1e-9);
assert_eq!(
rule.variants[0].ops,
vec![ShapeOp::Extrude(Expr::lit(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.as_lit().unwrap() - (-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 { spec, cases } = &ops[0] else {
panic!("expected Roof");
};
assert_eq!(spec.roof_type, RoofType::Gable);
assert!((spec.pitch.as_lit().unwrap() - 30.0).abs() < 1e-9);
assert!(spec.overhang.as_lit().unwrap().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 { spec, .. } = &ops[0] else {
panic!("expected Roof");
};
assert_eq!(spec.roof_type, RoofType::Hip);
assert!((spec.pitch.as_lit().unwrap() - 45.0).abs() < 1e-9);
assert!((spec.overhang.as_lit().unwrap() - 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());
}
}