forked from 1b-agent/fieldcraft-upstream
mesh: an enclosure's corner bosses are ringed where its walls join them (#402)
A boss the body's walls run into had its circle crossed by the walls' planes, so the projection layout refused it and the plan fell back to swept cells, fanning round the boss and its hole. Such a boss is now framed by the square round its whole circle, subdivided by the planes that cross it; every square inside (the hole's, the core) is divided alike, so the ring's blocks meet the walls node to node. On the walls' side the frame stops at their inner plane and its corner projects onto the middle of the inner fillet, which the ring takes in (a boss just touching the fillet included: no sliver). Cylinders a fillet's history names but that are a ring's circle are the ring's faces. Rings are built per patch of grid cells, the one-cell O-grid being the simplest patch. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
02b408c499
commit
0b7dda95f7
3 files changed
+205
-36
No files matched your search
@@ -1559,6 +1559,35 @@ mod acceptance {
|
||||
accept(&mut s, &r, 0.6, 6.0);
|
||||
}
|
||||
|
||||
/// An enclosure base: a box with rounded vertical edges, shelled open at the top, a screw
|
||||
/// boss joined at each corner (centred outside it, nearly touching the inner fillet) with a
|
||||
/// blind hole, and standoffs on the floor. Each boss's ring frames the whole circle,
|
||||
/// subdivided where the walls join it; the inner fillet is taken in by the ring's corner and
|
||||
/// the boss's near touch with it leaves no sliver.
|
||||
#[test]
|
||||
fn an_enclosure_base_rings_each_corner_boss_where_the_walls_join_it() {
|
||||
let mut commands = vec![
|
||||
json!({"command": "solid.box", "params": {"length": 56, "width": 42, "height": 16, "body_name": "Base"}}),
|
||||
json!({"command": "solid.fillet", "params": {"edges": [[0, 0, 8], [56, 0, 8], [56, 42, 8], [0, 42, 8]], "radius": 5}}),
|
||||
json!({"command": "solid.shell", "params": {"faces": [[28, 21, 16]], "thickness": 3, "body": "Base"}}),
|
||||
];
|
||||
for (x, y) in [(-1.0, -1.0), (57.0, -1.0), (-1.0, 43.0), (57.0, 43.0)] {
|
||||
commands.push(json!({"command": "solid.cylinder", "params": {"diameter": 13, "height": 16, "base": [x, y, 0], "operation": "join", "targets": ["Base"]}}));
|
||||
}
|
||||
for (x, y) in [(-1.0, -1.0), (57.0, -1.0), (-1.0, 43.0), (57.0, 43.0)] {
|
||||
commands.push(json!({"command": "solid.hole", "params": {"position": [x, y, 16], "diameter": 3.3, "depth": 8}}));
|
||||
}
|
||||
for (x, y) in [(16.5, 16.5), (39.5, 16.5)] {
|
||||
commands.push(json!({"command": "solid.cylinder", "params": {"diameter": 6, "height": 4, "base": [x, y, 3], "operation": "join", "targets": ["Base"]}}));
|
||||
}
|
||||
let (_d, mut s) = open(json!(commands));
|
||||
let plan = s.execute("mesh.plan", &json!({"body": "Base", "size": 1.5, "layers": 2})).unwrap();
|
||||
assert_eq!(plan["mesher"], "project", "{plan}");
|
||||
let r = s.execute("mesh.plan.apply", &json!({"plan": plan["name"]})).unwrap();
|
||||
assert!(r["layout"].as_str().unwrap().contains("10 circle(s) ringed"), "{r}");
|
||||
accept(&mut s, &r, 0.5, 10.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_filleted_box_meshes_structured_round_its_corners() {
|
||||
let (_d, mut s) = open(json!([
|
||||
|
||||
@@ -14,7 +14,12 @@
|
||||
//! side of the body near the hole is the frame there and the far side is as far; a fillet
|
||||
//! round a corner of the outline that holds a hole (a screw hole in a rounded corner) is taken
|
||||
//! in by the hole's ring, its frame reaching the two sides and its outer corner projected onto
|
||||
//! the fillet's middle, so the corner needs no blend layer.
|
||||
//! the fillet's middle, so the corner needs no blend layer. A boss the body's walls run into
|
||||
//! (planes of the body crossing its circle outside it) is framed by the square round its
|
||||
//! whole circle, subdivided by those planes, every square inside it divided alike so the
|
||||
//! ring's blocks meet the walls node to node; on the walls' side the frame stops at their
|
||||
//! inner plane and its corner projects onto the inner fillet's middle (a boss just touching
|
||||
//! the fillet included).
|
||||
//! - **Blend layer.** A side of the body a fillet or chamfer runs along, or that a shell's wall
|
||||
//! stands on, gets a layer of blocks between the grid (on the body shrunk by the blend's
|
||||
//! depth or the wall's thickness) and the side, one block per boundary face of the grid, the
|
||||
@@ -161,6 +166,14 @@ struct Group {
|
||||
/// The frame's half-side per local axis when a corner blend it takes in sets it: the
|
||||
/// distance from the centre to the body's side the blend runs along.
|
||||
forced: [Option<f64>; 2],
|
||||
/// A boss the body's walls run into: planes of the body cross its circle (outside it, where
|
||||
/// the walls join), so its frame is the square round the whole circle, subdivided by those
|
||||
/// planes, and the ring's blocks follow the subdivision.
|
||||
patch: bool,
|
||||
/// The extent per local axis of the concave blends a patch takes in (the inner fillet where
|
||||
/// the walls meet, in pieces where the boss touches it): the frame's side on the blends'
|
||||
/// side of the centre goes to their middle.
|
||||
blends: Option<[[f64; 2]; 2]>,
|
||||
}
|
||||
|
||||
/// Plan the block topology of the body.
|
||||
@@ -219,6 +232,8 @@ pub(crate) fn layout(inp: &Input) -> Result<Layout, String> {
|
||||
// The faces: planes square to an axis (with their outward sign), the circles' cylinders, and
|
||||
// the blends; anything else is not this layout's.
|
||||
let mut planes_on: [Vec<(f64, f64)>; 3] = [Vec::new(), Vec::new(), Vec::new()];
|
||||
// The planar faces square to an in-plane axis: (axis, offset, face).
|
||||
let mut side_faces: Vec<(usize, f64, usize)> = Vec::new();
|
||||
let mut blend_faces: Vec<usize> = Vec::new();
|
||||
for (f, s) in inp.surfaces.iter().enumerate() {
|
||||
let blend = inp.blends.contains(&(f as u32));
|
||||
@@ -226,15 +241,21 @@ pub(crate) fn layout(inp: &Input) -> Result<Layout, String> {
|
||||
Some(FaceSurface::Plane { normal, point }) => {
|
||||
let n = local(*normal);
|
||||
match (0..3).find(|&a| (n[a].abs() - 1.0).abs() < 1e-6) {
|
||||
Some(a) => planes_on[a].push((local(*point)[a], n[a].signum())),
|
||||
Some(a) => {
|
||||
planes_on[a].push((local(*point)[a], n[a].signum()));
|
||||
if a < 2 {
|
||||
side_faces.push((a, local(*point)[a], f));
|
||||
}
|
||||
}
|
||||
None if blend => blend_faces.push(f),
|
||||
None => return Err(format!("face `{}` is a plane at an angle to the axes", name(f))),
|
||||
}
|
||||
}
|
||||
Some(FaceSurface::Cylinder { origin, axis, radius }) if !blend => {
|
||||
if !((dot(*axis, d).abs() - 1.0).abs() < 1e-6 && on_circle(*origin, *radius)) {
|
||||
return Err(format!("face `{}` is a cylinder that is not one of the plan's rings", name(f)));
|
||||
}
|
||||
// A ring's cylinder, even where a fillet's history names it (a boss over a filleted
|
||||
// corner).
|
||||
Some(FaceSurface::Cylinder { origin, axis, radius }) if (dot(*axis, d).abs() - 1.0).abs() < 1e-6 && on_circle(*origin, *radius) => {}
|
||||
Some(FaceSurface::Cylinder { .. }) if !blend => {
|
||||
return Err(format!("face `{}` is a cylinder that is not one of the plan's rings", name(f)));
|
||||
}
|
||||
_ if blend => blend_faces.push(f),
|
||||
_ => return Err(format!("face `{}` is curved and neither a ring's cylinder nor a fillet's", name(f))),
|
||||
@@ -250,11 +271,25 @@ pub(crate) fn layout(inp: &Input) -> Result<Layout, String> {
|
||||
g.circles.push(*c);
|
||||
}
|
||||
}
|
||||
None => groups.push(Group { c: c.c, circles: vec![*c], frame: [[0.0; 2]; 2], forced: [None; 2] }),
|
||||
None => groups.push(Group { c: c.c, circles: vec![*c], frame: [[0.0; 2]; 2], forced: [None; 2], patch: false, blends: None }),
|
||||
}
|
||||
}
|
||||
for g in &mut groups {
|
||||
g.circles.sort_by(|a, b| b.r.total_cmp(&a.r));
|
||||
let Some(outer) = g.circles.first().copied() else { continue };
|
||||
let r0 = outer.r;
|
||||
let crossing: Vec<usize> = side_faces.iter().filter(|(a, v, _)| (v - g.c[*a]).abs() < r0 - eps).map(|(_, _, f)| *f).collect();
|
||||
if outer.kind != RingKind::Butterfly || crossing.is_empty() {
|
||||
continue;
|
||||
}
|
||||
// The walls join the boss outside its circle: no face of theirs inside it.
|
||||
for f in crossing {
|
||||
let pts = face_points.get(f).map_or(&[][..], Vec::as_slice);
|
||||
if pts.iter().any(|p| (p[0] - g.c[0]).hypot(p[1] - g.c[1]) < r0 * (1.0 - 1e-3)) {
|
||||
return Err(format!("face `{}` runs through the circle of radius {r0:.4}", name(f)));
|
||||
}
|
||||
}
|
||||
g.patch = true;
|
||||
}
|
||||
|
||||
// The blend layer: per side, the depth of the blends along it, or the shell's wall. A blend
|
||||
@@ -273,6 +308,13 @@ pub(crate) fn layout(inp: &Input) -> Result<Layout, String> {
|
||||
g.forced = half.map(Some);
|
||||
continue;
|
||||
}
|
||||
if let Some((g, extent)) = patch_blend(&groups, pts)
|
||||
&& let Some(g) = groups.get_mut(g)
|
||||
{
|
||||
let union = g.blends.map_or(extent, |b| std::array::from_fn(|a| [b[a][0].min(extent[a][0]), b[a][1].max(extent[a][1])]));
|
||||
g.blends = Some(union);
|
||||
continue;
|
||||
}
|
||||
let mut touched = false;
|
||||
for a in 0..3 {
|
||||
for s in 0..2 {
|
||||
@@ -341,11 +383,26 @@ pub(crate) fn layout(inp: &Input) -> Result<Layout, String> {
|
||||
let r0 = outer.r;
|
||||
for a in 0..2 {
|
||||
let c = g.c[a];
|
||||
if fixed[a].iter().any(|v| (v - c).abs() < r0 - eps) {
|
||||
if !g.patch && fixed[a].iter().any(|v| (v - c).abs() < r0 - eps) {
|
||||
return Err(format!("a face of the body runs through the circle of radius {r0:.4}"));
|
||||
}
|
||||
}
|
||||
let half = match outer.kind {
|
||||
// The square round the whole circle (its corners projected onto it at 45°), or
|
||||
// where a blend it takes in sets a side, at the walls' inner plane.
|
||||
RingKind::Butterfly if g.patch => std::array::from_fn(|a| {
|
||||
let mut h = [r0; 2];
|
||||
// The blends' near edge: the walls' inner plane, whose corner projects onto
|
||||
// the blend's middle.
|
||||
if let Some(b) = g.blends {
|
||||
if 0.5 * (b[a][0] + b[a][1]) > g.c[a] {
|
||||
h[1] = b[a][0] - g.c[a];
|
||||
} else {
|
||||
h[0] = g.c[a] - b[a][1];
|
||||
}
|
||||
}
|
||||
h
|
||||
}),
|
||||
RingKind::Butterfly => [[r0 / 2f64.sqrt(); 2]; 2],
|
||||
RingKind::OGrid => o_grid_frame(g, &fixed, eps)?,
|
||||
};
|
||||
@@ -353,7 +410,7 @@ pub(crate) fn layout(inp: &Input) -> Result<Layout, String> {
|
||||
let c = g.c[a];
|
||||
for s in 0..2 {
|
||||
let h = half[a][s];
|
||||
if outer.kind == RingKind::Butterfly && fixed[a].iter().any(|v| (v - c).abs() < h - eps) {
|
||||
if outer.kind == RingKind::Butterfly && !g.patch && fixed[a].iter().any(|v| (v - c).abs() < h - eps) {
|
||||
return Err(format!("a face of the body runs through the square inside the circle of radius {r0:.4}"));
|
||||
}
|
||||
g.frame[a][s] = if s == 0 { c - h } else { c + h };
|
||||
@@ -417,11 +474,14 @@ pub(crate) fn layout(inp: &Input) -> Result<Layout, String> {
|
||||
}
|
||||
for g in &groups {
|
||||
for a in 0..2 {
|
||||
// A patch's frame may stop short of its circle where the walls join (the ring's
|
||||
// faces there are projected out onto it), but holds the circles inside.
|
||||
let r0 = g.circles.first().map_or(0.0, |c| c.r);
|
||||
if !(g.frame[a][0] < g.c[a] - 0.7 * r0 && g.frame[a][1] > g.c[a] + 0.7 * r0) {
|
||||
let hold = if g.patch { 1.2 * g.circles.get(1).map_or(0.0, |c| c.r) } else { 0.7 * r0 };
|
||||
if !(g.frame[a][0] < g.c[a] - hold && g.frame[a][1] > g.c[a] + hold) {
|
||||
return Err(format!("the frame round the circle of radius {r0:.4} does not hold it"));
|
||||
}
|
||||
if planes[a].iter().any(|p| *p > g.frame[a][0] + eps && *p < g.frame[a][1] - eps) {
|
||||
if !g.patch && planes[a].iter().any(|p| *p > g.frame[a][0] + eps && *p < g.frame[a][1] - eps) {
|
||||
return Err(format!("a plane of the body runs through the frame round the circle of radius {r0:.4}"));
|
||||
}
|
||||
}
|
||||
@@ -444,19 +504,25 @@ pub(crate) fn layout(inp: &Input) -> Result<Layout, String> {
|
||||
.map(|(b, _)| [mid(&planes[0], b % n[0]), mid(&planes[1], (b / n[0]) % n[1]), mid(&planes[2], b / (n[0] * n[1]))])
|
||||
.collect();
|
||||
let index = |p: &[f64], v: f64| p.iter().position(|x| (x - v).abs() < eps);
|
||||
// The grid cells the rings replace.
|
||||
let mut replaced: Vec<usize> = Vec::new();
|
||||
let mut rings = 0;
|
||||
for g in &groups {
|
||||
let (Some(i0), Some(j0)) = (index(&planes[0], g.frame[0][0]), index(&planes[1], g.frame[1][0])) else {
|
||||
let (Some(i0), Some(i1), Some(j0), Some(j1)) =
|
||||
(index(&planes[0], g.frame[0][0]), index(&planes[0], g.frame[0][1]), index(&planes[1], g.frame[1][0]), index(&planes[1], g.frame[1][1]))
|
||||
else {
|
||||
return Err("a frame lost its planes".into());
|
||||
};
|
||||
let r0 = g.circles.first().map_or(0.0, |c| c.r);
|
||||
// The squares inside the frame: one per circle (corners at 45° on it), then the core.
|
||||
// Each: half-sides [-u, +u, -v, +v] and the radius it stands for per side.
|
||||
// Each: half-sides [-u, +u, -v, +v] and the radius it stands for per side (a patch's
|
||||
// frame stands for its circle, which the frame's corners are projected onto).
|
||||
let mut squares: Vec<([f64; 4], [f64; 4])> = Vec::new();
|
||||
let fr = [g.c[0] - g.frame[0][0], g.frame[0][1] - g.c[0], g.c[1] - g.frame[1][0], g.frame[1][1] - g.c[1]];
|
||||
squares.push((fr, fr));
|
||||
squares.push((fr, if g.patch { [r0; 4] } else { fr }));
|
||||
for c in &g.circles {
|
||||
let h = c.r / 2f64.sqrt();
|
||||
if squares.last().is_some_and(|(s, _)| s.iter().all(|x| (x - h).abs() < eps)) {
|
||||
if squares.last().is_some_and(|(s, _)| s.iter().all(|x| (x - h).abs() < eps)) || (g.patch && c.r == r0) {
|
||||
continue;
|
||||
}
|
||||
squares.push(([h; 4], [c.r; 4]));
|
||||
@@ -465,31 +531,71 @@ pub(crate) fn layout(inp: &Input) -> Result<Layout, String> {
|
||||
let inner = g.circles.last().map_or(0.0, |c| c.r);
|
||||
squares.push(([inner / (2.0 * 2f64.sqrt()); 4], [0.0; 4]));
|
||||
if squares.windows(2).any(|w| (0..4).any(|i| !(w[1].0[i] < w[0].0[i] - eps))) {
|
||||
return Err(format!("the frame round the circle of radius {:.4} is too tight for its ring", g.circles.first().map_or(0.0, |c| c.r)));
|
||||
return Err(format!("the frame round the circle of radius {r0:.4} is too tight for its ring"));
|
||||
}
|
||||
// The frame's subdivision by the grid's planes, as fractions along each side: every
|
||||
// square inside is divided alike, so the ring's blocks meet node to node.
|
||||
let frac = |a: usize, lo: usize, hi: usize| -> Vec<f64> {
|
||||
let (p0, p1) = (planes[a][lo], planes[a][hi]);
|
||||
planes[a][lo..=hi].iter().map(|p| (p - p0) / (p1 - p0)).collect()
|
||||
};
|
||||
let (fu, fv) = (frac(0, i0, i1), frac(1, j0, j1));
|
||||
// Point (u, v) (fractions along the axes) of square `k`.
|
||||
let at = |k: usize, u: f64, v: f64| -> [f64; 2] {
|
||||
let h = squares.get(k).map_or([0.0; 4], |s| s.0);
|
||||
[g.c[0] - h[0] + u * (h[0] + h[1]), g.c[1] - h[2] + v * (h[2] + h[3])]
|
||||
};
|
||||
// The frame's boundary, counter-clockwise, as (u, v) fractions with the side each
|
||||
// segment that starts there lies on (0: -u, 1: +u, 2: -v, 3: +v).
|
||||
let mut around: Vec<([f64; 2], usize)> = Vec::new();
|
||||
around.extend(fu[..fu.len() - 1].iter().map(|&u| ([u, 0.0], 2)));
|
||||
around.extend(fv[..fv.len() - 1].iter().map(|&v| ([1.0, v], 1)));
|
||||
around.extend(fu[1..].iter().rev().map(|&u| ([u, 1.0], 3)));
|
||||
around.extend(fv[1..].iter().rev().map(|&v| ([0.0, v], 0)));
|
||||
for w in 0..n[2] {
|
||||
let cell = (w * n[1] + j0) * n[0] + i0;
|
||||
let zmid = mid(&planes[2], w);
|
||||
for pair in squares.windows(2) {
|
||||
let ((_, outer_rho), (half, inner_rho)) = (pair[0], pair[1]);
|
||||
let first = t.blocks.len();
|
||||
t.o_grid(cell, &[1, 2], [0.5; 3]).map_err(|e| e.to_string())?;
|
||||
let corners = t.blocks.get(cell).copied().ok_or("an O-grid lost its block")?;
|
||||
for (bits, v) in [[0, 0], [1, 0], [1, 1], [0, 1], [0, 0], [1, 0], [1, 1], [0, 1]].iter().zip(corners) {
|
||||
if let Some(p) = t.vertices.get_mut(v as usize) {
|
||||
p[0] = if bits[0] == 0 { g.c[0] - half[0] } else { g.c[0] + half[1] };
|
||||
p[1] = if bits[1] == 0 { g.c[1] - half[2] } else { g.c[1] + half[3] };
|
||||
let (z0, z1) = (planes[2][w], planes[2][w + 1]);
|
||||
let zmid = 0.5 * (z0 + z1);
|
||||
for i in i0..i1 {
|
||||
for j in j0..j1 {
|
||||
replaced.push((w * n[1] + j) * n[0] + i);
|
||||
}
|
||||
}
|
||||
let add = |q: [[f64; 2]; 4], pr: V3, t: &mut Topology, probe: &mut Vec<V3>| {
|
||||
let base = t.vertices.len() as u32;
|
||||
for z in [z0, z1] {
|
||||
for p in q {
|
||||
t.vertices.push([p[0], p[1], z]);
|
||||
}
|
||||
}
|
||||
// The four blocks round it, in the O-grid's order: -v, +u, +v, -u.
|
||||
for (q, (dir, side)) in [([0.0, -1.0], 2), ([1.0, 0.0], 1), ([0.0, 1.0], 3), ([-1.0, 0.0], 0)].into_iter().enumerate() {
|
||||
let mut b = [base, base + 1, base + 2, base + 3, base + 4, base + 5, base + 6, base + 7];
|
||||
let pos = b.map(|v| t.vertices.get(v as usize).copied().unwrap_or_default());
|
||||
if corner_det(&pos) < 0.0 {
|
||||
b = [b[0], b[3], b[2], b[1], b[4], b[7], b[6], b[5]];
|
||||
}
|
||||
t.blocks.push(b);
|
||||
probe.push(pr);
|
||||
};
|
||||
for (k, pair) in squares.windows(2).enumerate() {
|
||||
let (outer_rho, inner_rho) = (pair[0].1, pair[1].1);
|
||||
for (s, &(p, side)) in around.iter().enumerate() {
|
||||
let Some(&(q, _)) = around.get((s + 1) % around.len()) else { continue };
|
||||
let quad = [at(k, p[0], p[1]), at(k, q[0], q[1]), at(k + 1, q[0], q[1]), at(k + 1, p[0], p[1])];
|
||||
// The probe: between the two radii the squares stand for, toward the
|
||||
// segment's middle.
|
||||
let m = [0.5 * (quad[0][0] + quad[1][0]) - g.c[0], 0.5 * (quad[0][1] + quad[1][1]) - g.c[1]];
|
||||
let len = m[0].hypot(m[1]).max(eps);
|
||||
let rho = 0.5 * (outer_rho[side] + inner_rho[side]);
|
||||
if first + q < t.blocks.len() {
|
||||
probe.push([g.c[0] + dir[0] * rho, g.c[1] + dir[1] * rho, zmid]);
|
||||
}
|
||||
add(quad, [g.c[0] + m[0] / len * rho, g.c[1] + m[1] / len * rho, zmid], &mut t, &mut probe);
|
||||
}
|
||||
if let Some(p) = probe.get_mut(cell) {
|
||||
*p = [g.c[0], g.c[1], zmid];
|
||||
}
|
||||
// The core, divided as the frame.
|
||||
let last = squares.len() - 1;
|
||||
for u in fu.windows(2) {
|
||||
for v in fv.windows(2) {
|
||||
let quad = [at(last, u[0], v[0]), at(last, u[1], v[0]), at(last, u[1], v[1]), at(last, u[0], v[1])];
|
||||
let cx = 0.25 * quad.iter().map(|p| p[0]).sum::<f64>();
|
||||
let cy = 0.25 * quad.iter().map(|p| p[1]).sum::<f64>();
|
||||
add(quad, [cx, cy, zmid], &mut t, &mut probe);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -497,6 +603,14 @@ pub(crate) fn layout(inp: &Input) -> Result<Layout, String> {
|
||||
if probe.len() != t.blocks.len() {
|
||||
return Err("the layout lost track of its blocks".into());
|
||||
}
|
||||
replaced.sort_unstable();
|
||||
replaced.dedup();
|
||||
t.remove(&replaced).map_err(|e| e.to_string())?;
|
||||
let mut k = 0;
|
||||
probe.retain(|_| {
|
||||
k += 1;
|
||||
replaced.binary_search(&(k - 1)).is_err()
|
||||
});
|
||||
t.merge(eps);
|
||||
|
||||
// The blend layer: a block on every boundary face of the grid on a side with a layer.
|
||||
@@ -626,6 +740,31 @@ fn corner_ring(groups: &[Group], pts: &[V3], lo: V3, hi: V3, eps: f64) -> Option
|
||||
})
|
||||
}
|
||||
|
||||
/// The patch ring that takes in the concave blend with points `pts` (local frame): a blend
|
||||
/// inside the square round a boss's whole circle and not inside the circle (the inner fillet
|
||||
/// where the walls meet the boss, the boss touching it at most). Returns the group and, per
|
||||
/// axis, the blend's extent.
|
||||
fn patch_blend(groups: &[Group], pts: &[V3]) -> Option<(usize, [[f64; 2]; 2])> {
|
||||
let (mut bl, mut bh) = ([f64::INFINITY; 2], [f64::NEG_INFINITY; 2]);
|
||||
for p in pts {
|
||||
for a in 0..2 {
|
||||
bl[a] = bl[a].min(p[a]);
|
||||
bh[a] = bh[a].max(p[a]);
|
||||
}
|
||||
}
|
||||
groups.iter().enumerate().find_map(|(i, g)| {
|
||||
let r0 = g.circles.first()?.r;
|
||||
let tol = 1e-3 * r0;
|
||||
if !g.patch || pts.iter().any(|p| (p[0] - g.c[0]).hypot(p[1] - g.c[1]) < 0.9 * r0) {
|
||||
return None;
|
||||
}
|
||||
if (0..2).any(|a| bl[a] < g.c[a] - r0 - tol || bh[a] > g.c[a] + r0 + tol) {
|
||||
return None;
|
||||
}
|
||||
Some((i, [[bl[0], bh[0]], [bl[1], bh[1]]]))
|
||||
})
|
||||
}
|
||||
|
||||
/// The half-sides of an O-grid ring's frame, per local axis and side: the hole centred in it.
|
||||
/// A side of the body within 3 radii (or a corner blend the ring takes in) is the frame on that
|
||||
/// side, and the frame's other side is as far from the centre unless the body's side is nearer
|
||||
|
||||
@@ -35,8 +35,9 @@ not guessed. Read it before you plan anything:
|
||||
body did not fit (a plane at an angle, a curved face that is not a ring or a blend, two
|
||||
circles in one frame), and then the frame cuts and swept cells are used instead (`pending`
|
||||
lists the rings left boxed). Check `all_hex`, `conforming`, `min_scaled_jacobian`.
|
||||
Known gap: a shelled box whose fillet radius exceeds the wall (the enclosure-base pattern)
|
||||
can still leave a twisted element at the outer fillet's bottom corner.
|
||||
A boss the walls run into (an enclosure's corner screw boss) is ringed over the square round
|
||||
its whole circle, the ring split where the walls join it; the inner fillet where the walls
|
||||
meet is taken in by the ring's corner, even where the boss just touches it.
|
||||
4. Then verify as in §4. When the plan does not fit (unmapped primary features, a failed cell),
|
||||
fall back to the manual process below, keeping the plan's cuts that work.
|
||||
|
||||
|
||||
Reference in new issue
Block a user