Extrude: to an object through the body, the automatic operation is Cut (#1633) (#1636)

Thanks @DuBearte.
This commit is contained in:
DuBearte authored and GitHub committed 2026-10-16 06:26:35 -04:00
1 parent e93f8c1b84
commit 4d58d3bbca
5 files changed
+48 -13

No files matched your search

+16 -4
View File
@@ -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<f64> {
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() {
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
+21
View File
@@ -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() {
+9 -7
View File
@@ -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, &params)
{
side = crate::smart_extrude::side(reach, 0);
}
let Some(fresh) = solidcraft_engine::auto_operation(&app.session, &params).and_then(|op| OPS.iter().position(|o| *o == op)) else {
return;
};
if let Kind::Extrude { operation, smart, .. } = &mut d.kind {