use symbios_shape::grammar::parse_ops;
use symbios_shape::ops::ShapeOp;
use symbios_shape::{FaceProfile, Interpreter, Quat, Scope, Vec3};
fn footprint() -> Scope {
Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(4.0, 1.0, 4.0))
}
#[test]
fn parser_accepts_l_shape_polygon() {
let ops = parse_ops("Polygon((0,0), (4,0), (4,2), (2,2), (2,4), (0,4)) I(\"Mass\")").unwrap();
assert_eq!(ops.len(), 2);
match &ops[0] {
ShapeOp::Polygon(verts) => {
assert_eq!(verts.len(), 6);
assert!((verts[0] - glam::DVec2::new(0.0, 0.0)).length() < 1e-9);
assert!((verts[3] - glam::DVec2::new(2.0, 2.0)).length() < 1e-9);
}
_ => panic!("expected Polygon op, got {:?}", ops[0]),
}
}
#[test]
fn parser_accepts_three_vertex_minimum() {
let ops = parse_ops("Polygon((0,0), (1,0), (0,1)) I(\"Tri\")").unwrap();
assert!(matches!(&ops[0], ShapeOp::Polygon(verts) if verts.len() == 3));
}
#[test]
fn parser_rejects_two_vertex_polygon() {
assert!(parse_ops("Polygon((0,0), (1,0)) I(\"X\")").is_err());
}
#[test]
fn parser_rejects_single_vertex_polygon() {
assert!(parse_ops("Polygon((0,0)) I(\"X\")").is_err());
}
#[test]
fn parser_rejects_empty_polygon() {
assert!(parse_ops("Polygon() I(\"X\")").is_err());
}
#[test]
fn parser_rejects_non_finite_vertex() {
assert!(parse_ops("Polygon((0,0), (1,0), (inf,1)) I(\"X\")").is_err());
}
#[test]
fn polygon_op_stamps_face_profile_on_terminal() {
let mut interp = Interpreter::new();
interp.add_rule(
"R",
parse_ops("Polygon((0,0), (4,0), (4,2), (2,2), (2,4), (0,4)) I(\"Mass\")").unwrap(),
);
let model = interp.derive(footprint(), "R").unwrap();
assert_eq!(model.len(), 1);
match &model.terminals[0].face_profile {
FaceProfile::Polygon(verts) => {
assert_eq!(verts.len(), 6);
}
other => panic!("expected Polygon profile, got {other:?}"),
}
}
#[test]
fn polygon_op_overrides_taper() {
let mut interp = Interpreter::new();
interp.add_rule(
"R",
parse_ops("Taper(0.5) Polygon((0,0), (1,0), (0,1)) I(\"X\")").unwrap(),
);
let model = interp.derive(footprint(), "R").unwrap();
assert!(matches!(
model.terminals[0].face_profile,
FaceProfile::Polygon(_)
));
}
#[test]
fn polygon_op_propagates_through_implicit_terminal() {
let mut interp = Interpreter::new();
interp.add_rule(
"R",
parse_ops("Polygon((0,0), (2,0), (2,2), (0,2)) Floor").unwrap(),
);
let model = interp.derive(footprint(), "R").unwrap();
assert_eq!(model.len(), 1);
assert_eq!(model.terminals[0].mesh_id, "Floor");
assert!(matches!(
model.terminals[0].face_profile,
FaceProfile::Polygon(_)
));
}
#[test]
fn polygon_op_then_density_yields_mass_properties() {
let mut interp = Interpreter::new();
interp.add_rule(
"R",
parse_ops(r#"Mat("Concrete", 2400) Polygon((0,0), (4,0), (4,4), (0,4)) I("Slab")"#)
.unwrap(),
);
let model = interp.derive(footprint(), "R").unwrap();
let mp = model.terminals[0]
.mass_properties
.as_ref()
.expect("polygon + density → mass_properties present");
assert!(
(mp.mass - 38_400.0).abs() < 1e-6,
"expected 38400 kg, got {}",
mp.mass
);
}
#[test]
fn direct_shape_op_polygon_construction() {
let verts = vec![
glam::DVec2::new(0.0, 0.0),
glam::DVec2::new(2.0, 0.0),
glam::DVec2::new(2.0, 2.0),
glam::DVec2::new(0.0, 2.0),
];
let mut interp = Interpreter::new();
interp.add_rule(
"R",
vec![ShapeOp::Polygon(verts), ShapeOp::I("Mass".to_string())],
);
let model = interp.derive(footprint(), "R").unwrap();
assert!(matches!(
model.terminals[0].face_profile,
FaceProfile::Polygon(ref v) if v.len() == 4
));
}