diff --git a/crates/engine/src/naming_history_tests.rs b/crates/engine/src/naming_history_tests.rs index 97560d42f7..e786049147 100644 --- a/crates/engine/src/naming_history_tests.rs +++ b/crates/engine/src/naming_history_tests.rs @@ -691,3 +691,25 @@ fn a_shell_point_pick_moved_off_its_face_reopens_only_that_face() { assert!(solid_at(&s, [100.0, 16.0, 10.0]), "the bottom face stays closed"); assert!(!solid_at(&s, [149.0, 25.0, 10.0]), "the end at x = 150 is open"); } + +/// A two-distance chamfer of a plate's bottom front edge running 220 up the front face, past the +/// 85 mm where the plate's sides start to taper (rig fillets/5181): the chamfer takes up the front +/// face's straight side edges too, and lies between their faces, but it is made from the picked +/// edge alone. Its one face has the picked edge's name everywhere on it (it was named after the +/// left side's edge, l6, the first edge it took up). +#[test] +fn a_chamfer_running_past_the_side_edges_it_takes_up_is_named_by_its_edge() { + let mut s = Session::default(); + run(&mut s, "sketch.create", json!({"plane": {"origin": [0, 0, 0], "x_dir": [1, 0, 0], "y_dir": [0, 0, 1]}, "name": "Bracket"})); + let taper = 30.675635228421154; + run(&mut s, "sketch.line", json!({"points": [[-50, 0], [50, 0], [50, 85], [taper, 222.5], [-taper, 222.5], [-50, 85]], "closed": true})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.extrude", json!({"sketch": "Bracket", "distance": 23, "body_name": "Bracket"})); + run(&mut s, "solid.chamfer", json!({"edges": [[0, 0, 0]], "distance": 220, "distance2": 11})); + let blends: Vec = names(&s, "Bracket").into_iter().map(|(n, _)| n).filter(|n| n.starts_with("F3:blend:")).collect(); + assert_eq!(blends, vec!["F3:blend:F2:side:l1|F2:start".to_string()]); + // On the chamfer's plane, through (x, -11, 0) and (x, 0, 220): below and above z = 85. + let on = |z: f64| Vec3::new(0.0, -11.0 * (1.0 - z / 220.0), z); + let at = solidcraft_doc::naming::face_names_at(&s.model.state(), &[on(40.0), on(150.0)]); + assert_eq!(at, vec![blends[0].clone(), blends[0].clone()]); +} diff --git a/crates/kernel/src/blend_smlib.rs b/crates/kernel/src/blend_smlib.rs index 1e6e7e10f7..0f197e43ca 100644 --- a/crates/kernel/src/blend_smlib.rs +++ b/crates/kernel/src/blend_smlib.rs @@ -362,8 +362,8 @@ mod imp { (Some(o), Some(ro)) => match r.history().map_err(|e| failed(what, "history", e))?.history(&[o], ro) { Ok(mut h) => { let operand = History::identity(body).edges; - let replaced = blended(body, edges, &h); - solidcraft_smlib::history::blends(&mut h, &operand, &replaced); + let taken_up = taken_up(body, edges, &h); + solidcraft_smlib::history::blends(&mut h, &operand, edges, &taken_up); Some(with_corners(h)) } Err(e) => { @@ -414,10 +414,10 @@ mod imp { h } - /// The operand's edges the blend replaced: each picked edge, and every edge no result edge - /// comes from (one the blend's corners took up), in the operand's edge order. - fn blended(body: &Body, picked: &[usize], h: &History) -> Vec { - let mut out: BTreeSet = picked.iter().copied().collect(); + /// The operand's edges the blend took up besides the picked ones: every edge no result edge + /// comes from (one the blend's corners took up, or one it ran over), in the operand's edge + /// order. + fn taken_up(body: &Body, picked: &[usize], h: &History) -> Vec { let kept: BTreeSet = h .edges .iter() @@ -427,8 +427,7 @@ mod imp { _ => None, }) .collect(); - out.extend((0..body.edge_count()).filter(|e| !kept.contains(e))); - out.into_iter().collect() + (0..body.edge_count()).filter(|e| !kept.contains(e) && !picked.contains(e)).collect() } } diff --git a/crates/smlib/src/history.rs b/crates/smlib/src/history.rs index 5ad04c3a9b..b88b54d128 100644 --- a/crates/smlib/src/history.rs +++ b/crates/smlib/src/history.rs @@ -202,13 +202,17 @@ impl Raw { } } -/// A blend's faces (fillet, chamfer) in `h`, the history of a blend of edges `blended` of its +/// A blend's faces (fillet, chamfer) in `h`, the history of a blend of edges `picked` of its /// one operand (operand 0), whose own edges `operand` lists (the faces each bounds, as in the -/// operand's identity history): SMLib makes a blend face with no tag, so each Generated face is -/// given [`FaceOrigin::Edge`] for every blended edge whose two faces both lie under faces next -/// to it and no longer meet each other directly. A corner patch between several blends matches -/// none of them and stays without origin. -pub fn blends(h: &mut History, operand: &[EdgeProv], blended: &[usize]) { +/// operand's identity history), which also took up edges `taken_up` (no result edge comes from +/// them: a corner's short edge, or an edge the blend ran over): SMLib makes a blend face with no +/// tag, so each Generated face is given [`FaceOrigin::Edge`] for every picked edge whose two +/// faces both lie under faces next to it and no longer meet each other directly, and only where +/// no picked edge is such, every taken-up edge that is. A chamfer running up past the end of the +/// face beside it takes up that face's side edge and lies between its faces too: it is made from +/// the picked edge alone. A corner patch between several blends matches none of them and stays +/// without origin. +pub fn blends(h: &mut History, operand: &[EdgeProv], picked: &[usize], taken_up: &[usize]) { let mut next_to: Vec> = vec![BTreeSet::new(); h.faces.len()]; for e in &h.edges { for f in &e.faces { @@ -238,14 +242,18 @@ pub fn blends(h: &mut History, operand: &[EdgeProv], blended: &[usize]) { if f.change != Change::Generated || !f.origins.is_empty() { continue; } - let edges = blended.iter().filter(|e| { + let under = |e: &&usize| { operand.get(**e).is_some_and(|p| { let [a, b] = p.faces.as_slice() else { return false }; let (a, b) = (FaceOrigin::Face { operand: 0, face: *a }, FaceOrigin::Face { operand: 0, face: *b }); around.contains(&a) && around.contains(&b) && !meeting.contains(&(a.min(b), a.max(b))) }) - }); - f.origins = edges.map(|e| FaceOrigin::Edge { operand: 0, edge: *e }).collect(); + }; + let mut edges: Vec = picked.iter().filter(under).copied().collect(); + if edges.is_empty() { + edges = taken_up.iter().filter(under).copied().collect(); + } + f.origins = edges.into_iter().map(|e| FaceOrigin::Edge { operand: 0, edge: e }).collect(); } } diff --git a/crates/smlib/src/history/tests.rs b/crates/smlib/src/history/tests.rs index 06ecf734bf..6fdc4b9944 100644 --- a/crates/smlib/src/history/tests.rs +++ b/crates/smlib/src/history/tests.rs @@ -96,7 +96,7 @@ fn a_fillet_face_comes_from_its_edge() { let mut h = History { operands: 1, faces, edges: vec![edge(4, 6), edge(5, 6), edge(0, 6), edge(2, 6), edge(0, 4)] }; // The operand's own edges: edge 0 between faces 4 and 5, edge 1 between 0 and 4. let operand = vec![edge(4, 5), edge(0, 4)]; - blends(&mut h, &operand, &[0, 1]); + blends(&mut h, &operand, &[0], &[1]); assert_eq!(h.faces[6].origins, vec![FaceOrigin::Edge { operand: 0, edge: 0 }]); assert!(h.faces[..6].iter().all(|f| f.origins.len() == 1)); } diff --git a/tests/kernel-cases/fillets/5181-two-distance-chamfer-runs-past-tapered-sides/expected.json b/tests/kernel-cases/fillets/5181-two-distance-chamfer-runs-past-tapered-sides/expected.json index dde396198d..0550322981 100644 --- a/tests/kernel-cases/fillets/5181-two-distance-chamfer-runs-past-tapered-sides/expected.json +++ b/tests/kernel-cases/fillets/5181-two-distance-chamfer-runs-past-tapered-sides/expected.json @@ -2,7 +2,7 @@ "sources": [ "tangency-labs/solidcraft#5181" ], - "what": "a 220 x 11 two-distance chamfer along the bottom edge of a 23 mm plate 100 wide whose sides run straight for 85 mm and then taper to 61.4 at 222.5: refused ('runs past the faces beside the edge into other geometry; the largest size that fits is 85 mm')", + "what": "a 220 x 11 two-distance chamfer along the bottom edge of a 23 mm plate 100 wide whose sides run straight for 85 mm and then taper to 61.4 at 222.5: it builds, the chamfer plane running on past the straight sides into the tapered ones, which it trims", "source": "analytic", "method": "the plate (profile 100x85 + the trapezoid to 61.351 at 222.5) x 23. The first face is the big face (it faces up most: horizontal, the bottom faces down): 220 up it, 11 into the bottom face. The chamfer plane cuts the wedge 11 (1 - z/220) deep across the full width w(z) (100, then tapering past z = 85): Int_0^220 w(z) 11 (1 - z/220) dz, the tapered sides simply trimmed", "body_count": 1,