A hole positioned on a face now follows that face along the hole's own axis instead of sliding to the face's nearest point. When the face no longer reaches the position, the hole stays where it was placed and the feature says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
90 lines
4.4 KiB
Rust
90 lines
4.4 KiB
Rust
//! A hole placed on a face follows that face along the hole's axis, never sideways: when the face
|
|
//! moves the hole moves with it, and when the face no longer reaches the place the hole stays
|
|
//! where it was put and says so instead of sliding to the face's nearest point.
|
|
|
|
use serde_json::{Value, json};
|
|
use solidcraft_geom::Vec3;
|
|
|
|
use crate::Session;
|
|
|
|
fn run(s: &mut Session, id: &str, p: Value) -> Value {
|
|
match s.execute(id, &p) {
|
|
Ok(v) => v,
|
|
Err(e) => panic!("{id} {p}: {e}"),
|
|
}
|
|
}
|
|
|
|
fn change(s: &mut Session, name: &str, expression: &str) {
|
|
run(s, "parameters.change", json!({"name": name, "expression": expression}));
|
|
}
|
|
|
|
fn volume(s: &mut Session) -> f64 {
|
|
run(s, "inspect.measure", json!({}))["total"]["volume_mm3"].as_f64().unwrap()
|
|
}
|
|
|
|
/// Whether the model holds material at (x, y, z).
|
|
fn solid_at(s: &Session, x: f64, y: f64, z: f64) -> bool {
|
|
s.model.state().bodies.iter().any(|b| b.body.contains(Vec3::new(x, y, z)))
|
|
}
|
|
|
|
/// What the hole feature says (its error and its warning); empty when it is silent.
|
|
fn said(s: &Session, feature: &str) -> String {
|
|
let id = s.doc.find_feature(feature).unwrap().id;
|
|
let r = s.model.result(id).unwrap();
|
|
format!("{}{}", r.error.clone().unwrap_or_default(), r.warning.clone().unwrap_or_default())
|
|
}
|
|
|
|
#[test]
|
|
fn a_hole_the_face_no_longer_reaches_stays_where_it_was_placed_and_says_so() {
|
|
let mut s = Session::default();
|
|
run(&mut s, "parameters.add", json!({"name": "length", "expression": "100 mm"}));
|
|
run(&mut s, "solid.box", json!({"length": "length", "width": 50, "height": 20, "name": "Plate"}));
|
|
run(&mut s, "solid.hole", json!({"position": [80, 25, 20], "diameter": "6 mm", "name": "Hole"}));
|
|
// The bore is at x = 80, 3 mm either way.
|
|
let drilled_at_80 = |s: &Session| !solid_at(s, 80.0, 25.0, 10.0) && solid_at(s, 76.0, 25.0, 10.0) && solid_at(s, 84.0, 25.0, 10.0);
|
|
assert!(drilled_at_80(&s));
|
|
// Longer: the place is still on the top face, and the hole stays at x = 80.
|
|
change(&mut s, "length", "150 mm");
|
|
assert_eq!(said(&s, "Hole"), "");
|
|
assert!(drilled_at_80(&s));
|
|
// Shorter than the place: nothing is drilled at the new end, and the hole says why. A hole
|
|
// moved to the face's nearest point would cut half a hole there (59 717 mm³ left).
|
|
change(&mut s, "length", "60 mm");
|
|
let left = volume(&mut s);
|
|
assert!((left - 60_000.0).abs() < 1.0, "half a hole was cut at the plate's end: {left} mm³ of 60000");
|
|
let msg = said(&s, "Hole");
|
|
assert!(msg.contains("no longer on face") && msg.contains("[80, 25, 20]"), "{msg}");
|
|
// Long again: the hole is back where it was put, with no message.
|
|
change(&mut s, "length", "100 mm");
|
|
assert_eq!(said(&s, "Hole"), "");
|
|
assert!(drilled_at_80(&s));
|
|
}
|
|
|
|
#[test]
|
|
fn a_slanted_hole_stays_on_its_axis_when_its_face_moves() {
|
|
let mut s = Session::default();
|
|
run(&mut s, "parameters.add", json!({"name": "height", "expression": "20 mm"}));
|
|
run(&mut s, "solid.box", json!({"length": 60, "width": 30, "height": "height", "name": "Block"}));
|
|
// Entering the top face at x = 20, running on in x as it goes down: 1 across for every 2 down.
|
|
run(&mut s, "solid.hole", json!({"position": [20, 15, 20], "direction": [1, 0, -2], "diameter": "2 mm", "name": "Hole"}));
|
|
// The axis is the line through the place along the direction, whatever the height of the block:
|
|
// x = 20 + (20 - z) / 2. At mid-height the bore is empty on it and solid 3 mm to either side
|
|
// (the bore is about 2.2 mm wide there).
|
|
let on_axis = |s: &Session, h: f64| {
|
|
let (x, z) = (20.0 + (20.0 - h / 2.0) / 2.0, h / 2.0);
|
|
assert!(!solid_at(s, x, 15.0, z), "block height {h}: the bore should pass through x = {x} at z = {z}");
|
|
assert!(solid_at(s, x - 3.0, 15.0, z) && solid_at(s, x + 3.0, 15.0, z), "block height {h}: material 3 mm beside the axis");
|
|
// (The kernel may note a boolean retry for a slanted bore; the hole's place is on its face.)
|
|
assert!(!said(s, "Hole").contains("no longer on face"), "block height {h}: {}", said(s, "Hole"));
|
|
};
|
|
on_axis(&s, 20.0);
|
|
// Taller: the top face moves up. Moved to the face's nearest point the hole would slide
|
|
// sideways onto another axis (5 mm further along x at mid-height); along its axis it enters
|
|
// higher up the same line.
|
|
change(&mut s, "height", "30 mm");
|
|
on_axis(&s, 30.0);
|
|
// Lower: the same line enters further along.
|
|
change(&mut s, "height", "12 mm");
|
|
on_axis(&s, 12.0);
|
|
}
|