Files
OneCAD/crates/ui-egui/src/blend_arrow_tests.rs
T
bherbruckandClaude Opus 5.5 6eaf7458ab Fillet/chamfer: point the drag arrow into the corner on inside edges (#1394)
* Fillet/chamfer: point the drag arrow into the corner on inside edges (#1384)

The radius/distance arrow always pointed against the faces' outward
bisector, which is into the body on an outside edge but away from the
corner on an inside one, where the blend adds material. The arrow and
the spline radius handles now take each edge's convexity into account,
so dragging out along the arrow grows the value on both kinds of edge,
and each edge of a mixed chain follows its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tests: no unreachable! in the blend arrow helper

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 08:58:51 -04:00

104 lines
4.9 KiB
Rust

//! Fillet and chamfer drag arrows on outside and inside edges (#1384): the arrow points where
//! the value grows (into the body on an outside edge, into the open corner on an inside one),
//! and dragging out along it increases the value.
use crate::canvas::{anchor, blend_grow_dir};
use crate::dialogs::Kind;
use crate::scenario::Harness;
use serde_json::json;
use solidcraft_engine::Sel;
use solidcraft_engine::geom::Vec3;
/// A 40 x 30 x 20 block with a 20 x 30 x 10 step on its left half: the edge along y at x 20,
/// z 20 is inside (concave); the block's right top edge (x 40, z 20) and the step's top right
/// edge (x 20, z 30) are outside (convex).
fn l_step() -> Harness {
let mut h = Harness::new();
h.app.run("solid.box", json!({"length": 40, "width": 30, "height": 20})).unwrap();
h.app.run("solid.box", json!({"length": 20, "width": 30, "height": 10, "corner": [0, 0, 20], "operation": "join"})).unwrap();
h.frames(2);
h
}
fn edge(h: &Harness, p: Vec3) -> Sel {
let st = h.app.session.world_state();
let m = st.body("Body1").unwrap().mesh();
let i = m.edges.iter().position(|e| e.windows(2).any(|w| p.dist_to_segment(w[0], w[1]) < 1e-9)).expect("an edge there");
Sel::Edge { body: "Body1".into(), index: i, point: p }
}
fn grow(h: &Harness, s: &Sel) -> Vec3 {
let Sel::Edge { body, index, point } = s else { panic!("an edge") };
blend_grow_dir(&h.app.session.world_state().body(body).unwrap().mesh(), *index, *point).unwrap()
}
fn close(a: Vec3, b: Vec3) -> bool {
a.dist(b) < 1e-6
}
const INSIDE: Vec3 = Vec3::new(20.0, 15.0, 20.0);
const OUTSIDE: Vec3 = Vec3::new(40.0, 15.0, 20.0);
const STEP_TOP: Vec3 = Vec3::new(20.0, 15.0, 30.0);
/// The direction function on its own: the corner's open side on the inside edge, into the
/// material on the outside ones, each edge of a mixed chain on its own.
#[test]
fn grow_direction_follows_each_edges_convexity() {
let h = l_step();
let s = std::f64::consts::FRAC_1_SQRT_2;
let open = Vec3::new(s, 0.0, s);
assert!(close(grow(&h, &edge(&h, INSIDE)), open), "inside: {:?}", grow(&h, &edge(&h, INSIDE)));
assert!(close(grow(&h, &edge(&h, OUTSIDE)), Vec3::new(-s, 0.0, -s)), "outside: {:?}", grow(&h, &edge(&h, OUTSIDE)));
// Same face normals as the inside edge, but outside: the opposite way.
assert!(close(grow(&h, &edge(&h, STEP_TOP)), -open), "step top: {:?}", grow(&h, &edge(&h, STEP_TOP)));
}
/// A plain box's outside edge keeps its arrow into the body.
#[test]
fn box_edge_arrow_still_points_into_the_body() {
let mut h = Harness::new();
h.app.run("solid.box", json!({"length": 40, "width": 30, "height": 20})).unwrap();
let e = edge(&h, Vec3::new(20.0, 0.0, 20.0));
for id in ["solid.fillet", "solid.chamfer"] {
h.app.session.selection = vec![e.clone()];
h.app.start(id);
let dir = anchor(&h.app, h.app.dialog.as_ref().unwrap()).unwrap().1.unwrap();
assert!(dir.y > 0.5 && dir.z < -0.5, "{id}: {dir:?}");
h.app.dialog = None;
}
}
/// The value of the open fillet or chamfer window.
fn value(h: &Harness) -> f64 {
let Some(Kind::Fillet { radius, .. }) = h.app.dialog.as_ref().map(|d| &d.kind) else { panic!("a fillet window") };
h.app.session.doc.eval(radius, solidcraft_engine::doc::expr::Kind::Length).unwrap()
}
/// On an inside edge (and on an outside one, a mixed chain's first edge either way), the arrow
/// points along the edge's own grow direction and dragging outward along it increases the value.
#[test]
fn dragging_out_along_the_arrow_grows_fillet_and_chamfer_on_both_kinds_of_edge() {
for first in [INSIDE, OUTSIDE] {
for id in ["solid.fillet", "solid.chamfer"] {
let mut h = l_step();
h.step(&json!({"call": "ui.view", "params": {"view": "iso"}})).unwrap();
h.step(&json!({"call": "ui.view", "params": {"view": "fit"}})).unwrap();
let chain = if first == INSIDE { vec![edge(&h, INSIDE), edge(&h, OUTSIDE)] } else { vec![edge(&h, OUTSIDE), edge(&h, INSIDE)] };
h.app.session.selection = chain;
h.app.start(id);
h.frames(3);
let shown = h.app.dialog.as_ref().unwrap().inputs[0].items[0].clone();
assert_eq!(h.app.dialog.as_ref().unwrap().inputs[0].items.len(), 2);
let want = grow(&h, &shown);
assert!(close(want, grow(&h, &edge(&h, first))) || matches!(&shown, Sel::Edge { point, .. } if *point != first));
let (at, dir) = anchor(&h.app, h.app.dialog.as_ref().unwrap()).unwrap();
assert!(close(dir.unwrap(), want), "{id} at {first:?}: {dir:?} vs {want:?}");
let before = value(&h);
let to = at + want * 6.0;
h.step(&json!({"drag": [{"handle": "arrow"}, {"world": [to.x, to.y, to.z]}], "ctrl": true})).unwrap();
let after = value(&h);
assert!(after > before && (after - 6.0).abs() < 0.3, "{id} at {first:?}: {before} -> {after}");
}
}
}