#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::cast_possible_wrap,
clippy::manual_assert,
clippy::vec_init_then_push,
clippy::print_stdout,
clippy::print_stderr
)]
use crate::kernel::BrepKernel;
fn parse_batch(result: &str) -> serde_json::Value {
serde_json::from_str(result).expect("batch result should be valid JSON")
}
fn assert_ok(parsed: &serde_json::Value, idx: usize) {
assert!(
parsed[idx].get("ok").is_some(),
"expected ok at index {idx}, got: {}",
parsed[idx]
);
}
fn assert_no_crash(parsed: &serde_json::Value, idx: usize, msg: &str) {
assert!(
parsed[idx].get("ok").is_some() || parsed[idx].get("error").is_some(),
"{msg}: got: {}",
parsed[idx]
);
}
fn ok_f64(parsed: &serde_json::Value, idx: usize) -> f64 {
parsed[idx]["ok"]
.as_f64()
.unwrap_or_else(|| panic!("expected ok f64 at index {idx}, got: {}", parsed[idx]))
}
fn ok_bbox(parsed: &serde_json::Value, idx: usize) -> [f64; 6] {
let arr = parsed[idx]["ok"]
.as_array()
.unwrap_or_else(|| panic!("expected ok array at index {idx}, got: {}", parsed[idx]));
assert_eq!(arr.len(), 6, "bbox should have 6 elements");
let mut out = [0.0; 6];
for (i, v) in arr.iter().enumerate() {
out[i] = v.as_f64().unwrap();
}
out
}
fn translate_matrix(x: f64, y: f64, z: f64) -> String {
format!("[1,0,0,{x}, 0,1,0,{y}, 0,0,1,{z}, 0,0,0,1]")
}
#[test]
fn compound_cut_4_cylinders() {
let mut k = BrepKernel::new();
let result = k.execute_batch(
r#"[
{"op": "makeBox", "args": {"width": 42, "height": 42, "depth": 5}},
{"op": "makeCylinder", "args": {"radius": 3.0, "height": 10.0}},
{"op": "copyAndTransformSolid", "args": {"solid": 1, "matrix": [1,0,0,5, 0,1,0,5, 0,0,1,-2.5, 0,0,0,1]}},
{"op": "copyAndTransformSolid", "args": {"solid": 1, "matrix": [1,0,0,37, 0,1,0,5, 0,0,1,-2.5, 0,0,0,1]}},
{"op": "copyAndTransformSolid", "args": {"solid": 1, "matrix": [1,0,0,5, 0,1,0,37, 0,0,1,-2.5, 0,0,0,1]}},
{"op": "copyAndTransformSolid", "args": {"solid": 1, "matrix": [1,0,0,37, 0,1,0,37, 0,0,1,-2.5, 0,0,0,1]}},
{"op": "compoundCut", "args": {"target": 0, "tools": [2, 3, 4, 5]}}
]"#,
);
let parsed = parse_batch(&result);
assert_no_crash(&parsed, 6, "compoundCut with 4 cylinders should not crash");
}
#[test]
fn compound_cut_wall_cutouts() {
let mut k = BrepKernel::new();
let result = k.execute_batch(
r#"[
{"op": "makeBox", "args": {"width": 20, "height": 20, "depth": 10}},
{"op": "makeBox", "args": {"width": 5, "height": 22, "depth": 5}},
{"op": "copyAndTransformSolid", "args": {"solid": 1, "matrix": [1,0,0,-2, 0,1,0,-1, 0,0,1,2.5, 0,0,0,1]}},
{"op": "copyAndTransformSolid", "args": {"solid": 1, "matrix": [1,0,0,17, 0,1,0,-1, 0,0,1,2.5, 0,0,0,1]}},
{"op": "makeBox", "args": {"width": 22, "height": 5, "depth": 5}},
{"op": "copyAndTransformSolid", "args": {"solid": 4, "matrix": [1,0,0,-1, 0,1,0,-2, 0,0,1,2.5, 0,0,0,1]}},
{"op": "copyAndTransformSolid", "args": {"solid": 4, "matrix": [1,0,0,-1, 0,1,0,17, 0,0,1,2.5, 0,0,0,1]}},
{"op": "compoundCut", "args": {"target": 0, "tools": [2, 3, 5, 6]}}
]"#,
);
let parsed = parse_batch(&result);
assert_no_crash(&parsed, 7, "compoundCut with wall cutouts should not crash");
}
#[test]
fn compound_cut_inserts_mixed() {
let mut k = BrepKernel::new();
let result = k.execute_batch(
r#"[
{"op": "makeBox", "args": {"width": 30, "height": 30, "depth": 8}},
{"op": "makeCylinder", "args": {"radius": 2.0, "height": 12.0}},
{"op": "copyAndTransformSolid", "args": {"solid": 1, "matrix": [1,0,0,15, 0,1,0,15, 0,0,1,-2, 0,0,0,1]}},
{"op": "makeBox", "args": {"width": 4, "height": 4, "depth": 12}},
{"op": "copyAndTransformSolid", "args": {"solid": 3, "matrix": [1,0,0,5, 0,1,0,5, 0,0,1,-2, 0,0,0,1]}},
{"op": "copyAndTransformSolid", "args": {"solid": 3, "matrix": [1,0,0,22, 0,1,0,22, 0,0,1,-2, 0,0,0,1]}},
{"op": "compoundCut", "args": {"target": 0, "tools": [2, 4, 5]}}
]"#,
);
let parsed = parse_batch(&result);
assert_no_crash(
&parsed,
6,
"compoundCut with mixed inserts should not crash",
);
}
#[test]
fn sequential_cut_many_dividers() {
let mut k = BrepKernel::new();
let result = k.execute_batch(
r#"[
{"op": "makeBox", "args": {"width": 40, "height": 40, "depth": 10}},
{"op": "makeBox", "args": {"width": 1, "height": 42, "depth": 12}},
{"op": "copyAndTransformSolid", "args": {"solid": 1, "matrix": [1,0,0,9.5, 0,1,0,-1, 0,0,1,-1, 0,0,0,1]}},
{"op": "copyAndTransformSolid", "args": {"solid": 1, "matrix": [1,0,0,19.5, 0,1,0,-1, 0,0,1,-1, 0,0,0,1]}},
{"op": "copyAndTransformSolid", "args": {"solid": 1, "matrix": [1,0,0,29.5, 0,1,0,-1, 0,0,1,-1, 0,0,0,1]}},
{"op": "makeBox", "args": {"width": 42, "height": 1, "depth": 12}},
{"op": "copyAndTransformSolid", "args": {"solid": 5, "matrix": [1,0,0,-1, 0,1,0,9.5, 0,0,1,-1, 0,0,0,1]}},
{"op": "copyAndTransformSolid", "args": {"solid": 5, "matrix": [1,0,0,-1, 0,1,0,19.5, 0,0,1,-1, 0,0,0,1]}},
{"op": "copyAndTransformSolid", "args": {"solid": 5, "matrix": [1,0,0,-1, 0,1,0,29.5, 0,0,1,-1, 0,0,0,1]}},
{"op": "cut", "args": {"solidA": 0, "solidB": 2}},
{"op": "cut", "args": {"solidA": 9, "solidB": 3}},
{"op": "cut", "args": {"solidA": 10, "solidB": 4}},
{"op": "cut", "args": {"solidA": 11, "solidB": 6}},
{"op": "cut", "args": {"solidA": 12, "solidB": 7}},
{"op": "cut", "args": {"solidA": 13, "solidB": 8}}
]"#,
);
let parsed = parse_batch(&result);
for i in 9..=14 {
assert_no_crash(&parsed, i, &format!("sequential cut step {i}"));
}
}
#[test]
fn compound_cut_slotted() {
let mut k = BrepKernel::new();
let result = k.execute_batch(
r#"[
{"op": "makeBox", "args": {"width": 30, "height": 10, "depth": 5}},
{"op": "makeCylinder", "args": {"radius": 1.0, "height": 8.0}},
{"op": "copyAndTransformSolid", "args": {"solid": 1, "matrix": [1,0,0,5, 0,1,0,5, 0,0,1,-1.5, 0,0,0,1]}},
{"op": "copyAndTransformSolid", "args": {"solid": 1, "matrix": [1,0,0,10, 0,1,0,5, 0,0,1,-1.5, 0,0,0,1]}},
{"op": "copyAndTransformSolid", "args": {"solid": 1, "matrix": [1,0,0,15, 0,1,0,5, 0,0,1,-1.5, 0,0,0,1]}},
{"op": "copyAndTransformSolid", "args": {"solid": 1, "matrix": [1,0,0,20, 0,1,0,5, 0,0,1,-1.5, 0,0,0,1]}},
{"op": "copyAndTransformSolid", "args": {"solid": 1, "matrix": [1,0,0,25, 0,1,0,5, 0,0,1,-1.5, 0,0,0,1]}},
{"op": "compoundCut", "args": {"target": 0, "tools": [2, 3, 4, 5, 6]}}
]"#,
);
let parsed = parse_batch(&result);
assert_no_crash(&parsed, 7, "compoundCut with 5 slot cylinders");
}
#[test]
fn compound_cut_honeycomb() {
let mut k = BrepKernel::new();
let mut ops: Vec<String> = vec![
r#"{"op": "makeBox", "args": {"width": 30, "height": 30, "depth": 3}}"#.to_string(),
r#"{"op": "makeCylinder", "args": {"radius": 2.0, "height": 6.0}}"#.to_string(),
];
let mut tool_handles = Vec::new();
let mut handle = 2u32;
for row in 0..3 {
for col in 0..3 {
let x = 5.0 + col as f64 * 10.0;
let y = 5.0 + row as f64 * 10.0;
let mat = translate_matrix(x, y, -1.5);
ops.push(format!(
r#"{{"op": "copyAndTransformSolid", "args": {{"solid": 1, "matrix": {mat}}}}}"#
));
tool_handles.push(handle);
handle += 1;
}
}
let tools_json = serde_json::to_string(&tool_handles).unwrap();
ops.push(format!(
r#"{{"op": "compoundCut", "args": {{"target": 0, "tools": {tools_json}}}}}"#
));
let json = format!("[{}]", ops.join(","));
let result = k.execute_batch(&json);
let parsed = parse_batch(&result);
let last_idx = ops.len() - 1;
assert_no_crash(&parsed, last_idx, "compoundCut with 9 honeycomb cylinders");
}
#[test]
fn fillet_then_compound_cut() {
let mut k = BrepKernel::new();
let r1 = k.execute_batch(
r#"[
{"op": "makeBox", "args": {"width": 20, "height": 20, "depth": 10}},
{"op": "solidEdges", "args": {"solid": 0}}
]"#,
);
let p1 = parse_batch(&r1);
assert_ok(&p1, 1);
let edges = p1[1]["ok"]
.as_array()
.expect("solidEdges should return array");
assert!(!edges.is_empty(), "box should have edges");
let edge0 = edges[0].as_u64().unwrap();
let r2 = k.execute_batch(&format!(
r#"[{{"op": "fillet", "args": {{"solid": 0, "radius": 1.0, "edges": [{edge0}]}}}}]"#
));
let p2 = parse_batch(&r2);
if p2[0].get("error").is_some() {
return; }
let r3 = k.execute_batch(
r#"[
{"op": "makeCylinder", "args": {"radius": 2.0, "height": 14.0}},
{"op": "copyAndTransformSolid", "args": {"solid": 2, "matrix": [1,0,0,5, 0,1,0,5, 0,0,1,-2, 0,0,0,1]}},
{"op": "copyAndTransformSolid", "args": {"solid": 2, "matrix": [1,0,0,15, 0,1,0,5, 0,0,1,-2, 0,0,0,1]}},
{"op": "copyAndTransformSolid", "args": {"solid": 2, "matrix": [1,0,0,10, 0,1,0,15, 0,0,1,-2, 0,0,0,1]}},
{"op": "compoundCut", "args": {"target": 1, "tools": [3, 4, 5]}}
]"#,
);
let p3 = parse_batch(&r3);
assert_no_crash(&p3, 4, "fillet + compoundCut");
}
#[test]
fn sequential_cut_5_cylinders() {
let mut k = BrepKernel::new();
let result = k.execute_batch(
r#"[
{"op": "makeBox", "args": {"width": 20, "height": 20, "depth": 8}},
{"op": "makeCylinder", "args": {"radius": 1.5, "height": 12.0}},
{"op": "copyAndTransformSolid", "args": {"solid": 1, "matrix": [1,0,0,4, 0,1,0,10, 0,0,1,-2, 0,0,0,1]}},
{"op": "copyAndTransformSolid", "args": {"solid": 1, "matrix": [1,0,0,8, 0,1,0,10, 0,0,1,-2, 0,0,0,1]}},
{"op": "copyAndTransformSolid", "args": {"solid": 1, "matrix": [1,0,0,12, 0,1,0,10, 0,0,1,-2, 0,0,0,1]}},
{"op": "copyAndTransformSolid", "args": {"solid": 1, "matrix": [1,0,0,16, 0,1,0,10, 0,0,1,-2, 0,0,0,1]}},
{"op": "copyAndTransformSolid", "args": {"solid": 1, "matrix": [1,0,0,10, 0,1,0,10, 0,0,1,-2, 0,0,0,1]}},
{"op": "cut", "args": {"solidA": 0, "solidB": 2}},
{"op": "cut", "args": {"solidA": 7, "solidB": 3}},
{"op": "cut", "args": {"solidA": 8, "solidB": 4}},
{"op": "cut", "args": {"solidA": 9, "solidB": 5}},
{"op": "cut", "args": {"solidA": 10, "solidB": 6}}
]"#,
);
let parsed = parse_batch(&result);
for i in 7..=11 {
assert_no_crash(&parsed, i, &format!("sequential cut step {i}"));
}
}
#[test]
fn compound_cut_after_fuse() {
let mut k = BrepKernel::new();
let result = k.execute_batch(
r#"[
{"op": "makeBox", "args": {"width": 10, "height": 10, "depth": 10}},
{"op": "makeBox", "args": {"width": 10, "height": 10, "depth": 10}},
{"op": "copyAndTransformSolid", "args": {"solid": 1, "matrix": [1,0,0,5, 0,1,0,0, 0,0,1,0, 0,0,0,1]}},
{"op": "fuse", "args": {"solidA": 0, "solidB": 2}},
{"op": "makeCylinder", "args": {"radius": 1.5, "height": 14.0}},
{"op": "copyAndTransformSolid", "args": {"solid": 4, "matrix": [1,0,0,5, 0,1,0,5, 0,0,1,-2, 0,0,0,1]}},
{"op": "copyAndTransformSolid", "args": {"solid": 4, "matrix": [1,0,0,10, 0,1,0,5, 0,0,1,-2, 0,0,0,1]}},
{"op": "compoundCut", "args": {"target": 3, "tools": [5, 6]}}
]"#,
);
let parsed = parse_batch(&result);
assert_no_crash(&parsed, 7, "fuse + compoundCut");
}
#[test]
fn fuse_all_mixed_overlap_and_disjoint() {
let mut k = BrepKernel::new();
let result = k.execute_batch(
r#"[
{"op": "makeBox", "args": {"width": 10, "height": 10, "depth": 10}},
{"op": "makeBox", "args": {"width": 10, "height": 10, "depth": 10}},
{"op": "copyAndTransformSolid", "args": {"solid": 1, "matrix": [1,0,0,5, 0,1,0,0, 0,0,1,0, 0,0,0,1]}},
{"op": "copyAndTransformSolid", "args": {"solid": 1, "matrix": [1,0,0,50, 0,1,0,0, 0,0,1,0, 0,0,0,1]}},
{"op": "fuseAll", "args": {"solids": [0, 2, 3]}}
]"#,
);
let parsed = parse_batch(&result);
assert_no_crash(&parsed, 4, "fuseAll mix of overlapping and disjoint boxes");
assert_ok(&parsed, 4);
let handle = parsed[4]["ok"]
.as_u64()
.expect("fuseAll should return a solid handle") as u32;
let counts = k
.get_entity_counts(handle)
.expect("fuseAll result should be a valid solid");
assert_eq!(
counts.len(),
3,
"entity counts are [faces, edges, vertices]"
);
assert!(
counts[0] > 0 && counts[1] > 0 && counts[2] > 0,
"fused solid should be non-empty, got {counts:?}"
);
}
#[test]
fn batch_fuse_cut_fillet_compound() {
let mut k = BrepKernel::new();
let r1 = k.execute_batch(
r#"[
{"op": "makeBox", "args": {"width": 10, "height": 10, "depth": 10}},
{"op": "makeBox", "args": {"width": 10, "height": 10, "depth": 5}},
{"op": "copyAndTransformSolid", "args": {"solid": 1, "matrix": [1,0,0,0, 0,1,0,0, 0,0,1,10, 0,0,0,1]}},
{"op": "fuse", "args": {"solidA": 0, "solidB": 2}},
{"op": "solidEdges", "args": {"solid": 3}}
]"#,
);
let p1 = parse_batch(&r1);
assert_ok(&p1, 3); assert_ok(&p1, 4); let edges = p1[4]["ok"]
.as_array()
.expect("solidEdges should return array");
assert!(!edges.is_empty(), "fused solid should have edges");
let edge0 = edges[0].as_u64().unwrap();
let r2 = k.execute_batch(&format!(
r#"[{{"op": "fillet", "args": {{"solid": 3, "radius": 0.5, "edges": [{edge0}]}}}}]"#
));
let p2 = parse_batch(&r2);
if p2[0].get("error").is_some() {
return; }
let r3 = k.execute_batch(
r#"[
{"op": "makeCylinder", "args": {"radius": 1.0, "height": 20.0}},
{"op": "copyAndTransformSolid", "args": {"solid": 5, "matrix": [1,0,0,5, 0,1,0,5, 0,0,1,-2, 0,0,0,1]}},
{"op": "copyAndTransformSolid", "args": {"solid": 5, "matrix": [1,0,0,3, 0,1,0,3, 0,0,1,-2, 0,0,0,1]}},
{"op": "copyAndTransformSolid", "args": {"solid": 5, "matrix": [1,0,0,7, 0,1,0,7, 0,0,1,-2, 0,0,0,1]}},
{"op": "compoundCut", "args": {"target": 4, "tools": [6, 7, 8]}}
]"#,
);
let p3 = parse_batch(&r3);
assert_no_crash(&p3, 4, "full pipeline (fuse+fillet+compoundCut)");
}
#[test]
fn compound_cut_then_measure() {
let mut k = BrepKernel::new();
let result = k.execute_batch(
r#"[
{"op": "makeBox", "args": {"width": 20, "height": 20, "depth": 5}},
{"op": "makeCylinder", "args": {"radius": 2.0, "height": 8.0}},
{"op": "copyAndTransformSolid", "args": {"solid": 1, "matrix": [1,0,0,5, 0,1,0,5, 0,0,1,-1.5, 0,0,0,1]}},
{"op": "copyAndTransformSolid", "args": {"solid": 1, "matrix": [1,0,0,15, 0,1,0,5, 0,0,1,-1.5, 0,0,0,1]}},
{"op": "copyAndTransformSolid", "args": {"solid": 1, "matrix": [1,0,0,5, 0,1,0,15, 0,0,1,-1.5, 0,0,0,1]}},
{"op": "copyAndTransformSolid", "args": {"solid": 1, "matrix": [1,0,0,15, 0,1,0,15, 0,0,1,-1.5, 0,0,0,1]}},
{"op": "compoundCut", "args": {"target": 0, "tools": [2, 3, 4, 5]}},
{"op": "volume", "args": {"solid": 6}},
{"op": "boundingBox", "args": {"solid": 6}}
]"#,
);
let parsed = parse_batch(&result);
if parsed[6].get("ok").is_some() {
assert_ok(&parsed, 7);
assert_ok(&parsed, 8);
let vol = ok_f64(&parsed, 7);
assert!(
vol > 0.0 && vol < 2000.0,
"volume should be positive and less than original (2000): {vol}"
);
}
}
#[test]
fn sequential_booleans_volume_accuracy() {
let mut k = BrepKernel::new();
let result = k.execute_batch(
r#"[
{"op": "makeBox", "args": {"width": 10, "height": 10, "depth": 10}},
{"op": "volume", "args": {"solid": 0}},
{"op": "makeCylinder", "args": {"radius": 1.0, "height": 14.0}},
{"op": "copyAndTransformSolid", "args": {"solid": 1, "matrix": [1,0,0,2, 0,1,0,5, 0,0,1,-2, 0,0,0,1]}},
{"op": "copyAndTransformSolid", "args": {"solid": 1, "matrix": [1,0,0,5, 0,1,0,5, 0,0,1,-2, 0,0,0,1]}},
{"op": "copyAndTransformSolid", "args": {"solid": 1, "matrix": [1,0,0,8, 0,1,0,5, 0,0,1,-2, 0,0,0,1]}},
{"op": "cut", "args": {"solidA": 0, "solidB": 2}},
{"op": "cut", "args": {"solidA": 5, "solidB": 3}},
{"op": "cut", "args": {"solidA": 6, "solidB": 4}},
{"op": "volume", "args": {"solid": 7}}
]"#,
);
let parsed = parse_batch(&result);
for i in 6..=8 {
assert_ok(&parsed, i);
}
let original_vol = ok_f64(&parsed, 1);
let result_vol = ok_f64(&parsed, 9);
let expected = original_vol - 3.0 * std::f64::consts::PI * 1.0 * 10.0;
let rel_error = ((result_vol - expected) / expected).abs();
assert!(
rel_error < 0.05,
"volume after 3 cylinder cuts: got {result_vol:.1}, expected {expected:.1}, \
error {:.1}% (issue #260)",
rel_error * 100.0
);
}
#[test]
fn fillet_box_bbox_unchanged() {
let mut k = BrepKernel::new();
let r1 = k.execute_batch(
r#"[
{"op": "makeBox", "args": {"width": 10, "height": 10, "depth": 10}},
{"op": "boundingBox", "args": {"solid": 0}},
{"op": "solidEdges", "args": {"solid": 0}}
]"#,
);
let p1 = parse_batch(&r1);
assert_ok(&p1, 1); assert_ok(&p1, 2); let bbox_before = ok_bbox(&p1, 1);
let edges = p1[2]["ok"]
.as_array()
.expect("solidEdges should return array");
assert!(!edges.is_empty(), "box should have edges");
let edge0 = edges[0].as_u64().unwrap();
let r2 = k.execute_batch(&format!(
r#"[
{{"op": "fillet", "args": {{"solid": 0, "radius": 1.0, "edges": [{edge0}]}}}},
{{"op": "boundingBox", "args": {{"solid": 1}}}}
]"#
));
let p2 = parse_batch(&r2);
if p2[0].get("ok").is_some() {
assert_ok(&p2, 1);
let bbox_after = ok_bbox(&p2, 1);
let tol = 0.01;
for i in 0..6 {
assert!(
(bbox_before[i] - bbox_after[i]).abs() < tol,
"bbox[{i}] shifted after fillet: {:.3} → {:.3} (issue #260)",
bbox_before[i],
bbox_after[i]
);
}
}
}
#[test]
fn compound_cut_bbox_accurate() {
let mut k = BrepKernel::new();
let result = k.execute_batch(
r#"[
{"op": "makeBox", "args": {"width": 20, "height": 20, "depth": 10}},
{"op": "boundingBox", "args": {"solid": 0}},
{"op": "makeCylinder", "args": {"radius": 2.0, "height": 14.0}},
{"op": "copyAndTransformSolid", "args": {"solid": 1, "matrix": [1,0,0,10, 0,1,0,10, 0,0,1,-2, 0,0,0,1]}},
{"op": "cut", "args": {"solidA": 0, "solidB": 2}},
{"op": "boundingBox", "args": {"solid": 3}}
]"#,
);
let parsed = parse_batch(&result);
assert_ok(&parsed, 1); let bbox_before = ok_bbox(&parsed, 1);
if parsed[4].get("ok").is_some() {
assert_ok(&parsed, 5); let bbox_after = ok_bbox(&parsed, 5);
let tol = 0.1;
for i in 0..6 {
assert!(
(bbox_before[i] - bbox_after[i]).abs() < tol,
"bbox[{i}] shifted after cylinder cut: {:.3} → {:.3} (issue #260)",
bbox_before[i],
bbox_after[i]
);
}
}
}
#[test]
fn gridfinity_lip_fillet_fuse_volume() {
let mut k = BrepKernel::new();
let r1 = k.execute_batch(
r#"[
{"op": "makeBox", "args": {"width": 42, "height": 42, "depth": 7}},
{"op": "solidEdges", "args": {"solid": 0}}
]"#,
);
let p1 = parse_batch(&r1);
assert_ok(&p1, 0);
assert_ok(&p1, 1);
let edges = p1[1]["ok"]
.as_array()
.expect("solidEdges should return array");
assert!(!edges.is_empty(), "box should have edges");
let edge0 = edges[0].as_u64().unwrap();
let r2 = k.execute_batch(&format!(
r#"[{{"op": "fillet", "args": {{"solid": 0, "radius": 0.8, "edges": [{edge0}]}}}}]"#
));
let p2 = parse_batch(&r2);
assert_ok(&p2, 0);
let filleted_handle = p2[0]["ok"].as_u64().unwrap();
let r3 = k.execute_batch(&format!(
r#"[
{{"op": "makeBox", "args": {{"width": 44, "height": 44, "depth": 2}}}},
{{"op": "transform", "args": {{"solid": 2, "matrix": [1,0,0,-1, 0,1,0,-1, 0,0,1,7, 0,0,0,1]}}}},
{{"op": "fuse", "args": {{"solidA": {filleted_handle}, "solidB": 2}}}},
{{"op": "volume", "args": {{"solid": 3}}}}
]"#,
));
let p3 = parse_batch(&r3);
assert_ok(&p3, 2);
let vol = ok_f64(&p3, 3);
assert!(
vol > 10000.0 && vol < 20000.0,
"gridfinity lip fuse volume should be ~16000: got {vol:.0} (issue #270)"
);
}
const OUTER_DIM: f64 = 41.5; const CORNER_R: f64 = 4.0; const WALL: f64 = 2.6; const _LIP_EXT: f64 = 1.2; const Z_EXT: f64 = -1.2; const Z_BASE: f64 = 0.0;
const Z_TAPER1: f64 = 0.7; const Z_VERT: f64 = 2.5; const Z_PEAK: f64 = 4.4; const INSET_BOTTOM: f64 = 2.6; const INSET_MID: f64 = 1.9; const INSET_TOP: f64 = 0.0;
const TOP_FILLET: f64 = 0.6;
const WALL_THICKNESS: f64 = 1.6; const WALL_HEIGHT: f64 = 21.0;
fn make_rounded_rect_face(k: &mut BrepKernel, w: f64, d: f64, r: f64, z: f64) -> u32 {
let hw = w / 2.0;
let hd = d / 2.0;
let r = r.min(hw).min(hd).max(0.05);
let pts: [(f64, f64); 8] = [
(hw, -(hd - r)), (hw, hd - r), (hw - r, hd), (-(hw - r), hd), (-hw, hd - r), (-hw, -(hd - r)), (-(hw - r), -hd), (hw - r, -hd), ];
let centers: [(f64, f64); 4] = [
(hw - r, hd - r), (-(hw - r), hd - r), (-(hw - r), -(hd - r)), (hw - r, -(hd - r)), ];
let mut edges = Vec::with_capacity(8);
edges.push(
k.make_line_edge(pts[0].0, pts[0].1, z, pts[1].0, pts[1].1, z)
.unwrap(),
);
edges.push(
k.make_circle_arc_3d(
pts[1].0,
pts[1].1,
z,
pts[2].0,
pts[2].1,
z,
centers[0].0,
centers[0].1,
z,
0.0,
0.0,
1.0,
)
.unwrap(),
);
edges.push(
k.make_line_edge(pts[2].0, pts[2].1, z, pts[3].0, pts[3].1, z)
.unwrap(),
);
edges.push(
k.make_circle_arc_3d(
pts[3].0,
pts[3].1,
z,
pts[4].0,
pts[4].1,
z,
centers[1].0,
centers[1].1,
z,
0.0,
0.0,
1.0,
)
.unwrap(),
);
edges.push(
k.make_line_edge(pts[4].0, pts[4].1, z, pts[5].0, pts[5].1, z)
.unwrap(),
);
edges.push(
k.make_circle_arc_3d(
pts[5].0,
pts[5].1,
z,
pts[6].0,
pts[6].1,
z,
centers[2].0,
centers[2].1,
z,
0.0,
0.0,
1.0,
)
.unwrap(),
);
edges.push(
k.make_line_edge(pts[6].0, pts[6].1, z, pts[7].0, pts[7].1, z)
.unwrap(),
);
edges.push(
k.make_circle_arc_3d(
pts[7].0,
pts[7].1,
z,
pts[0].0,
pts[0].1,
z,
centers[3].0,
centers[3].1,
z,
0.0,
0.0,
1.0,
)
.unwrap(),
);
let wire = k.make_wire(edges, true).unwrap();
k.make_face_from_wire(wire).unwrap()
}
fn section_dims(inset: f64) -> (f64, f64, f64) {
let w = OUTER_DIM - 2.0 * inset;
let d = OUTER_DIM - 2.0 * inset;
let r = (CORNER_R - inset).max(0.1);
(w, d, r)
}
fn make_outer_sections(k: &mut BrepKernel) -> Vec<u32> {
let mut faces = Vec::new();
let sections: [(f64, f64); 2] = [(Z_EXT, 0.0), (Z_PEAK, 0.0)];
for &(z, inset) in §ions {
let (w, d, r) = section_dims(inset);
faces.push(make_rounded_rect_face(k, w, d, r, z));
}
faces
}
fn make_inner_sections_offset(k: &mut BrepKernel, z_offset: f64) -> Vec<u32> {
let mut faces = Vec::new();
let sections: [(f64, f64); 5] = [
(Z_EXT + z_offset, INSET_BOTTOM + WALL),
(Z_BASE + z_offset, INSET_BOTTOM + WALL),
(Z_TAPER1 + z_offset, INSET_MID + WALL),
(Z_VERT + z_offset, INSET_MID + WALL),
(Z_PEAK + z_offset, INSET_TOP + WALL),
];
for &(z, inset) in §ions {
let (w, d, r) = section_dims(inset);
faces.push(make_rounded_rect_face(k, w, d, r, z));
}
faces
}
fn make_inner_sections(k: &mut BrepKernel) -> Vec<u32> {
make_inner_sections_offset(k, 0.0)
}
#[test]
fn gridfinity_d1_lip_ring_loft_cut() {
let mut k = BrepKernel::new();
let outer_faces = make_outer_sections(&mut k);
let inner_faces = make_inner_sections(&mut k);
let outer_json = serde_json::to_string(&outer_faces).unwrap();
let r1 = k.execute_batch(&format!(
r#"[{{"op": "loft", "args": {{"faces": {outer_json}}}}}]"#
));
let p1 = parse_batch(&r1);
assert_ok(&p1, 0);
let outer_solid = p1[0]["ok"].as_u64().unwrap() as u32;
let oc = k.get_entity_counts(outer_solid).unwrap();
let o_euler = (oc[2] as i64) - (oc[1] as i64) + (oc[0] as i64);
let ov = k.validate_solid(outer_solid).unwrap();
eprintln!(
"D1 outer loft: F={}, E={}, V={}, euler={o_euler}, validation_issues={ov}",
oc[0], oc[1], oc[2]
);
let inner_json = serde_json::to_string(&inner_faces).unwrap();
let r2 = k.execute_batch(&format!(
r#"[{{"op": "loft", "args": {{"faces": {inner_json}}}}}]"#
));
let p2 = parse_batch(&r2);
assert_ok(&p2, 0);
let inner_solid = p2[0]["ok"].as_u64().unwrap() as u32;
let ic = k.get_entity_counts(inner_solid).unwrap();
let i_euler = (ic[2] as i64) - (ic[1] as i64) + (ic[0] as i64);
let iv = k.validate_solid(inner_solid).unwrap();
eprintln!(
"D1 inner loft: F={}, E={}, V={}, euler={i_euler}, validation_issues={iv}",
ic[0], ic[1], ic[2]
);
let r3 = k.execute_batch(&format!(
r#"[
{{"op": "cut", "args": {{"solidA": {outer_solid}, "solidB": {inner_solid}}}}},
{{"op": "volume", "args": {{"solid": {}}}}},
{{"op": "boundingBox", "args": {{"solid": {}}}}}
]"#,
outer_solid + 2, outer_solid + 2,
));
let p3 = parse_batch(&r3);
assert_ok(&p3, 0);
let vol = ok_f64(&p3, 1);
assert!(
vol > 100.0 && vol < 5000.0,
"lip ring volume should be 100-5000 mm³: got {vol:.1}"
);
let bbox = ok_bbox(&p3, 2);
let z_extent = bbox[5] - bbox[2];
assert!(
(z_extent - 5.6).abs() < 1.0,
"lip Z extent should be ~5.6mm: got {z_extent:.2}"
);
let lip_handle = p3[0]["ok"].as_u64().unwrap() as u32;
let lip_id = k.resolve_solid(lip_handle).unwrap();
let val = k.validate_solid(lip_handle).unwrap();
let counts = k.get_entity_counts(lip_handle).unwrap();
let f = counts[0] as usize;
let e = counts[1] as usize;
let v = counts[2] as usize;
let euler = (v as i64) - (e as i64) + (f as i64);
eprintln!("D1 lip ring: F={f}, E={e}, V={v}, euler={euler}, vol={vol:.1}, val={val}");
if let Ok(report) = brepkit_operations::validate::validate_solid(&k.topo, lip_id) {
for issue in &report.issues {
if issue.severity == brepkit_operations::validate::Severity::Error {
eprintln!(" ERR: {}", issue.description);
}
}
}
{
use std::collections::HashMap;
let solid_data = k.topo.solid(lip_id).unwrap();
let shell = k.topo.shell(solid_data.outer_shell()).unwrap();
let mut edge_face_count: HashMap<usize, usize> = HashMap::new();
for &fid in shell.faces() {
let face = k.topo.face(fid).unwrap();
let wire = k.topo.wire(face.outer_wire()).unwrap();
for oe in wire.edges() {
*edge_face_count.entry(oe.edge().index()).or_insert(0) += 1;
}
}
let boundary: Vec<usize> = edge_face_count
.iter()
.filter(|&(_, &c)| c == 1)
.map(|(&e, _)| e)
.collect();
eprintln!(" Boundary edges: {}", boundary.len());
for &eidx in boundary.iter().take(8) {
if let Some(eid) = k.topo.edge_id_from_index(eidx)
&& let Ok(edge) = k.topo.edge(eid)
{
let s = k.topo.vertex(edge.start()).unwrap().point();
let e = k.topo.vertex(edge.end()).unwrap().point();
let curve = match edge.curve() {
brepkit_topology::edge::EdgeCurve::Line => "line",
brepkit_topology::edge::EdgeCurve::Circle(_) => "circle",
_ => "other",
};
eprintln!(
" edge[{eidx}] {curve}: ({:.3},{:.3},{:.3}) → ({:.3},{:.3},{:.3})",
s.x(),
s.y(),
s.z(),
e.x(),
e.y(),
e.z()
);
}
}
}
assert!(
val == 0,
"lip ring should have 0 validation issues: got {val}"
);
assert!(
euler == 2,
"Euler should be 2 for a valid solid: got {euler} (F={f}, E={e}, V={v})"
);
assert!(
f < 200,
"lip ring should have < 200 faces (no explosion): got {f}"
);
}
#[test]
fn gridfinity_d1a_concentric_box_cut() {
let mut k = BrepKernel::new();
let result = k.execute_batch(
r#"[
{"op": "makeBox", "args": {"width": 20, "height": 20, "depth": 10}},
{"op": "makeBox", "args": {"width": 14, "height": 14, "depth": 8}},
{"op": "transform", "args": {"solid": 1, "matrix": [1,0,0,3, 0,1,0,3, 0,0,1,1, 0,0,0,1]}},
{"op": "cut", "args": {"solidA": 0, "solidB": 1}}
]"#,
);
let parsed = parse_batch(&result);
assert_ok(&parsed, 3);
let solid = parsed[3]["ok"].as_u64().unwrap() as u32;
let counts = k.get_entity_counts(solid).unwrap();
let f = counts[0] as usize;
let e = counts[1] as usize;
let v = counts[2] as usize;
let euler = (v as i64) - (e as i64) + (f as i64);
let val = k.validate_solid(solid).unwrap();
eprintln!("D1a concentric box cut: F={f}, E={e}, V={v}, euler={euler}, val={val}");
assert!(
euler == 4,
"D1a Euler should be 4 (internal cavity): got {euler}"
);
}
#[test]
fn gridfinity_d1a1c_octagon_cut() {
let mut k = BrepKernel::new();
let outer_r = 20.75; let inner_r = 18.15; let n = 8;
let height = 5.6;
let mut outer_pts = Vec::new();
let mut inner_pts = Vec::new();
for i in 0..n {
let angle = std::f64::consts::TAU * (i as f64) / (n as f64);
outer_pts.extend_from_slice(&[outer_r * angle.cos(), outer_r * angle.sin(), 0.0]);
inner_pts.extend_from_slice(&[inner_r * angle.cos(), inner_r * angle.sin(), 0.0]);
}
let outer_face = k.make_polygon(outer_pts).unwrap();
let inner_face = k.make_polygon(inner_pts).unwrap();
let r1 = k.execute_batch(&format!(
r#"[
{{"op": "extrude", "args": {{"face": {outer_face}, "dz": 1.0, "distance": {height}}}}},
{{"op": "extrude", "args": {{"face": {inner_face}, "dz": 1.0, "distance": {height}}}}},
{{"op": "cut", "args": {{"solidA": 0, "solidB": 1}}}}
]"#
));
let p1 = parse_batch(&r1);
assert_ok(&p1, 2);
let solid = p1[2]["ok"].as_u64().unwrap() as u32;
let counts = k.get_entity_counts(solid).unwrap();
let f = counts[0] as usize;
let e = counts[1] as usize;
let v = counts[2] as usize;
let euler = (v as i64) - (e as i64) + (f as i64);
let val = k.validate_solid(solid).unwrap();
eprintln!("D1a1c octagon cut: F={f}, E={e}, V={v}, euler={euler}, val={val}");
assert!(euler == 2, "D1a1c Euler should be 2: got {euler}");
}
#[test]
fn gridfinity_d1a2_concentric_box_coplanar() {
let mut k = BrepKernel::new();
let result = k.execute_batch(
r#"[
{"op": "makeBox", "args": {"width": 20, "height": 20, "depth": 10}},
{"op": "makeBox", "args": {"width": 14, "height": 14, "depth": 10}},
{"op": "transform", "args": {"solid": 1, "matrix": [1,0,0,3, 0,1,0,3, 0,0,1,0, 0,0,0,1]}},
{"op": "cut", "args": {"solidA": 0, "solidB": 1}}
]"#,
);
let parsed = parse_batch(&result);
assert_ok(&parsed, 3);
let solid = parsed[3]["ok"].as_u64().unwrap() as u32;
let counts = k.get_entity_counts(solid).unwrap();
let f = counts[0] as usize;
let e = counts[1] as usize;
let v = counts[2] as usize;
let euler = (v as i64) - (e as i64) + (f as i64);
let val = k.validate_solid(solid).unwrap();
eprintln!("D1a2 coplanar box cut: F={f}, E={e}, V={v}, euler={euler}, val={val}");
assert!(euler == 2, "D1a2 Euler should be 2: got {euler}");
}
#[test]
fn gridfinity_d1b_lip_ring_no_coplanar() {
let mut k = BrepKernel::new();
let outer_faces = make_outer_sections(&mut k);
let inner_faces = make_inner_sections_offset(&mut k, -0.1);
let outer_json = serde_json::to_string(&outer_faces).unwrap();
let r1 = k.execute_batch(&format!(
r#"[{{"op": "loft", "args": {{"faces": {outer_json}}}}}]"#
));
let p1 = parse_batch(&r1);
assert_ok(&p1, 0);
let outer_solid = p1[0]["ok"].as_u64().unwrap() as u32;
let inner_json = serde_json::to_string(&inner_faces).unwrap();
let r2 = k.execute_batch(&format!(
r#"[{{"op": "loft", "args": {{"faces": {inner_json}}}}}]"#
));
let p2 = parse_batch(&r2);
assert_ok(&p2, 0);
let inner_solid = p2[0]["ok"].as_u64().unwrap() as u32;
let r3 = k.execute_batch(&format!(
r#"[{{"op": "cut", "args": {{"solidA": {outer_solid}, "solidB": {inner_solid}}}}}]"#
));
let p3 = parse_batch(&r3);
assert_ok(&p3, 0);
let lip_handle = p3[0]["ok"].as_u64().unwrap() as u32;
let lv = k.validate_solid(lip_handle).unwrap();
let counts = k.get_entity_counts(lip_handle).unwrap();
let f = counts[0] as usize;
let e = counts[1] as usize;
let v = counts[2] as usize;
let euler = (v as i64) - (e as i64) + (f as i64);
let lip_id = k.resolve_solid(lip_handle).unwrap();
let solid_data = k.topo.solid(lip_id).unwrap();
let shell = k.topo.shell(solid_data.outer_shell()).unwrap();
let inner_loop_count: i64 = shell
.faces()
.iter()
.map(|&fid| k.topo.face(fid).unwrap().inner_wires().len() as i64)
.sum();
let adjusted_euler = euler - inner_loop_count;
eprintln!(
"D1b no-coplanar: F={f}, E={e}, V={v}, euler={euler}, inner_loops={inner_loop_count}, adjusted_euler={adjusted_euler}, validation_issues={lv}"
);
assert_eq!(lv, 0, "D1b should have 0 validation issues, got {lv}");
assert!(
adjusted_euler == 2,
"D1b adjusted Euler should be 2: got {adjusted_euler} (raw euler={euler}, inner_loops={inner_loop_count}, F={f}, E={e}, V={v})"
);
}
#[test]
fn gridfinity_d2_lip_ring_with_fillet() {
let mut k = BrepKernel::new();
let outer_faces = make_outer_sections(&mut k);
let inner_faces = make_inner_sections(&mut k);
let outer_json = serde_json::to_string(&outer_faces).unwrap();
let r1 = k.execute_batch(&format!(
r#"[{{"op": "loft", "args": {{"faces": {outer_json}}}}}]"#
));
let p1 = parse_batch(&r1);
assert_ok(&p1, 0);
let outer_solid = p1[0]["ok"].as_u64().unwrap() as u32;
let inner_json = serde_json::to_string(&inner_faces).unwrap();
let r2 = k.execute_batch(&format!(
r#"[{{"op": "loft", "args": {{"faces": {inner_json}}}}}]"#
));
let p2 = parse_batch(&r2);
assert_ok(&p2, 0);
let inner_solid = p2[0]["ok"].as_u64().unwrap() as u32;
let r3 = k.execute_batch(&format!(
r#"[{{"op": "cut", "args": {{"solidA": {outer_solid}, "solidB": {inner_solid}}}}}]"#
));
let p3 = parse_batch(&r3);
assert_ok(&p3, 0);
let lip_handle = p3[0]["ok"].as_u64().unwrap() as u32;
let r3b = k.execute_batch(&format!(
r#"[{{"op": "boundingBox", "args": {{"solid": {lip_handle}}}}}]"#
));
let p3b = parse_batch(&r3b);
assert_ok(&p3b, 0);
let bbox_before = ok_bbox(&p3b, 0);
let r4 = k.execute_batch(&format!(
r#"[{{"op": "solidEdges", "args": {{"solid": {lip_handle}}}}}]"#
));
let p4 = parse_batch(&r4);
assert_ok(&p4, 0);
let edges = p4[0]["ok"]
.as_array()
.expect("solidEdges should return array");
if edges.is_empty() {
panic!("lip solid should have edges");
}
let edge0 = edges[0].as_u64().unwrap();
let r5 = k.execute_batch(&format!(
r#"[{{"op": "fillet", "args": {{"solid": {lip_handle}, "radius": {TOP_FILLET}, "edges": [{edge0}]}}}}]"#
));
let p5 = parse_batch(&r5);
if p5[0].get("error").is_some() {
eprintln!(
"D2 fillet failed (expected for known bugs): {}",
p5[0]["error"]
);
return; }
let filleted = p5[0]["ok"].as_u64().unwrap() as u32;
let r6 = k.execute_batch(&format!(
r#"[{{"op": "boundingBox", "args": {{"solid": {filleted}}}}}]"#
));
let p6 = parse_batch(&r6);
assert_ok(&p6, 0);
let bbox_after = ok_bbox(&p6, 0);
let tol = 0.5; for i in 0..6 {
assert!(
(bbox_before[i] - bbox_after[i]).abs() < tol,
"D2 bbox[{i}] shifted after fillet: {:.3} → {:.3} (issue #260)",
bbox_before[i],
bbox_after[i]
);
}
eprintln!("D2 lip ring + fillet: bbox stable, fillet succeeded");
}
#[test]
fn gridfinity_d3_shelled_box_with_lip() {
let mut k = BrepKernel::new();
let box_face = make_rounded_rect_face(&mut k, OUTER_DIM, OUTER_DIM, CORNER_R, 0.0);
let r1 = k.execute_batch(&format!(
r#"[{{"op": "extrude", "args": {{"face": {box_face}, "dz": 1.0, "distance": {WALL_HEIGHT}}}}}]"#
));
let p1 = parse_batch(&r1);
assert_ok(&p1, 0);
let box_solid = p1[0]["ok"].as_u64().unwrap() as u32;
let top_face = k
.get_solid_faces(box_solid)
.unwrap()
.iter()
.copied()
.find(|&fh| {
k.resolve_face(fh)
.ok()
.and_then(|fid| k.topo.face(fid).ok())
.and_then(brepkit_topology::face::Face::effective_plane_normal)
.is_some_and(|n| n.z() > 0.5 * n.length())
})
.expect("extruded box should have a +Z top face");
let r2 = k.execute_batch(&format!(
r#"[{{"op": "shell", "args": {{"solid": {box_solid}, "thickness": {WALL_THICKNESS}, "faces": [{top_face}]}}}}]"#
));
let p2 = parse_batch(&r2);
assert_ok(&p2, 0);
let box_handle = p2[0]["ok"].as_u64().unwrap() as u32;
let outer_faces = make_outer_sections(&mut k);
let inner_faces = make_inner_sections(&mut k);
let outer_json = serde_json::to_string(&outer_faces).unwrap();
let r3 = k.execute_batch(&format!(
r#"[{{"op": "loft", "args": {{"faces": {outer_json}}}}}]"#
));
let p3 = parse_batch(&r3);
assert_ok(&p3, 0);
let outer_solid = p3[0]["ok"].as_u64().unwrap() as u32;
let inner_json = serde_json::to_string(&inner_faces).unwrap();
let r4 = k.execute_batch(&format!(
r#"[{{"op": "loft", "args": {{"faces": {inner_json}}}}}]"#
));
let p4 = parse_batch(&r4);
assert_ok(&p4, 0);
let inner_solid = p4[0]["ok"].as_u64().unwrap() as u32;
let r5 = k.execute_batch(&format!(
r#"[{{"op": "cut", "args": {{"solidA": {outer_solid}, "solidB": {inner_solid}}}}}]"#
));
let p5 = parse_batch(&r5);
assert_ok(&p5, 0);
let lip_handle = p5[0]["ok"].as_u64().unwrap() as u32;
let mat = translate_matrix(0.0, 0.0, WALL_HEIGHT);
let r6 = k.execute_batch(&format!(
r#"[{{"op": "transform", "args": {{"solid": {lip_handle}, "matrix": {mat}}}}}]"#
));
let p6 = parse_batch(&r6);
assert_ok(&p6, 0);
let bc = k.get_entity_counts(box_handle).unwrap();
let lc = k.get_entity_counts(lip_handle).unwrap();
eprintln!("D3 before fuse: box F={}, lip F={}", bc[0], lc[0]);
let r7 = k.execute_batch(&format!(
r#"[{{"op": "fuse", "args": {{"solidA": {box_handle}, "solidB": {lip_handle}}}}}]"#
));
let p7 = parse_batch(&r7);
if p7[0].get("error").is_some() {
eprintln!("D3 fuse failed: {}", p7[0]["error"]);
return;
}
let fused = p7[0]["ok"].as_u64().unwrap() as u32;
let r8 = k.execute_batch(&format!(
r#"[{{"op": "volume", "args": {{"solid": {fused}}}}}]"#
));
let p8 = parse_batch(&r8);
let vol = ok_f64(&p8, 0);
eprintln!("D3 shelled box + lip: vol={vol:.1}");
assert!(
vol > 5000.0,
"fused volume should be > 5000 mm³: got {vol:.1}"
);
let counts = k.get_entity_counts(fused).unwrap();
let fc = counts[0] as usize;
eprintln!("D3 face count: {fc}");
assert!(fc < 200, "fused solid should have < 200 faces: got {fc}");
}
#[test]
fn gridfinity_d5_box_with_filleted_lip() {
let mut k = BrepKernel::new();
let box_face = make_rounded_rect_face(&mut k, OUTER_DIM, OUTER_DIM, CORNER_R, 0.0);
let r1 = k.execute_batch(&format!(
r#"[{{"op": "extrude", "args": {{"face": {box_face}, "dz": 1.0, "distance": {WALL_HEIGHT}}}}}]"#
));
let p1 = parse_batch(&r1);
assert_ok(&p1, 0);
let box_solid = p1[0]["ok"].as_u64().unwrap() as u32;
let top_face = k
.get_solid_faces(box_solid)
.unwrap()
.iter()
.copied()
.find(|&fh| {
k.resolve_face(fh)
.ok()
.and_then(|fid| k.topo.face(fid).ok())
.and_then(brepkit_topology::face::Face::effective_plane_normal)
.is_some_and(|n| n.z() > 0.5 * n.length())
})
.expect("extruded box should have a +Z top face");
let r2 = k.execute_batch(&format!(
r#"[{{"op": "shell", "args": {{"solid": {box_solid}, "thickness": {WALL_THICKNESS}, "faces": [{top_face}]}}}}]"#
));
let p2 = parse_batch(&r2);
assert_ok(&p2, 0);
let box_handle = p2[0]["ok"].as_u64().unwrap() as u32;
let outer_faces = make_outer_sections(&mut k);
let inner_faces = make_inner_sections(&mut k);
let outer_json = serde_json::to_string(&outer_faces).unwrap();
let r3 = k.execute_batch(&format!(
r#"[{{"op": "loft", "args": {{"faces": {outer_json}}}}}]"#
));
let p3 = parse_batch(&r3);
assert_ok(&p3, 0);
let outer_solid = p3[0]["ok"].as_u64().unwrap() as u32;
let inner_json = serde_json::to_string(&inner_faces).unwrap();
let r4 = k.execute_batch(&format!(
r#"[{{"op": "loft", "args": {{"faces": {inner_json}}}}}]"#
));
let p4 = parse_batch(&r4);
assert_ok(&p4, 0);
let inner_solid = p4[0]["ok"].as_u64().unwrap() as u32;
let r5 = k.execute_batch(&format!(
r#"[{{"op": "cut", "args": {{"solidA": {outer_solid}, "solidB": {inner_solid}}}}}]"#
));
let p5 = parse_batch(&r5);
assert_ok(&p5, 0);
let lip_handle = p5[0]["ok"].as_u64().unwrap() as u32;
let pre_lc = k.get_entity_counts(lip_handle).unwrap();
let pre_euler = (pre_lc[2] as i64) - (pre_lc[1] as i64) + (pre_lc[0] as i64);
assert!(
pre_euler >= 2,
"lip solid should have euler >= 2 before fillet, got {pre_euler}"
);
let r5b = k.execute_batch(&format!(
r#"[{{"op": "solidEdges", "args": {{"solid": {lip_handle}}}}}]"#
));
let p5b = parse_batch(&r5b);
assert_ok(&p5b, 0);
let peak_edge = p5b[0]["ok"]
.as_array()
.expect("edges")
.iter()
.filter_map(serde_json::Value::as_u64)
.find(|&eh| {
k.resolve_edge(eh as u32)
.ok()
.and_then(|eid| k.topo.edge(eid).ok())
.is_some_and(|edge| {
let z0 = k
.topo
.vertex(edge.start())
.map(|v| v.point().z())
.unwrap_or(f64::NAN);
let z1 = k
.topo
.vertex(edge.end())
.map(|v| v.point().z())
.unwrap_or(f64::NAN);
z0.min(z1) >= Z_PEAK - 1.0 && z0.max(z1) >= Z_PEAK - 0.01
})
})
.expect("lip should have a peak rim edge near Z_PEAK");
let r5c = k.execute_batch(&format!(
r#"[{{"op": "fillet", "args": {{"solid": {lip_handle}, "radius": {TOP_FILLET}, "edges": [{peak_edge}]}}}}]"#
));
let p5c = parse_batch(&r5c);
assert_ok(&p5c, 0);
let lip_final = p5c[0]["ok"].as_u64().unwrap() as u32;
let lc = k.get_entity_counts(lip_final).unwrap();
let lip_val = k.validate_solid(lip_final).unwrap();
let lip_euler = (lc[2] as i64) - (lc[1] as i64) + (lc[0] as i64);
assert!(
lip_val <= 2,
"filleted lip should have <= 2 validation issues, got {lip_val}"
);
assert!(
lip_euler >= 2,
"filleted lip Euler characteristic should be >= 2, got {lip_euler}"
);
let mat = translate_matrix(0.0, 0.0, WALL_HEIGHT);
let r6 = k.execute_batch(&format!(
r#"[{{"op": "transform", "args": {{"solid": {lip_final}, "matrix": {mat}}}}}]"#
));
let p6 = parse_batch(&r6);
assert_ok(&p6, 0);
let r7 = k.execute_batch(&format!(
r#"[{{"op": "fuse", "args": {{"solidA": {box_handle}, "solidB": {lip_final}}}}}]"#
));
let p7 = parse_batch(&r7);
assert_ok(&p7, 0);
let fused = p7[0]["ok"].as_u64().unwrap() as u32;
let r8 = k.execute_batch(&format!(
r#"[{{"op": "volume", "args": {{"solid": {fused}}}}}]"#
));
let p8 = parse_batch(&r8);
let vol = ok_f64(&p8, 0);
let fc = k.get_entity_counts(fused).unwrap();
let euler = (fc[2] as i64) - (fc[1] as i64) + (fc[0] as i64);
eprintln!(
"D5 result: F={}, E={}, V={}, euler={euler}, vol={vol:.1}",
fc[0], fc[1], fc[2]
);
assert!(
fc[0] < 200,
"fused solid should have < 200 faces (mesh fallback otherwise): got {}",
fc[0]
);
assert!(
vol > 5000.0,
"fused volume should be > 5000 mm³: got {vol:.1}"
);
}
#[test]
fn gridfinity_d4_full_1x1_bin() {
let mut k = BrepKernel::new();
let box_face = make_rounded_rect_face(&mut k, OUTER_DIM, OUTER_DIM, CORNER_R, 0.0);
let r1 = k.execute_batch(&format!(
r#"[{{"op": "extrude", "args": {{"face": {box_face}, "dz": 1.0, "distance": {WALL_HEIGHT}}}}}]"#
));
let p1 = parse_batch(&r1);
assert_ok(&p1, 0);
let box_solid = p1[0]["ok"].as_u64().unwrap() as u32;
let faces = k.get_solid_faces(box_solid).unwrap();
assert!(!faces.is_empty(), "box should have faces");
let top_face = faces
.iter()
.copied()
.find(|&fh| {
k.resolve_face(fh)
.ok()
.and_then(|fid| k.topo.face(fid).ok())
.and_then(brepkit_topology::face::Face::effective_plane_normal)
.is_some_and(|n| n.z() > 0.5 * n.length())
})
.expect("extruded box should have a +Z top face");
let r3 = k.execute_batch(&format!(
r#"[{{"op": "shell", "args": {{"solid": {box_solid}, "thickness": {WALL_THICKNESS}, "faces": [{top_face}]}}}}]"#
));
let p3 = parse_batch(&r3);
assert_ok(&p3, 0);
let shelled = p3[0]["ok"].as_u64().unwrap() as u32;
let outer_faces = make_outer_sections(&mut k);
let inner_faces = make_inner_sections(&mut k);
let outer_json = serde_json::to_string(&outer_faces).unwrap();
let r4 = k.execute_batch(&format!(
r#"[{{"op": "loft", "args": {{"faces": {outer_json}}}}}]"#
));
let p4 = parse_batch(&r4);
assert_ok(&p4, 0);
let outer_solid = p4[0]["ok"].as_u64().unwrap() as u32;
let inner_json = serde_json::to_string(&inner_faces).unwrap();
let r5 = k.execute_batch(&format!(
r#"[{{"op": "loft", "args": {{"faces": {inner_json}}}}}]"#
));
let p5 = parse_batch(&r5);
assert_ok(&p5, 0);
let inner_solid = p5[0]["ok"].as_u64().unwrap() as u32;
let r6 = k.execute_batch(&format!(
r#"[{{"op": "cut", "args": {{"solidA": {outer_solid}, "solidB": {inner_solid}}}}}]"#
));
let p6 = parse_batch(&r6);
assert_ok(&p6, 0);
let lip_handle = p6[0]["ok"].as_u64().unwrap() as u32;
let mat = translate_matrix(0.0, 0.0, WALL_HEIGHT);
let r7 = k.execute_batch(&format!(
r#"[{{"op": "transform", "args": {{"solid": {lip_handle}, "matrix": {mat}}}}}]"#
));
let p7 = parse_batch(&r7);
assert_ok(&p7, 0);
let r8 = k.execute_batch(&format!(
r#"[{{"op": "fuse", "args": {{"solidA": {shelled}, "solidB": {lip_handle}}}}}]"#
));
let p8 = parse_batch(&r8);
if p8[0].get("error").is_some() {
eprintln!("D4 fuse failed: {}", p8[0]["error"]);
return;
}
let fused = p8[0]["ok"].as_u64().unwrap() as u32;
let r9 = k.execute_batch(&format!(
r#"[
{{"op": "volume", "args": {{"solid": {fused}}}}},
{{"op": "boundingBox", "args": {{"solid": {fused}}}}}
]"#
));
let p9 = parse_batch(&r9);
let vol = ok_f64(&p9, 0);
let bbox = ok_bbox(&p9, 1);
let counts = k.get_entity_counts(fused).unwrap();
let f = counts[0] as usize;
let e = counts[1] as usize;
let v = counts[2] as usize;
let euler = (v as i64) - (e as i64) + (f as i64);
let fused_id = k.resolve_solid(fused).unwrap();
let solid_data = k.topo.solid(fused_id).unwrap();
let shell = k.topo.shell(solid_data.outer_shell()).unwrap();
let inner_loop_count: i64 = shell
.faces()
.iter()
.map(|&fid| k.topo.face(fid).unwrap().inner_wires().len() as i64)
.sum();
let adjusted_euler = euler - inner_loop_count;
eprintln!("D4 full 1×1 bin:");
eprintln!(" volume: {vol:.1} mm³");
eprintln!(
" bbox: [{:.1}, {:.1}, {:.1}] → [{:.1}, {:.1}, {:.1}]",
bbox[0], bbox[1], bbox[2], bbox[3], bbox[4], bbox[5]
);
eprintln!(
" faces={f}, edges={e}, verts={v}, euler={euler}, inner_loops={inner_loop_count}, adjusted_euler={adjusted_euler}"
);
assert!(
adjusted_euler == 2,
"Adjusted Euler should be 2: got {adjusted_euler} (raw={euler}, inner_loops={inner_loop_count})"
);
assert!(f < 200, "face count should be < 200: got {f}");
assert!(vol > 5000.0, "volume should be > 5000 mm³: got {vol:.1}");
let expected_z = WALL_HEIGHT + Z_PEAK; let z_extent = bbox[5] - bbox[2];
assert!(
(z_extent - expected_z).abs() < 2.0,
"Z extent should be ~{expected_z:.1}mm: got {z_extent:.1}"
);
}
#[test]
fn box_volume_sanity() {
let mut k = BrepKernel::new();
let result = k.execute_batch(
r#"[
{"op": "makeBox", "args": {"width": 10, "height": 10, "depth": 10}},
{"op": "volume", "args": {"solid": 0}}
]"#,
);
let parsed = parse_batch(&result);
assert_ok(&parsed, 0);
let vol = ok_f64(&parsed, 1);
let expected = 1000.0;
let rel_error = ((vol - expected) / expected).abs();
assert!(
rel_error < 0.01,
"box volume should be 1000: got {vol:.1}, error {:.1}%",
rel_error * 100.0
);
}
#[test]
fn rounded_shelled_body_classifies_cavity_outside() {
use brepkit_math::vec::Point3;
let mut k = BrepKernel::new();
let bf = make_rounded_rect_face(&mut k, OUTER_DIM, OUTER_DIM, CORNER_R, 0.0);
let bs = parse_batch(&k.execute_batch(&format!(
r#"[{{"op":"extrude","args":{{"face":{bf},"dz":1.0,"distance":{WALL_HEIGHT}}}}}]"#
)))[0]["ok"]
.as_u64()
.unwrap() as u32;
let tf = k
.get_solid_faces(bs)
.unwrap()
.iter()
.copied()
.find(|&fh| {
k.resolve_face(fh)
.ok()
.and_then(|fid| k.topo.face(fid).ok())
.and_then(brepkit_topology::face::Face::effective_plane_normal)
.is_some_and(|n| n.z() > 0.5 * n.length())
})
.unwrap();
let shelled = parse_batch(&k.execute_batch(&format!(
r#"[{{"op":"shell","args":{{"solid":{bs},"thickness":{WALL_THICKNESS},"faces":[{tf}]}}}}]"#
)))[0]["ok"]
.as_u64()
.unwrap() as u32;
let sid = k.resolve_solid(shelled).unwrap();
let topo = &*k.topo;
let cavity = brepkit_algo::classifier::classify_point(
topo,
sid,
Point3::new(0.0, 0.0, WALL_HEIGHT / 2.0),
)
.unwrap();
assert_eq!(
cavity,
brepkit_algo::FaceClass::Outside,
"open-cavity centre must classify Outside, got {cavity:?}"
);
let wall = brepkit_algo::classifier::classify_point(
topo,
sid,
Point3::new(
OUTER_DIM / 2.0 - WALL_THICKNESS / 2.0,
0.0,
WALL_HEIGHT / 2.0,
),
)
.unwrap();
assert_eq!(
wall,
brepkit_algo::FaceClass::Inside,
"wall-material point must classify Inside, got {wall:?}"
);
}