diff --git a/crates/engine/src/cmd/features.rs b/crates/engine/src/cmd/features.rs index ed5843a3..fa07dd15 100644 --- a/crates/engine/src/cmd/features.rs +++ b/crates/engine/src/cmd/features.rs @@ -141,6 +141,20 @@ fn extrude_base(s: &Session, p: &Value) -> Option<(Vec3, Vec3)> { Some((ss.plane.to_world(q), ss.plane.normal())) } +/// How far an extrude with these parameters goes from its profile along the profile's normal +/// (negative: the other way). To an object, the target's height, on whichever side it is: the +/// extrude goes there and its `direction` does not count (#1633). Otherwise its distance, turned +/// round for the negative direction. +pub fn extrude_reach(s: &Session, p: &Value) -> Option { + if let Some(t) = p.get("to").and_then(vec3) { + let (base, n) = extrude_base(s, p)?; + return Some((t - base).dot(n)).filter(|h| h.is_finite()); + } + let d = s.doc.eval(&expr(p, "distance")?, Kind::Length).ok()?; + let dir = str_(p, "direction").map(str::to_ascii_lowercase); + Some(if matches!(dir.as_deref(), Some("negative" | "reverse" | "flip")) { -d } else { d }) +} + /// How far past the profile [`auto_operation`] looks for material it starts in. const START_PROBE_MM: f64 = 0.1; @@ -152,10 +166,8 @@ const START_PROBE_MM: f64 = 0.1; /// 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); - let symmetric = dir.as_deref() == Some("symmetric"); - let d = if matches!(dir.as_deref(), Some("negative" | "reverse" | "flip")) { -d } else { d }; + let d = extrude_reach(s, p)?; + let symmetric = p.get("to").is_none() && str_(p, "direction").is_some_and(|d| d.eq_ignore_ascii_case("symmetric")); let (base, n) = extrude_base(s, p)?; let st = s.model.state(); if st.bodies.is_empty() || !d.is_finite() { diff --git a/crates/engine/src/cmd/mod.rs b/crates/engine/src/cmd/mod.rs index a37f226a..34883809 100644 --- a/crates/engine/src/cmd/mod.rs +++ b/crates/engine/src/cmd/mod.rs @@ -33,7 +33,7 @@ mod edit; mod face; mod features; mod features_more; -pub use features::auto_operation; +pub use features::{auto_operation, extrude_reach}; mod component_link; mod config; mod file; diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index b5c2da8d..6b60a557 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -31,7 +31,7 @@ use serde_json::Value; use solidcraft_doc::{Document, Model, ModelState}; use solidcraft_geom::Vec3; -pub use cmd::{CommandInfo, CommandSpec, auto_operation, command_specs, find_command}; +pub use cmd::{CommandInfo, CommandSpec, auto_operation, command_specs, extrude_reach, find_command}; pub use solidcraft_doc as doc; pub use solidcraft_doc::Visibility; pub use solidcraft_geom as geom; diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index d0be87f3..c5c419f8 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -1196,6 +1196,27 @@ fn extrude_auto_operation() { assert!(rel(volume(&mut s), 40.0 * 30.0 * 15.0 - 100.0 * 8.0) < 1e-6); } +/// #1633: to an object, the extrude goes to it on whichever side it is, whatever its +/// `direction`: through the body to its far face is a Cut, out to something above is a Join. +#[test] +fn extrude_auto_operation_follows_the_to_object() { + let mut s = Session::default(); + run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20})); + run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 5, "corner": [0, 0, 50], "body_name": "Above"})); + for direction in ["positive", "negative"] { + // From the top face to the bottom face: through the body. + let down = json!({"face": [20, 15, 20], "to": [10, 10, 0], "distance": 19, "direction": direction}); + assert!(extrude_reach(&s, &down).is_some_and(|h| (h + 20.0).abs() < 1e-6), "{down}"); + assert_eq!(auto_operation(&s, &down), Some("cut"), "{down}"); + // From the top face up to the box above: out of the body. + let up = json!({"face": [20, 15, 20], "to": [10, 10, 50], "distance": 19, "direction": direction}); + assert!(extrude_reach(&s, &up).is_some_and(|h| (h - 30.0).abs() < 1e-6), "{up}"); + assert_eq!(auto_operation(&s, &up), Some("join"), "{up}"); + } + // Without a To object the direction still counts. + assert_eq!(extrude_reach(&s, &json!({"face": [20, 15, 20], "distance": 5, "direction": "negative"})), Some(-5.0)); +} + /// Additive extrusions default to joining even when only a profile side touches material. #[test] fn auto_extrude_joins_side_contact_elbow_and_keeps_new_body_explicit() { diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index dbe7edf6..10219146 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -2054,16 +2054,18 @@ fn auto_operation(app: &SolveApp, d: &mut Dialog) { } 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); + let mut side = crate::smart_extrude::side(dist, *direction); if !matches!(d.kind, Kind::Extrude { auto_op: true, .. }) { return; } - 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 { + let Some(params) = dialog_commands(app, d).ok().and_then(|c| c.into_iter().next()).map(|(_, p)| p) else { return }; + // To an object, the extrude goes to it on whichever side it is: that is the side (#1633). + if params.get("to").is_some() + && let Some(reach) = solidcraft_engine::extrude_reach(&app.session, ¶ms) + { + side = crate::smart_extrude::side(reach, 0); + } + let Some(fresh) = solidcraft_engine::auto_operation(&app.session, ¶ms).and_then(|op| OPS.iter().position(|o| *o == op)) else { return; }; if let Kind::Extrude { operation, smart, .. } = &mut d.kind {