* 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 <[email protected]> * Scenario s07: the XZ profile seen from behind now extrudes toward the camera Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
d73cd8a177
commit
ed22d663e4
8 files changed
+363
-11
No files matched your search
@@ -137,9 +137,16 @@ fn extrude_base(s: &Session, p: &Value) -> Option<(Vec3, Vec3)> {
|
||||
Some((ss.plane.to_world(q), ss.plane.normal()))
|
||||
}
|
||||
|
||||
/// How far past the profile [`auto_operation`] looks for material it starts in.
|
||||
const START_PROBE_MM: f64 = 0.1;
|
||||
|
||||
/// What `operation: "auto"` means for an extrude with these parameters: `cut` when it goes
|
||||
/// into a body, otherwise `join` when the active component has a solid. Keeping an
|
||||
/// addition separate requires an explicit `new` operation.
|
||||
///
|
||||
/// "Goes into a body" is decided where the extrude starts: just past the profile in the
|
||||
/// direction it grows, or halfway along it. With the halfway point alone, a cut dragged far
|
||||
/// past a body's far side read as a Join (#1257).
|
||||
pub fn auto_operation(s: &Session, p: &Value) -> Option<&'static str> {
|
||||
let d = s.doc.eval(&expr(p, "distance")?, Kind::Length).ok()?;
|
||||
let dir = str_(p, "direction").map(str::to_ascii_lowercase);
|
||||
@@ -161,10 +168,13 @@ pub fn auto_operation(s: &Session, p: &Value) -> Option<&'static str> {
|
||||
return Some("new");
|
||||
}
|
||||
let inside = |q: Vec3| bodies.iter().any(|b| b.mesh().contains(q));
|
||||
// A step into whatever is right in front of the profile, short of the far end.
|
||||
let near = |d: f64| d.signum() * (d.abs() * 0.5).min(START_PROBE_MM);
|
||||
let into = |d: f64| inside(base + n * near(d)) || inside(base + n * (d * 0.5));
|
||||
if symmetric {
|
||||
return Some(if inside(base + n * (d.abs() * 0.5)) || inside(base - n * (d.abs() * 0.5)) { "cut" } else { "join" });
|
||||
return Some(if into(d.abs()) || into(-d.abs()) { "cut" } else { "join" });
|
||||
}
|
||||
if inside(base + n * (d * 0.5)) {
|
||||
if into(d) {
|
||||
Some("cut")
|
||||
} else {
|
||||
// A profile can meet material along its side, or only around part of a
|
||||
|
||||
@@ -1148,6 +1148,9 @@ fn extrude_auto_operation() {
|
||||
assert!(rel(volume(&mut s), 40.0 * 30.0 * 15.0) < 1e-6);
|
||||
// Pulled out, it joins.
|
||||
assert_eq!(auto_operation(&s, &json!({"face": [20, 15, 15], "distance": 5})), Some("join"));
|
||||
// Pushed in far past the bottom (halfway is below the box), it still cuts (#1257).
|
||||
assert_eq!(auto_operation(&s, &json!({"face": [20, 15, 15], "distance": -50})), Some("cut"));
|
||||
assert_eq!(auto_operation(&s, &json!({"face": [20, 15, 15], "distance": 50, "direction": "negative"})), Some("cut"));
|
||||
// A sketch on XY under the box: up goes into it (cut), down grows out of it (join), and a
|
||||
// profile beside the box still defaults to Join (New Body remains explicit).
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY"}));
|
||||
|
||||
@@ -159,7 +159,7 @@ fn poly_mid(poly: &[Vec2]) -> Option<(Vec2, Vec2)> {
|
||||
last
|
||||
}
|
||||
|
||||
fn profile_plane(app: &SolveApp, sketch: u64) -> Option<solidcraft_engine::geom::Plane> {
|
||||
pub(crate) fn profile_plane(app: &SolveApp, sketch: u64) -> Option<solidcraft_engine::geom::Plane> {
|
||||
let component = app.session.doc.feature(sketch)?.component;
|
||||
let state = app.session.model.state();
|
||||
let plane = state.sketch(sketch)?.plane;
|
||||
|
||||
@@ -42,6 +42,8 @@ pub enum Kind {
|
||||
/// Into a body: cut; additions: join when a solid exists, otherwise new.
|
||||
/// until the user picks one.
|
||||
auto_op: bool,
|
||||
/// The direction from the picked side and the latched operation (#1257).
|
||||
smart: crate::smart_extrude::Smart,
|
||||
/// The other side's distance (two sides).
|
||||
distance2: String,
|
||||
/// Taper angle (empty or zero: none).
|
||||
@@ -439,6 +441,7 @@ impl Dialog {
|
||||
direction: 0,
|
||||
operation: usize::from(has_bodies),
|
||||
auto_op: true,
|
||||
smart: Default::default(),
|
||||
distance2: "10 mm".into(),
|
||||
taper: "0 deg".into(),
|
||||
taper_present: false,
|
||||
@@ -1797,23 +1800,56 @@ fn measure_rows(app: &SolveApp, ui: &mut egui::Ui, r: Option<&Value>) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep an automatic extrude operation in step with the geometry.
|
||||
/// Smart Extrude (#1257, see [`crate::smart_extrude`]): a new pick sets the direction from the
|
||||
/// side it was picked from, and the automatic operation follows the geometry, latched per side.
|
||||
fn auto_operation(app: &SolveApp, d: &mut Dialog) {
|
||||
let Kind::Extrude { smart, .. } = &d.kind else { return };
|
||||
let picked = d.inputs.first().map(|i| i.items.as_slice()).unwrap_or_default();
|
||||
if smart.picked != picked {
|
||||
let new_first = smart.picked.first() != picked.first();
|
||||
let picked = picked.to_vec();
|
||||
let side_of_pick = if new_first && !smart.manual_dir { picked.first().and_then(|first| picked_side(app, d, first)) } else { None };
|
||||
let Kind::Extrude { direction, smart, .. } = &mut d.kind else { return };
|
||||
smart.picked = picked;
|
||||
smart.latch = None;
|
||||
if *direction < 2
|
||||
&& let Some(dir) = side_of_pick
|
||||
{
|
||||
*direction = dir;
|
||||
}
|
||||
}
|
||||
let Kind::Extrude { direction, distance, .. } = &d.kind else { return };
|
||||
let Ok(dist) = app.session.doc.eval(distance, solidcraft_engine::doc::expr::Kind::Length) else { return };
|
||||
let side = crate::smart_extrude::side(dist, *direction);
|
||||
if !matches!(d.kind, Kind::Extrude { auto_op: true, .. }) {
|
||||
return;
|
||||
}
|
||||
let Some(op) =
|
||||
dialog_commands(app, d).ok().and_then(|c| c.into_iter().next()).and_then(|(_, p)| solidcraft_engine::auto_operation(&app.session, &p))
|
||||
let Some(fresh) = dialog_commands(app, d)
|
||||
.ok()
|
||||
.and_then(|c| c.into_iter().next())
|
||||
.and_then(|(_, p)| solidcraft_engine::auto_operation(&app.session, &p))
|
||||
.and_then(|op| OPS.iter().position(|o| *o == op))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if let Kind::Extrude { operation, .. } = &mut d.kind
|
||||
&& let Some(i) = OPS.iter().position(|o| *o == op)
|
||||
{
|
||||
*operation = i;
|
||||
if let Kind::Extrude { operation, smart, .. } = &mut d.kind {
|
||||
*operation = crate::smart_extrude::operation(fresh, *operation, side, &mut smart.latch);
|
||||
}
|
||||
}
|
||||
|
||||
/// The one-side direction a new extrude takes for its first pick: toward the side of a sketch
|
||||
/// profile (or curve) the camera looks from; None for picks that keep their own way.
|
||||
fn picked_side(app: &SolveApp, d: &Dialog, first: &Sel) -> Option<usize> {
|
||||
let normal = match first {
|
||||
Sel::Profile { sketch, .. } => crate::canvas::profile_plane(app, *sketch)?.normal(),
|
||||
Sel::SketchCurve { id } => app.session.world_state().sketches.iter().rev().find(|ss| ss.sketch.curve_index(id).is_some())?.plane.normal(),
|
||||
Sel::Face { .. } => return Some(0),
|
||||
_ => return None,
|
||||
};
|
||||
let (at, _) = crate::canvas::anchor(app, d)?;
|
||||
Some(crate::smart_extrude::picked_direction(crate::smart_extrude::view_ray(&app.cam, at), normal, false))
|
||||
}
|
||||
|
||||
/// Show the active dialog (if any), docked flush to an edge of the viewport just below the axis
|
||||
/// view, sized to its content. The right edge is its place, except while a sketch is open: the
|
||||
/// Sketch Palette keeps the right there, and the dialog takes the left so both stay on screen.
|
||||
@@ -1995,6 +2031,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
direction,
|
||||
operation,
|
||||
auto_op,
|
||||
smart,
|
||||
distance2,
|
||||
taper,
|
||||
taper_present,
|
||||
@@ -2050,6 +2087,9 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
combo(ui, "ex_dir", &["One Side", "Two Sides", "Symmetric"], &mut pos);
|
||||
if pos != order.iter().position(|o| o == direction).unwrap_or(0) {
|
||||
*direction = order.get(pos).copied().unwrap_or(0);
|
||||
// An explicit direction ends the automatic choices (#1257).
|
||||
smart.manual_dir = true;
|
||||
*auto_op = false;
|
||||
}
|
||||
ui.end_row();
|
||||
if *direction < 2 {
|
||||
@@ -2057,6 +2097,9 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
let mut reverse = *direction == 1;
|
||||
if ui.checkbox(&mut reverse, tr("Reverse Direction")).changed() {
|
||||
*direction = usize::from(reverse);
|
||||
// An explicit direction ends the automatic choices (#1257).
|
||||
smart.manual_dir = true;
|
||||
*auto_op = false;
|
||||
}
|
||||
ui.end_row();
|
||||
}
|
||||
@@ -4139,6 +4182,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
|
||||
},
|
||||
operation: op_index(operation),
|
||||
auto_op: false,
|
||||
smart: crate::smart_extrude::Smart { manual_dir: true, ..Default::default() },
|
||||
distance2: extent.distance2.clone().unwrap_or_else(|| "10 mm".into()),
|
||||
taper: extent.taper.clone().unwrap_or_else(|| "0 deg".into()),
|
||||
taper_present: extent.taper.is_some(),
|
||||
|
||||
@@ -136,6 +136,9 @@ pub(crate) mod sketch_text;
|
||||
mod sketch_text_handles;
|
||||
mod sketch_text_move;
|
||||
pub mod sketch_tools;
|
||||
pub mod smart_extrude;
|
||||
#[cfg(test)]
|
||||
mod smart_extrude_ui_tests;
|
||||
pub mod stand_up;
|
||||
pub mod theme;
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
//! Smart Extrude (#1257): which way a new extrude goes and what it does to the bodies, decided
|
||||
//! from where the profile was picked and where the arrow is dragged, and kept steady while the
|
||||
//! drag goes on.
|
||||
//!
|
||||
//! - Direction: a sketch profile (or a thin extrude's curve) extrudes toward the side it was
|
||||
//! picked from, so a profile picked from the bottom view goes down. A body's planar face
|
||||
//! always goes outward. Once the user sets the direction (Reverse, Direction), picks no
|
||||
//! longer change it.
|
||||
//! - Operation: the first choice for each side of the profile is latched. A drag that starts
|
||||
//! into a body is a Cut and stays one however far it goes, through the far side included;
|
||||
//! one that starts as a Join does not turn into a Cut when it reaches another body. When
|
||||
//! the distance passes through zero onto the other side (a drag reversed past the start, or
|
||||
//! Reverse), the operation is decided afresh for that side and latched there. A zero
|
||||
//! distance changes nothing. Choosing an operation by hand ends the automatic choice for
|
||||
//! the rest of the extrude.
|
||||
|
||||
use solidcraft_engine::Sel;
|
||||
use solidcraft_engine::geom::Vec3;
|
||||
|
||||
/// The extrude window's automatic state.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Smart {
|
||||
/// The picks the latch and the direction were decided for.
|
||||
pub picked: Vec<Sel>,
|
||||
/// The direction was set by hand (or comes from a saved extrude): picks keep it.
|
||||
pub manual_dir: bool,
|
||||
/// The automatic operation (an index into the operations) and the side it was chosen for.
|
||||
pub latch: Option<(usize, i8)>,
|
||||
}
|
||||
|
||||
/// One-side direction for a new pick: 0 (along the normal) or 1 (reversed). `ray` is the view
|
||||
/// ray toward the pick; a ray running along the normal looks at the back of the profile, so
|
||||
/// the extrude comes back toward the camera. Body faces always go outward.
|
||||
pub fn picked_direction(ray: Vec3, normal: Vec3, body_face: bool) -> usize {
|
||||
usize::from(!body_face && ray.dot(normal) > 1e-9)
|
||||
}
|
||||
|
||||
/// Which side of the profile an extrude goes to: +1 along the normal, -1 against it, 0 for
|
||||
/// none (a zero distance). Symmetric and two-sided extrudes go both ways, so always +1.
|
||||
pub fn side(distance: f64, direction: usize) -> i8 {
|
||||
if direction >= 2 {
|
||||
return 1;
|
||||
}
|
||||
let d = if direction == 1 { -distance } else { distance };
|
||||
if !d.is_finite() || d.abs() < 1e-9 {
|
||||
0
|
||||
} else if d > 0.0 {
|
||||
1
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
/// The operation to show: the latched one while the extrude stays on the side it was chosen
|
||||
/// for, otherwise `fresh` (what the geometry says now), which is then latched for this side.
|
||||
/// `current` stands while the distance is zero.
|
||||
pub fn operation(fresh: usize, current: usize, side: i8, latch: &mut Option<(usize, i8)>) -> usize {
|
||||
if side == 0 {
|
||||
return current;
|
||||
}
|
||||
match *latch {
|
||||
Some((op, s)) if s == side => op,
|
||||
_ => {
|
||||
*latch = Some((fresh, side));
|
||||
fresh
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The view ray at `at`: from the eye for a perspective view, along the view for a parallel one.
|
||||
pub fn view_ray(cam: &solidcraft_engine::render::Camera, at: Vec3) -> Vec3 {
|
||||
if cam.fov > 0.0 { (at - cam.eye()).normalized().unwrap_or(-cam.back()) } else { -cam.back() }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const NEW: usize = 0;
|
||||
const JOIN: usize = 1;
|
||||
const CUT: usize = 2;
|
||||
|
||||
#[test]
|
||||
fn direction_follows_the_picked_side() {
|
||||
let up = Vec3::Z;
|
||||
// From the top view the ray runs down: the extrude goes up (along the normal).
|
||||
assert_eq!(picked_direction(-Vec3::Z, up, false), 0);
|
||||
// From the bottom view the ray runs up: the extrude goes down.
|
||||
assert_eq!(picked_direction(Vec3::Z, up, false), 1);
|
||||
// Edge on: along the normal, as before.
|
||||
assert_eq!(picked_direction(Vec3::X, up, false), 0);
|
||||
// A body's face goes outward from either side.
|
||||
assert_eq!(picked_direction(Vec3::Z, up, true), 0);
|
||||
assert_eq!(picked_direction(-Vec3::Z, up, true), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn side_of_the_distance() {
|
||||
assert_eq!(side(5.0, 0), 1);
|
||||
assert_eq!(side(-5.0, 0), -1);
|
||||
assert_eq!(side(5.0, 1), -1);
|
||||
assert_eq!(side(-5.0, 1), 1);
|
||||
assert_eq!(side(0.0, 0), 0);
|
||||
assert_eq!(side(f64::NAN, 0), 0);
|
||||
assert_eq!(side(-5.0, 2), 1, "symmetric");
|
||||
assert_eq!(side(-5.0, 3), 1, "two sides");
|
||||
}
|
||||
|
||||
/// A drag into a box that goes on past its far side: the geometry alone would say Join
|
||||
/// there, the latch keeps the Cut.
|
||||
#[test]
|
||||
fn a_cut_stays_a_cut_past_the_far_side() {
|
||||
let mut latch = None;
|
||||
let mut op = JOIN;
|
||||
// Into the body: Cut.
|
||||
op = operation(CUT, op, -1, &mut latch);
|
||||
assert_eq!(op, CUT);
|
||||
// Further, then past the far side, where a fresh look would see no material.
|
||||
for fresh in [CUT, CUT, JOIN, JOIN] {
|
||||
op = operation(fresh, op, -1, &mut latch);
|
||||
assert_eq!(op, CUT);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_join_does_not_turn_into_a_cut() {
|
||||
let mut latch = None;
|
||||
let mut op = operation(JOIN, NEW, 1, &mut latch);
|
||||
assert_eq!(op, JOIN);
|
||||
// The tool reaches another body further out.
|
||||
op = operation(CUT, op, 1, &mut latch);
|
||||
assert_eq!(op, JOIN);
|
||||
}
|
||||
|
||||
/// Reversing through zero decides afresh for the new side; back again, afresh again.
|
||||
#[test]
|
||||
fn reversing_through_zero_decides_again() {
|
||||
let mut latch = None;
|
||||
let mut op = operation(CUT, JOIN, -1, &mut latch);
|
||||
assert_eq!(op, CUT);
|
||||
// At zero nothing changes.
|
||||
op = operation(JOIN, op, 0, &mut latch);
|
||||
assert_eq!(op, CUT);
|
||||
assert_eq!(latch, Some((CUT, -1)));
|
||||
// Out the other side, away from the body: Join, latched there.
|
||||
op = operation(JOIN, op, 1, &mut latch);
|
||||
assert_eq!(op, JOIN);
|
||||
op = operation(CUT, op, 1, &mut latch);
|
||||
assert_eq!(op, JOIN);
|
||||
// Back into the body: Cut again.
|
||||
op = operation(CUT, op, -1, &mut latch);
|
||||
assert_eq!(op, CUT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_ray_of_both_projections() {
|
||||
let mut cam = solidcraft_engine::render::Camera { fov: 0.0, ..Default::default() };
|
||||
assert!((view_ray(&cam, Vec3::ZERO) + cam.back()).len() < 1e-12);
|
||||
cam.fov = 0.8;
|
||||
let at = Vec3::new(10.0, 0.0, 0.0);
|
||||
let r = view_ray(&cam, at);
|
||||
assert!((r - (at - cam.eye()).normalized().unwrap()).len() < 1e-12);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
//! 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));
|
||||
}
|
||||
@@ -139,7 +139,10 @@
|
||||
"start": "solid.extrude"
|
||||
},
|
||||
{
|
||||
"text": "20"
|
||||
"note": "The iso view sees the XZ sketch from behind (its normal is +Y), so the extrude comes toward the camera (-Y). -20 takes it back along +Y, where the joint below expects it."
|
||||
},
|
||||
{
|
||||
"text": "-20"
|
||||
},
|
||||
{
|
||||
"key": "Enter"
|
||||
|
||||
Reference in new issue
Block a user