forked from 1b-agent/fieldcraft-upstream
When the grid layout does not fit a swept body (sides at an angle to the axes, root arcs where an arm meets its bosses), mesh.plan tries the ring-and-pillow layout (cmd/plan/pillow.rs): each boss is an annulus from its bore or pedal hole to its circle through every level, the arm a grid carved a margin inside the outline and the bosses, with a pillow layer of blocks to the wall each boundary face faces; a grid corner facing a boss and the outline gets a corner block reaching the kink where the outline leaves the boss's circle. The old crank test (frame cuts) becomes the heavy acceptance test at sizes 2.5, 3 and 4; the moved frame cut keeps a unit test. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
992bc71b6b
commit
7c3107dec0
3 files changed
+1186
-23
No files matched your search
@@ -35,6 +35,7 @@ use fieldcraft_model::{CutRole, Datum, MeshPlan, Partition, PlanCut, PlanFeature
|
||||
use serde_json::{Value, json};
|
||||
|
||||
mod blocks;
|
||||
mod pillow;
|
||||
|
||||
use crate::requirement::Requirement;
|
||||
use crate::schema::ProblemKind;
|
||||
@@ -969,7 +970,9 @@ fn layout_of(design: &Design, plan: &MeshPlan) -> Result<Option<std::result::Res
|
||||
None
|
||||
};
|
||||
let input = blocks::Input { shape, surfaces: &surfaces, mesh: &mesh, direction, rings: &plan.rings, blends: blend_faces, shell, size: plan.size };
|
||||
Ok(Some(blocks::layout(&input)))
|
||||
// The grid layout, else the ring-and-pillow layout (a section at an angle to the axes); the
|
||||
// grid's reason when neither fits.
|
||||
Ok(Some(blocks::layout(&input).or_else(|why| pillow::layout(&input).map_err(|other| format!("{why}; ring and pillow: {other}")))))
|
||||
}
|
||||
|
||||
fn plan(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
@@ -1653,6 +1656,39 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// A cut's rectangular opening whose ends are faces of the body (a pocket's end walls): the
|
||||
/// kernel cannot cut along a face (#427), so those frame sides move one element (twice the
|
||||
/// gap) out of the opening; the sides off every face stay where they are (#455).
|
||||
#[test]
|
||||
fn a_frame_side_on_an_opening_s_end_face_moves_one_element_out_of_it() {
|
||||
let x = [1.0, 0.0, 0.0];
|
||||
let y = [0.0, 1.0, 0.0];
|
||||
let z = [0.0, 0.0, 1.0];
|
||||
let body = super::Clearance {
|
||||
corners: vec![[-20.0, -20.0, 0.0], [190.0, 20.0, 16.0]],
|
||||
// The pocket's end walls (x = 48 and x = 130), facing into it.
|
||||
planes: vec![(x, 48.0, "F6:side:l4"), (x.map(|c| -c), -130.0, "F6:side:l2")],
|
||||
curved: Vec::new(),
|
||||
gap: 1.5,
|
||||
};
|
||||
let pocket = super::Imprint {
|
||||
feature: 6,
|
||||
cut: true,
|
||||
extent: None,
|
||||
sides: vec![(x, 48.0), (x, 130.0), (y, -7.0), (y, 7.0)],
|
||||
center: Some([89.0, 0.0, 4.0]),
|
||||
};
|
||||
let mut cuts = Vec::new();
|
||||
super::imprint_cuts(&[pocket], z, &body, &mut cuts);
|
||||
let at = |n: [f64; 3]| -> Vec<f64> {
|
||||
let mut v: Vec<f64> = cuts.iter().filter(|c| super::parallel(c.normal, n)).map(|c| super::dot(c.point, n)).collect();
|
||||
v.sort_by(f64::total_cmp);
|
||||
v
|
||||
};
|
||||
assert_eq!(at(x), vec![45.0, 133.0], "{cuts:?}");
|
||||
assert_eq!(at(y), vec![-7.0, 7.0], "{cuts:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_fillet_is_a_blend_layer_and_hostile_plans_are_refused() {
|
||||
let (_d, mut s) = open(json!([
|
||||
@@ -1893,30 +1929,26 @@ mod acceptance {
|
||||
accept(&mut s, &r, 0.5, 10.0);
|
||||
}
|
||||
|
||||
/// The crank arm sample, whole: the back pocket's frame cuts lie on its end faces (x = 48 and
|
||||
/// x = 130), which the kernel cannot cut along; the plan moves them one element out of the
|
||||
/// pocket, so both are made and the cells between the bosses mesh structured (#455).
|
||||
/// The crank arm sample, whole (#455, #190): its sides are at an angle to the axes and its
|
||||
/// root arcs meet the bosses at a kink, so it takes the ring-and-pillow layout: each boss an
|
||||
/// annulus from its bore or pedal hole to its circle through every level, the arm a carved
|
||||
/// grid with a pillow layer to the outline and the bosses' circles, a corner block at each
|
||||
/// root. One conforming all-hex mesh from the plan alone, at three sizes.
|
||||
#[test]
|
||||
fn the_crank_arm_meshes_from_its_plan_with_its_pocket_framed() {
|
||||
for size in [3.0, 4.0] {
|
||||
#[ignore = "heavy: cargo xtask test --heavy (CI heavy job)"]
|
||||
fn the_crank_arm_meshes_from_its_plan_with_rings_and_a_pillow() {
|
||||
for size in [2.5, 3.0, 4.0] {
|
||||
let mut s = Session::default();
|
||||
s.execute("file.sample", &json!({"name": "crank-arm"})).unwrap();
|
||||
s.execute("mesh.clear", &json!({})).unwrap();
|
||||
s.execute("geometry.partition.delete", &json!({"name": "Half"})).unwrap();
|
||||
let plan = s.execute("mesh.plan", &json!({"body": "CrankArm", "size": size})).unwrap();
|
||||
let frames: Vec<f64> =
|
||||
plan["cuts"].as_array().unwrap().iter().filter(|c| c["feature"] == 6).map(|c| c["point"][0].as_f64().unwrap()).collect();
|
||||
assert_eq!(frames.len(), 2, "{plan}");
|
||||
assert!(frames.iter().all(|x| (x - (48.0 - size)).abs() < 1e-9 || (x - (130.0 + size)).abs() < 1e-9), "{frames:?}");
|
||||
assert_eq!(plan["mesher"], "project", "size {size}: {plan}");
|
||||
assert!(plan["cuts"].as_array().is_none_or(Vec::is_empty), "{plan}");
|
||||
let r = s.execute("mesh.plan.apply", &json!({"plan": plan["name"]})).unwrap();
|
||||
assert_eq!(r["skipped_cuts"], json!([]), "{r}");
|
||||
assert_eq!((r["all_hex"].as_bool(), r["conforming"].as_bool()), (Some(true), Some(true)), "{r}");
|
||||
let q = s.execute("mesh.quality", &json!({})).unwrap();
|
||||
let (sj, er) = (q["metrics"]["scaled_jacobian"]["min"].as_f64().unwrap(), q["metrics"]["edge_ratio"]["max"].as_f64().unwrap());
|
||||
assert!(sj >= 0.45 && er <= 10.0, "size {size}: min scaled Jacobian {sj}, max edge ratio {er}");
|
||||
let geometry: f64 =
|
||||
s.execute("geometry.bodies", &json!({})).unwrap().as_array().unwrap().iter().map(|b| b["volume"].as_f64().unwrap()).sum();
|
||||
assert!((q["volume"].as_f64().unwrap() - geometry).abs() < 0.01 * geometry, "{q}");
|
||||
let layout = r["layout"].as_str().unwrap();
|
||||
assert!(layout.contains("2 circle(s) ringed") && layout.contains("pillow layer"), "{r}");
|
||||
accept(&mut s, &r, 0.5, 7.0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,8 @@ pub(crate) struct Layout {
|
||||
/// The frame round each circle group: its axis point (the sweep's coordinate 0) and the
|
||||
/// smallest half-side of the frame.
|
||||
pub(crate) frames: Vec<(V3, f64)>,
|
||||
/// The ring-and-pillow layout ([`super::pillow`]): the pillow's least thickness along the walls.
|
||||
pub(crate) pillow: Option<f64>,
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
@@ -80,13 +82,14 @@ impl Layout {
|
||||
pub(crate) fn summary(&self) -> String {
|
||||
let layers: Vec<String> = self.layers.iter().map(|(s, d, w)| format!("{s} {d:.4} mm ({w})")).collect();
|
||||
format!(
|
||||
"projection blocking: {} blocks over a {}×{}×{} grid, {} circle(s) ringed{}",
|
||||
"projection blocking: {} blocks over a {}×{}×{} grid, {} circle(s) ringed{}{}",
|
||||
self.topology.blocks.len(),
|
||||
self.planes[0].saturating_sub(1),
|
||||
self.planes[1].saturating_sub(1),
|
||||
self.planes[2].saturating_sub(1),
|
||||
self.rings,
|
||||
if layers.is_empty() { String::new() } else { format!(", a blend layer on {}", layers.join(", ")) }
|
||||
if layers.is_empty() { String::new() } else { format!(", a blend layer on {}", layers.join(", ")) },
|
||||
self.pillow.map_or_else(String::new, |t| format!(", a pillow layer from the carved grid to the walls ({t:.4} mm and more)"))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -107,7 +110,7 @@ pub(crate) struct Input<'a> {
|
||||
|
||||
/// The local frame: local axis `a` is world axis `PERM[k][a]`, `k` the sweep's world axis (a
|
||||
/// cyclic permutation, so blocks keep their handedness).
|
||||
const PERM: [[usize; 3]; 3] = [[1, 2, 0], [2, 0, 1], [0, 1, 2]];
|
||||
pub(super) const PERM: [[usize; 3]; 3] = [[1, 2, 0], [2, 0, 1], [0, 1, 2]];
|
||||
|
||||
const SIDE_NAMES: [[&str; 2]; 3] = [["-x", "+x"], ["-y", "+y"], ["-z", "+z"]];
|
||||
|
||||
@@ -700,7 +703,7 @@ pub(crate) fn layout(inp: &Input) -> Result<Layout, String> {
|
||||
(world([g.c[0], g.c[1], 0.0]), half)
|
||||
})
|
||||
.collect();
|
||||
Ok(Layout { topology: t, rings, layers, planes: planes.each_ref().map(Vec::len), frames })
|
||||
Ok(Layout { topology: t, rings, layers, planes: planes.each_ref().map(Vec::len), frames, pillow: None })
|
||||
}
|
||||
|
||||
/// The ring that takes in the blend with points `pts` (local frame): a blend round a corner of
|
||||
@@ -811,7 +814,7 @@ fn hex_faces(b: &[u32; 8]) -> [[u32; 4]; 6] {
|
||||
}
|
||||
|
||||
/// The Jacobian determinant at corner 1 of a hex8 block (positive: right-handed).
|
||||
fn corner_det(c: &[V3; 8]) -> f64 {
|
||||
pub(super) fn corner_det(c: &[V3; 8]) -> f64 {
|
||||
dot(sub(c[1], c[0]), cross(sub(c[3], c[0]), sub(c[4], c[0])))
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user