use symbios_shape::grammar::parse_ops;
use symbios_shape::ops::{AttachCase, AttachSelector, ShapeOp};
use symbios_shape::{Interpreter, Quat, Scope, ShapeError, Vec3};
fn parent_scope() -> Scope {
Scope::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(2.0, 4.0, 6.0))
}
fn run_attach(world_axis: Vec3) -> Vec<symbios_shape::model::Terminal> {
let mut interp = Interpreter::new();
interp.add_rule(
"R",
vec![ShapeOp::Attach {
world_axis,
cases: vec![AttachCase {
selector: AttachSelector::Surface,
rule: "Dormer".into(),
}],
}],
);
interp.derive(parent_scope(), "R").unwrap().terminals
}
#[test]
fn attach_up_aligns_local_y_with_world_up() {
let t = run_attach(Vec3::Y);
assert_eq!(t.len(), 1);
let new_y = t[0].scope.rotation * Vec3::Y;
assert!((new_y - Vec3::Y).length() < 1e-9);
}
#[test]
fn attach_down_aligns_local_y_with_world_down() {
let t = run_attach(Vec3::NEG_Y);
assert_eq!(t.len(), 1);
let new_y = t[0].scope.rotation * Vec3::Y;
assert!((new_y - Vec3::NEG_Y).length() < 1e-9);
}
#[test]
fn attach_forward_aligns_local_y_with_negative_z() {
let t = run_attach(Vec3::NEG_Z);
let new_y = t[0].scope.rotation * Vec3::Y;
assert!((new_y - Vec3::NEG_Z).length() < 1e-9);
}
#[test]
fn attach_back_aligns_local_y_with_positive_z() {
let t = run_attach(Vec3::Z);
let new_y = t[0].scope.rotation * Vec3::Y;
assert!((new_y - Vec3::Z).length() < 1e-9);
}
#[test]
fn attach_right_aligns_local_y_with_positive_x() {
let t = run_attach(Vec3::X);
let new_y = t[0].scope.rotation * Vec3::Y;
assert!((new_y - Vec3::X).length() < 1e-9);
}
#[test]
fn attach_left_aligns_local_y_with_negative_x() {
let t = run_attach(Vec3::NEG_X);
let new_y = t[0].scope.rotation * Vec3::Y;
assert!((new_y - Vec3::NEG_X).length() < 1e-9);
}
#[test]
fn attach_arbitrary_axis_normalizes_internally() {
let t = run_attach(Vec3::new(2.0, 2.0, 0.0));
let new_y = t[0].scope.rotation * Vec3::Y;
let expected = Vec3::new(2.0, 2.0, 0.0).normalize();
assert!((new_y - expected).length() < 1e-9);
}
#[test]
fn attach_surface_inherits_xy_size_and_zero_z() {
let t = run_attach(Vec3::Y);
let s = t[0].scope.size;
assert!((s.x - 2.0).abs() < 1e-9);
assert!((s.y - 4.0).abs() < 1e-9);
assert!(s.z.abs() < 1e-9);
}
#[test]
fn attach_surface_origin_matches_parent() {
let t = run_attach(Vec3::Y);
assert!((t[0].scope.position - Vec3::ZERO).length() < 1e-9);
}
#[test]
fn attach_with_no_matching_case_emits_nothing() {
let mut interp = Interpreter::new();
interp.add_rule(
"R",
vec![ShapeOp::Attach {
world_axis: Vec3::Y,
cases: vec![],
}],
);
let model = interp.derive(parent_scope(), "R").unwrap();
assert!(model.is_empty());
}
#[test]
fn attach_all_selector_falls_through_to_surface() {
let mut interp = Interpreter::new();
interp.add_rule(
"R",
vec![ShapeOp::Attach {
world_axis: Vec3::Y,
cases: vec![AttachCase {
selector: AttachSelector::All,
rule: "Dormer".into(),
}],
}],
);
let model = interp.derive(parent_scope(), "R").unwrap();
assert_eq!(model.len(), 1);
assert_eq!(model.terminals[0].mesh_id, "Dormer");
}
#[test]
fn attach_zero_axis_rejected() {
let mut interp = Interpreter::new();
interp.add_rule(
"R",
vec![ShapeOp::Attach {
world_axis: Vec3::ZERO,
cases: vec![AttachCase {
selector: AttachSelector::Surface,
rule: "X".into(),
}],
}],
);
assert!(matches!(
interp.derive(parent_scope(), "R"),
Err(ShapeError::InvalidAlignTarget)
));
}
#[test]
fn attach_nan_axis_rejected() {
let mut interp = Interpreter::new();
interp.add_rule(
"R",
vec![ShapeOp::Attach {
world_axis: Vec3::new(f64::NAN, 1.0, 0.0),
cases: vec![AttachCase {
selector: AttachSelector::Surface,
rule: "X".into(),
}],
}],
);
assert!(matches!(
interp.derive(parent_scope(), "R"),
Err(ShapeError::InvalidAlignTarget)
));
}
#[test]
fn attach_via_grammar_with_named_axis() {
let ops = parse_ops("Attach(Up) { Surface: Dormer }").unwrap();
let mut interp = Interpreter::new();
interp.add_rule("R", ops);
let model = interp.derive(parent_scope(), "R").unwrap();
assert_eq!(model.len(), 1);
assert_eq!(model.terminals[0].mesh_id, "Dormer");
}