* Smart Extrude: direction from the picked side, Cut into a body stays a Cut (#1257) - A sketch profile (or a thin extrude's curve) extrudes toward the side it was picked from: from the bottom view it goes down. Body faces still go outward. - The automatic operation is latched per side of the profile: a drag that starts into a body is a Cut and stays one past the far side; a Join does not become a Cut when it reaches another body. Passing through zero decides afresh for the new side. - An explicit operation, Direction or Reverse ends the automatic choices. - auto_operation also looks just past the profile, so a long cut through a body reads as a Cut from the start (the engine's operation: "auto" too). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Scenario s07: the XZ profile seen from behind now extrudes toward the camera Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
126 lines
5.3 KiB
Rust
126 lines
5.3 KiB
Rust
//! Smart Extrude in the real window (#1257): the direction follows the side a profile is picked
|
|
//! from, a drag into a body cuts and stays a Cut past the far side, and explicit choices win.
|
|
use crate::dialogs::{Dialog, Kind};
|
|
use crate::scenario::Harness;
|
|
use serde_json::json;
|
|
|
|
const JOIN: usize = 1;
|
|
const CUT: usize = 2;
|
|
|
|
/// The extrude window's direction, operation and whether the operation is still automatic.
|
|
fn state(h: &Harness) -> (usize, usize, bool) {
|
|
let Some(Dialog { kind: Kind::Extrude { direction, operation, auto_op, .. }, .. }) = &h.app.dialog else {
|
|
panic!("extrude dialog: {:?}", h.app.dialog)
|
|
};
|
|
(*direction, *operation, *auto_op)
|
|
}
|
|
|
|
fn bounds_z(h: &Harness) -> (f64, f64) {
|
|
let st = h.app.session.world_state();
|
|
let b = st.bodies.last().expect("a body").mesh().bounds();
|
|
(b.min.z, b.max.z)
|
|
}
|
|
|
|
fn volume(h: &Harness) -> f64 {
|
|
let st = h.app.session.world_state();
|
|
st.bodies.iter().map(|b| solidcraft_engine::doc::kernel::measure(&b.body).unwrap().volume).sum()
|
|
}
|
|
|
|
/// A sketch on XY extruded from the top view goes up, and from the bottom view goes down.
|
|
#[test]
|
|
fn a_profile_picked_from_below_extrudes_down() {
|
|
for (view, dir, z) in [("top", 0, (0.0, 10.0)), ("bottom", 1, (-10.0, 0.0))] {
|
|
let mut h = Harness::new();
|
|
h.app.run("sketch.create", json!({"plane":"XY"})).unwrap();
|
|
h.app.run("sketch.rectangle.two_point", json!({"p0":[0,0],"p1":[20,20]})).unwrap();
|
|
h.app.run("sketch.finish", json!({})).unwrap();
|
|
h.app.run("select.clear", json!({})).unwrap();
|
|
h.step(&json!({"call":"ui.view","params":{"view":view}})).unwrap();
|
|
h.settle();
|
|
h.step(&json!({"start":"solid.extrude"})).unwrap();
|
|
// Pick the profile in the viewport, as a hand does.
|
|
h.step(&json!({"click":{"world":[10,10,0]}})).unwrap();
|
|
h.frames(2);
|
|
assert_eq!(h.app.dialog.as_ref().unwrap().inputs[0].items.len(), 1, "{view}: the profile is picked");
|
|
assert_eq!(state(&h).0, dir, "{view}: direction");
|
|
h.step(&json!({"key":"Enter"})).unwrap();
|
|
assert!(h.app.dialog.is_none(), "{view}: OK");
|
|
let (lo, hi) = bounds_z(&h);
|
|
assert!((lo - z.0).abs() < 1e-6 && (hi - z.1).abs() < 1e-6, "{view}: the body spans z {lo}..{hi}");
|
|
}
|
|
}
|
|
|
|
/// A 20 x 20 x 10 box and an extrude from its top: the top face itself (Press Pull), or a
|
|
/// 10 x 10 square sketched on it.
|
|
fn box_with_top_extrude(face: bool) -> Harness {
|
|
let mut h = Harness::new();
|
|
h.app.run("solid.box", json!({"length":20,"width":20,"height":10,"corner":[0,0,0]})).unwrap();
|
|
if !face {
|
|
h.app.run("sketch.create", json!({"plane":{"origin":[0,0,10],"x_dir":[1,0,0],"y_dir":[0,1,0]}})).unwrap();
|
|
h.app.run("sketch.rectangle.two_point", json!({"p0":[5,5],"p1":[15,15]})).unwrap();
|
|
h.app.run("sketch.finish", json!({})).unwrap();
|
|
h.app.run("select.clear", json!({})).unwrap();
|
|
}
|
|
h.step(&json!({"call":"ui.view","params":{"view":"home"}})).unwrap();
|
|
h.step(&json!({"call":"ui.view","params":{"view":"fit"}})).unwrap();
|
|
h.settle();
|
|
if face {
|
|
h.step(&json!({"start":"solid.press_pull"})).unwrap();
|
|
h.step(&json!({"click":{"world":[10,10,10]}})).unwrap();
|
|
} else {
|
|
// The new sketch's profile is taken at once.
|
|
h.step(&json!({"start":"solid.extrude"})).unwrap();
|
|
}
|
|
h.frames(2);
|
|
assert_eq!(h.app.dialog.as_ref().unwrap().inputs[0].items.len(), 1, "the top is picked");
|
|
h
|
|
}
|
|
|
|
fn drag_to(h: &mut Harness, z: f64) {
|
|
h.step(&json!({"drag":[{"handle":"arrow"},{"world":[10,10,z]}]})).unwrap();
|
|
h.frames(2);
|
|
}
|
|
|
|
/// Pushed into the box the top cuts; dragged on far past the bottom it stays a Cut (a cut
|
|
/// through), for a face (Press Pull) and a profile on it alike.
|
|
#[test]
|
|
fn a_drag_past_the_far_side_stays_a_cut() {
|
|
for face in [true, false] {
|
|
let mut h = box_with_top_extrude(face);
|
|
assert_eq!(state(&h), (0, JOIN, true), "face {face}: outward from the top: Join");
|
|
drag_to(&mut h, 5.0);
|
|
assert_eq!(state(&h), (0, CUT, true), "face {face}: into the box: Cut");
|
|
drag_to(&mut h, -40.0);
|
|
assert_eq!(state(&h), (0, CUT, true), "face {face}: far past the bottom: still Cut");
|
|
if face {
|
|
continue;
|
|
}
|
|
h.step(&json!({"key":"Enter"})).unwrap();
|
|
assert!(h.app.dialog.is_none());
|
|
assert!((volume(&h) - (4000.0 - 1000.0)).abs() < 1e-6, "a square hole right through: {}", volume(&h));
|
|
}
|
|
}
|
|
|
|
/// The reverse of a drag: out the other side of zero the choice is made afresh.
|
|
#[test]
|
|
fn reversing_past_the_start_joins_again() {
|
|
let mut h = box_with_top_extrude(true);
|
|
drag_to(&mut h, 4.0);
|
|
assert_eq!(state(&h).1, CUT);
|
|
drag_to(&mut h, 16.0);
|
|
assert_eq!(state(&h), (0, JOIN, true), "pulled out: Join");
|
|
h.step(&json!({"key":"Enter"})).unwrap();
|
|
assert!((volume(&h) - 20.0 * 20.0 * 16.0).abs() < 1e-6, "{}", volume(&h));
|
|
}
|
|
|
|
/// Reverse is an explicit choice: the operation stops following the geometry.
|
|
#[test]
|
|
fn an_explicit_reverse_keeps_the_operation() {
|
|
let mut h = box_with_top_extrude(true);
|
|
h.step(&json!({"widget":"Reverse Direction"})).unwrap();
|
|
h.frames(2);
|
|
assert_eq!(state(&h), (1, JOIN, false), "reversed into the box, the operation stays as it was");
|
|
drag_to(&mut h, 2.0);
|
|
assert_eq!(state(&h), (1, JOIN, false));
|
|
}
|