forked from 1b-agent/fieldcraft-upstream
mesh: split a projection block through its whole sheet; name twisted elements' block junction (#399)
Topology::split cuts a block at given fractions and every block chained to it through parallel edges, orienting the fractions through the chain, so the topology stays conforming. A certified-invalid element is reported with its block, cell, centre and the block vertex and blocks meeting there; the mesh is never emitted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
4dadaa50aa
commit
febd40fadc
6 files changed
+312
-23
No files matched your search
@@ -20,7 +20,7 @@ use crate::{CommandKind, CommandSpec, EngineError, Result, Session};
|
||||
pub const HEX_PROJECT: CommandSpec = CommandSpec {
|
||||
id: "mesh.hex.project",
|
||||
label: "Projection Blocking",
|
||||
params: r#"{"body": "Body1", "size": 2.0 (element size, mm; default 1/25 of the body's diagonal), "template": "grid" (default) | "sphere" | "butterfly" | "blocks", "center": [x, y, z], "radius": r, "axis": [0, 0, 1], "levels": [z0, z1, …] (sphere and butterfly: default from the body's bounding box; butterfly levels are distances along the axis from the centre), "inset": 0.55 (inner square or cube corner at this fraction of the radius), "grid": {"planes": {"x": [...], "y": [...], "z": [...]} or "cells": [ni, nj, nk] (even planes over the bounding box, default [1, 1, 1]), "carve": [{"i": [a, b], "j": [c, d], "k": [e, f]}] (block index ranges, inclusive, removed)}, "blocks": {"vertices": [[x, y, z], ...], "blocks": [[v1..v8], ...] (0-based vertices, hex8 order)}, "operations": [{"op": "o_grid", "block": b, "open": [faces], "inset": 0.5 or [i1, i2, i3], "extrude": {"offset": [dx, dy, dz], "layers": 1, "faces": "all" (the open face's butterfly: a round branch) | "inner" (its inner face: a square stub)}} | {"op": "extrude", "faces": [[block, face], ...], "offset": [dx, dy, dz], "layers": 1} | {"op": "connect", "from": [block, face], "to": [block, face], "layers": 1} (blocks bridging two faces: closes a handle) | {"op": "remove", "blocks": [...]} (carve; later blocks renumber) | {"op": "merge", "distance": d}] (applied in order; blocks are 0-based, new blocks appended, the O-grid's inner block keeping the old index; a face is its hex8 number 1-6 or the direction it faces, "+x" … "-z"), "preview": true (return the topology without meshing), "faces": [{"block": b, "face": 1-6, "to": ["face names"]}] (targets set by hand; other outer block faces go to the smooth group of B-rep faces nearest them), "divisions": [{"block": b, "direction": 1|2|3, "count": n}], "smoothing": "elliptic" (default) | "elliptic-controlled" (Thomas–Middlecoff) | "laplace" | "none", "iterations": 100, "min_quality": 0.2 (smallest scaled Jacobian accepted), "merge": distance (join nodes of other bodies' meshes within it; default 1e-6 of the diagonal, 0 never), "tolerance": chordal tolerance of the tessellation} — replaces the body's mesh with an all-hex mesh of the block topology: outer block faces projected onto their B-rep faces, edges between faces with different targets onto the curve where those meet (tangent-plane iteration), vertices on three onto their point, nodes placed by transfinite interpolation and smoothed with the boundary held on the geometry. Elements are tagged with the body and boundary faces with their B-rep faces. Reports blocks, divisions, targets and quality before and after smoothing; a corner that flattens on a curved face is an error that says to use an O-grid or template there"#,
|
||||
params: r#"{"body": "Body1", "size": 2.0 (element size, mm; default 1/25 of the body's diagonal), "template": "grid" (default) | "sphere" | "butterfly" | "blocks", "center": [x, y, z], "radius": r, "axis": [0, 0, 1], "levels": [z0, z1, …] (sphere and butterfly: default from the body's bounding box; butterfly levels are distances along the axis from the centre), "inset": 0.55 (inner square or cube corner at this fraction of the radius), "grid": {"planes": {"x": [...], "y": [...], "z": [...]} or "cells": [ni, nj, nk] (even planes over the bounding box, default [1, 1, 1]), "carve": [{"i": [a, b], "j": [c, d], "k": [e, f]}] (block index ranges, inclusive, removed)}, "blocks": {"vertices": [[x, y, z], ...], "blocks": [[v1..v8], ...] (0-based vertices, hex8 order)}, "operations": [{"op": "o_grid", "block": b, "open": [faces], "inset": 0.5 or [i1, i2, i3], "extrude": {"offset": [dx, dy, dz], "layers": 1, "faces": "all" (the open face's butterfly: a round branch) | "inner" (its inner face: a square stub)}} | {"op": "extrude", "faces": [[block, face], ...], "offset": [dx, dy, dz], "layers": 1} | {"op": "connect", "from": [block, face], "to": [block, face], "layers": 1} (blocks bridging two faces: closes a handle) | {"op": "remove", "blocks": [...]} (carve; later blocks renumber) | {"op": "merge", "distance": d} | {"op": "split", "block": b, "direction": 1|2|3 or "x"|"y"|"z", "at": [fractions]} (cuts the block and the whole sheet of blocks the cut runs through, so the topology stays conforming; fractions from the block's side-0 face, or from the low end along an axis; the block keeps its first slab, the others are appended)] (applied in order; blocks are 0-based, new blocks appended, the O-grid's inner block keeping the old index; a face is its hex8 number 1-6 or the direction it faces, "+x" … "-z"), "preview": true (return the topology without meshing), "faces": [{"block": b, "face": 1-6, "to": ["face names"]}] (targets set by hand; other outer block faces go to the smooth group of B-rep faces nearest them), "divisions": [{"block": b, "direction": 1|2|3, "count": n}], "smoothing": "elliptic" (default) | "elliptic-controlled" (Thomas–Middlecoff) | "laplace" | "none", "iterations": 100, "min_quality": 0.2 (smallest scaled Jacobian accepted), "merge": distance (join nodes of other bodies' meshes within it; default 1e-6 of the diagonal, 0 never), "tolerance": chordal tolerance of the tessellation} — replaces the body's mesh with an all-hex mesh of the block topology: outer block faces projected onto their B-rep faces, edges between faces with different targets onto the curve where those meet (tangent-plane iteration), vertices on three onto their point, nodes placed by transfinite interpolation and smoothed with the boundary held on the geometry. Elements are tagged with the body and boundary faces with their B-rep faces. Reports blocks, divisions, targets and quality before and after smoothing; a corner that flattens on a curved face is an error that says to use an O-grid or template there"#,
|
||||
form: Some(super::forms::HEX_PROJECT),
|
||||
kind: CommandKind::Model,
|
||||
requires: &[Requirement::Geometry],
|
||||
@@ -101,6 +101,20 @@ enum Operation {
|
||||
Merge {
|
||||
distance: f64,
|
||||
},
|
||||
Split {
|
||||
block: usize,
|
||||
direction: DirSel,
|
||||
at: Vec<f64>,
|
||||
},
|
||||
}
|
||||
|
||||
/// A block direction: 1, 2 or 3 (along 1→2, 1→4, 1→5), or the axis it runs nearest ("x", "y",
|
||||
/// "z"; fractions then count from its low end).
|
||||
#[derive(Deserialize, Clone)]
|
||||
#[serde(untagged)]
|
||||
enum DirSel {
|
||||
Number(usize),
|
||||
Axis(String),
|
||||
}
|
||||
|
||||
fn face_of(t: &Topology, block: usize, f: &FaceSel) -> Result<u8> {
|
||||
@@ -164,6 +178,25 @@ fn operate(t: &mut Topology, ops: &[Operation]) -> Result<()> {
|
||||
}
|
||||
t.merge(*distance);
|
||||
}
|
||||
Operation::Split { block, direction, at } => {
|
||||
bounded(at, "operations.at")?;
|
||||
let (d, against) = match direction {
|
||||
DirSel::Number(n @ 1..=3) => (n - 1, false),
|
||||
DirSel::Number(n) => return Err(EngineError::Params(format!("split direction {n}: 1, 2, 3 or x, y, z"))),
|
||||
DirSel::Axis(a) => {
|
||||
let axis = match a.as_str() {
|
||||
"x" => [1.0, 0.0, 0.0],
|
||||
"y" => [0.0, 1.0, 0.0],
|
||||
"z" => [0.0, 0.0, 1.0],
|
||||
other => return Err(EngineError::Params(format!("split direction `{other}`: 1, 2, 3 or x, y, z"))),
|
||||
};
|
||||
t.direction_along(*block, axis)?
|
||||
}
|
||||
};
|
||||
// Fractions count from the block's side-0 face; along an axis, from its low end.
|
||||
let at: Vec<f64> = if against { at.iter().rev().map(|x| 1.0 - x).collect() } else { at.clone() };
|
||||
t.split(*block, d, &at)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -99,6 +99,10 @@ fn hostile_parameters_change_nothing() {
|
||||
json!({"body": "Ball", "template": "sphere", "operations": [{"op": "o_grid", "block": 77}]}),
|
||||
json!({"body": "Ball", "template": "sphere", "operations": [{"op": "extrude", "faces": [[0, "+w"]], "offset": [0, 0, 1]}]}),
|
||||
json!({"body": "Ball", "template": "sphere", "operations": [{"op": "merge", "distance": -1}]}),
|
||||
json!({"body": "Ball", "template": "sphere", "operations": [{"op": "split", "block": 0, "direction": "w", "at": [0.5]}]}),
|
||||
json!({"body": "Ball", "template": "sphere", "operations": [{"op": "split", "block": 0, "direction": 4, "at": [0.5]}]}),
|
||||
json!({"body": "Ball", "template": "sphere", "operations": [{"op": "split", "block": 0, "direction": "x", "at": [1.5]}]}),
|
||||
json!({"body": "Ball", "template": "sphere", "operations": [{"op": "split", "block": 99, "direction": 1, "at": [0.5]}]}),
|
||||
json!({"body": "Ball", "template": "blocks"}),
|
||||
json!({"body": "Ball", "template": "blocks", "blocks": {"vertices": [[0, 0, 0]], "blocks": [[0, 0, 0, 0, 0, 0, 0, 0]]}}),
|
||||
json!({"body": "Ball", "grid": {"cells": [0, 1, 1]}}),
|
||||
@@ -128,26 +132,66 @@ fn cup_params(size: f64, inset: Value) -> Value {
|
||||
json!({"body": "Cup", "template": "butterfly", "center": [0, 0, 0], "axis": [0, 0, 1], "radius": 20, "levels": [0, 4, 6, 14, 18, 26, 30], "size": size,
|
||||
"operations": [
|
||||
{"op": "o_grid", "block": 0, "open": [1, 2], "inset": 0.6},
|
||||
{"op": "o_grid", "block": 14, "open": ["+x"], "inset": inset},
|
||||
{"op": "o_grid", "block": 22, "open": ["+x"], "inset": inset},
|
||||
{"op": "split", "block": 14, "direction": "y", "at": [0.36, 0.64]},
|
||||
{"op": "o_grid", "block": 54, "open": ["+x"], "inset": inset},
|
||||
{"op": "o_grid", "block": 62, "open": ["+x"], "inset": inset},
|
||||
{"op": "extrude", "faces": [[54, "+x"]], "offset": [12, 0, 0]},
|
||||
{"op": "extrude", "faces": [[84, "+x"]], "offset": [4, 0, 0]},
|
||||
{"op": "extrude", "faces": [[62, "+x"]], "offset": [12, 0, 0]},
|
||||
{"op": "extrude", "faces": [[86, "+x"]], "offset": [4, 0, 0]},
|
||||
{"op": "connect", "from": [85, "+z"], "to": [87, "-z"], "layers": 2},
|
||||
{"op": "remove", "blocks": [1, 2, 3, 4, 5, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45]}
|
||||
]})
|
||||
}
|
||||
|
||||
/// A cup with a handle: a double O-grid section (the floor's butterfly inside the wall's), the
|
||||
/// cavity carved, the handle a square bar extruded from O-grids in the wall and closed by a
|
||||
/// connection. The wall's +x petals are split in three around the handle first, so each arm's
|
||||
/// footprint sits in a block face with even margins round it (#399).
|
||||
fn cup_with_a_handle_at(size: f64) {
|
||||
let (_d, mut s) = cup_session();
|
||||
let r = s.execute("mesh.hex.project", &cup_params(size, json!(0.5))).unwrap();
|
||||
assert!(sj(&r) >= 0.3, "{r}");
|
||||
let (v, body) = (r["volume"].as_f64().unwrap(), r["body_volume"].as_f64().unwrap());
|
||||
assert!((v - body).abs() < 0.01 * body, "{r}");
|
||||
assert!(r["max_gap"].as_f64().unwrap() < 1e-6, "{r}");
|
||||
let q = s.execute("mesh.quality", &json!({})).unwrap();
|
||||
assert_eq!(q["valid"].as_u64(), Some(s.model.elements().len() as u64), "{q}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cup_with_a_handle() {
|
||||
cup_with_a_handle_at(2.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "heavy: cargo xtask test --heavy (CI heavy job)"]
|
||||
fn cup_with_a_handle_refined() {
|
||||
cup_with_a_handle_at(1.0);
|
||||
}
|
||||
|
||||
/// Without the split, the ring block under the upper arm is squeezed between the petal's edge
|
||||
/// and the window-top plane and folds: the mesher refuses the mesh and names the twisted
|
||||
/// elements' block and the block junction they touch.
|
||||
#[test]
|
||||
fn twisted_junction_elements_are_reported_not_meshed() {
|
||||
let (_d, mut s) = cup_session();
|
||||
let mut p = cup_params(2.0, json!([0.5, 0.3, 0.3]));
|
||||
p["operations"] = json!([
|
||||
{"op": "o_grid", "block": 0, "open": [1, 2], "inset": 0.6},
|
||||
{"op": "o_grid", "block": 14, "open": ["+x"], "inset": [0.5, 0.3, 0.3]},
|
||||
{"op": "o_grid", "block": 22, "open": ["+x"], "inset": [0.5, 0.3, 0.3]},
|
||||
{"op": "extrude", "faces": [[14, "+x"]], "offset": [12, 0, 0]},
|
||||
{"op": "extrude", "faces": [[44, "+x"]], "offset": [4, 0, 0]},
|
||||
{"op": "extrude", "faces": [[22, "+x"]], "offset": [12, 0, 0]},
|
||||
{"op": "extrude", "faces": [[46, "+x"]], "offset": [4, 0, 0]},
|
||||
{"op": "connect", "from": [45, "+z"], "to": [47, "-z"], "layers": 2},
|
||||
{"op": "remove", "blocks": [1, 2, 3, 4, 5]}
|
||||
]})
|
||||
}
|
||||
|
||||
/// A cup with a handle: a double O-grid section (the floor's butterfly inside the wall's), the
|
||||
/// cavity carved, the handle a square bar extruded from O-grids in the wall and closed by a
|
||||
/// connection. Open: a few elements at the upper handle junction stay folded (#399).
|
||||
#[test]
|
||||
#[ignore = "#399: the upper handle junction leaves a few folded elements"]
|
||||
fn cup_with_a_handle() {
|
||||
let (_d, mut s) = cup_session();
|
||||
let r = s.execute("mesh.hex.project", &cup_params(2.0, json!([0.5, 0.3, 0.3]))).unwrap();
|
||||
assert!(sj(&r) >= 0.3, "{r}");
|
||||
]);
|
||||
let model = s.model.clone();
|
||||
let e = s.execute("mesh.hex.project", &p).unwrap_err().to_string();
|
||||
assert!(e.contains("twisted") && e.contains("where blocks 17, 37, 38, 41 meet"), "{e}");
|
||||
assert_eq!(s.model, model);
|
||||
}
|
||||
|
||||
/// The cup without its handle: the floor's O-grid inside the wall's butterfly, cavity carved.
|
||||
@@ -158,8 +202,7 @@ fn cup_floor_double_o_grid() {
|
||||
{"command": "solid.cylinder", "params": {"base": [0, 0, 4], "radius": 16, "height": 27, "operation": "cut", "targets": ["Cup"]}}
|
||||
]));
|
||||
let mut p = cup_params(2.0, json!(0.5));
|
||||
let ops = p["operations"].as_array().unwrap().clone();
|
||||
p["operations"] = json!([ops[0], ops[8]]);
|
||||
p["operations"] = json!([{"op": "o_grid", "block": 0, "open": [1, 2], "inset": 0.6}, {"op": "remove", "blocks": [1, 2, 3, 4, 5]}]);
|
||||
let r = s.execute("mesh.hex.project", &p).unwrap();
|
||||
assert!(sj(&r) >= 0.5, "{r}");
|
||||
let (v, body) = (r["volume"].as_f64().unwrap(), r["body_volume"].as_f64().unwrap());
|
||||
|
||||
Reference in new issue
Block a user