diff --git a/crates/doc/src/assembly.rs b/crates/doc/src/assembly.rs index c6df1ebc..bf95abfd 100644 --- a/crates/doc/src/assembly.rs +++ b/crates/doc/src/assembly.rs @@ -148,7 +148,11 @@ fn map_plane_ref(m: &Mat, p: &mut PlaneRef) { *at = apply_point(m, *at); map_geo_ref(m, face); } - PlaneRef::Midplane { .. } | PlaneRef::TwoEdges { .. } | PlaneRef::ThreePoints { .. } | PlaneRef::AlongPath { .. } => { + PlaneRef::Midplane { .. } + | PlaneRef::TwoEdges { .. } + | PlaneRef::ThreePoints { .. } + | PlaneRef::AlongPath { .. } + | PlaneRef::Perpendicular { .. } => { for r in p.refs_mut() { map_geo_ref(m, r); } diff --git a/crates/doc/src/construct.rs b/crates/doc/src/construct.rs index 0fd1f9aa..c666faec 100644 --- a/crates/doc/src/construct.rs +++ b/crates/doc/src/construct.rs @@ -513,6 +513,15 @@ pub(crate) fn resolve_geo_plane(ctx: &Ctx, p: &PlaneRef) -> Result { let (on, n) = surface_normal(s, *at).ok_or_else(|| bad("the point is on the face's axis"))?; Plane::from_normal(on, n).ok_or_else(degenerate) } + PlaneRef::Perpendicular { plane: base, line: l } => { + let pl = plane(ctx, base)?; + let (o, d) = line(ctx, l)?; + let n = d + .cross(pl.normal()) + .normalized() + .ok_or_else(|| bad("the line is square to the plane: every plane through it is perpendicular; pick a line in or along the plane"))?; + Plane::new(o, d, n.cross(d)).ok_or_else(degenerate) + } PlaneRef::AlongPath { path, t } => { let t = Document::eval_in(ctx.vals, t, Kind::Unitless)?; let (p, tan) = along(&path_points(ctx, path)?, t).ok_or_else(|| bad("the path has no length"))?; @@ -621,6 +630,7 @@ impl PlaneRef { pub fn refs_mut(&mut self) -> Vec<&mut GeoRef> { match self { PlaneRef::Midplane { a, b } | PlaneRef::TwoEdges { a, b } => vec![a.as_mut(), b.as_mut()], + PlaneRef::Perpendicular { plane, line } => vec![plane.as_mut(), line.as_mut()], PlaneRef::ThreePoints { a, b, c } => vec![a.as_mut(), b.as_mut(), c.as_mut()], PlaneRef::Tangent { face, .. } => vec![face.as_mut()], PlaneRef::AlongPath { path, .. } => vec![path.as_mut()], diff --git a/crates/doc/src/document.rs b/crates/doc/src/document.rs index 8e626671..2301980e 100644 --- a/crates/doc/src/document.rs +++ b/crates/doc/src/document.rs @@ -54,6 +54,8 @@ pub enum PlaneRef { Tangent { face: Box, at: Vec3 }, /// Square to an edge or sketch curve, a fraction `t` (expression, 0…1) of the way along it. AlongPath { path: Box, t: String }, + /// Through a straight edge (or line, axis) and square to a plane (or planar face). + Perpendicular { plane: Box, line: Box }, /// A planar body face: found again on each evaluation (the face with this plane's normal /// nearest `at`), so sketches on it follow when earlier features change; `plane` is the /// plane as picked (its frame, and the fallback when the face is gone). `name` is the face's @@ -1808,7 +1810,8 @@ impl Document { | PlaneRef::TwoEdges { .. } | PlaneRef::ThreePoints { .. } | PlaneRef::Tangent { .. } - | PlaneRef::AlongPath { .. } => { + | PlaneRef::AlongPath { .. } + | PlaneRef::Perpendicular { .. } => { let st = st.ok_or_else(|| DocError::Invalid("this plane is built from the model: evaluate first".into()))?; crate::construct::resolve_geo_plane(&crate::construct::Ctx { doc: self, vals, st }, p) } diff --git a/crates/engine/src/cmd/construct.rs b/crates/engine/src/cmd/construct.rs index d9b3611d..e1787bbf 100644 --- a/crates/engine/src/cmd/construct.rs +++ b/crates/engine/src/cmd/construct.rs @@ -83,6 +83,9 @@ pub static COMMANDS: &[CommandSpec] = &[ .at("SOLID", "CONSTRUCT") .icon("plane") .params("path: edge or sketch curve ref; t: expr, 0…1 along it (default 0.5); name? The plane is square to the path there"), + CommandSpec::new("construct.plane.perpendicular", "Perpendicular Plane", plane_perpendicular).at("SOLID", "CONSTRUCT").icon("plane").params( + "plane: plane or planar face ref; line: straight edge, sketch line or axis ref; name? The plane holds the line and is square to the plane", + ), ]; /// A reference from its JSON form. @@ -249,3 +252,9 @@ fn plane_along(s: &mut Session, p: &Value) -> Result { let t = t_param(s, p, cmd)?; plane(s, p, PlaneRef::AlongPath { path: Box::new(path), t }) } + +fn plane_perpendicular(s: &mut Session, p: &Value) -> Result { + let cmd = "construct.plane.perpendicular"; + let (base, line) = (geo(s, p.get("plane"), cmd, "plane")?, geo(s, p.get("line"), cmd, "line")?); + plane(s, p, PlaneRef::Perpendicular { plane: Box::new(base), line: Box::new(line) }) +} diff --git a/crates/engine/src/construct_tests.rs b/crates/engine/src/construct_tests.rs index 19621845..3c1d72e0 100644 --- a/crates/engine/src/construct_tests.rs +++ b/crates/engine/src/construct_tests.rs @@ -257,3 +257,18 @@ fn construction_lives_in_the_component_frame() { run(&mut s, "occurrence.move", json!({"occurrence": "Part:1", "translate": [0, 20, 0]})); assert!(near(point(&s, "PinTop"), Vec3::new(50.0, 20.0, 10.0)), "{:?}", point(&s, "PinTop")); } + +#[test] +fn plane_perpendicular_to_a_plane_through_a_line() { + let mut s = part(); + // Through the box's front bottom edge (along X), square to the bottom: the front face's plane. + run(&mut s, "construct.plane.perpendicular", json!({"plane": {"face": [20, 15, 0]}, "line": {"edge": [20, 0, 0]}, "name": "F"})); + let f = plane(&s, "F"); + assert!(parallel(f.normal(), Vec3::Y) && on_plane(&f, Vec3::new(7.0, 0.0, 13.0)), "{f:?}"); + // Through the world Z axis, square to XZ: the YZ plane. + run(&mut s, "construct.plane.perpendicular", json!({"plane": "XZ", "line": {"axis": "Z"}, "name": "G"})); + let g = plane(&s, "G"); + assert!(parallel(g.normal(), Vec3::X) && on_plane(&g, Vec3::new(0.0, 5.0, 9.0)), "{g:?}"); + // A line square to the plane has no single answer. + assert!(s.execute("construct.plane.perpendicular", &json!({"plane": "XY", "line": {"axis": "Z"}})).is_err()); +} diff --git a/xtask/data/fusion-ids.tsv b/xtask/data/fusion-ids.tsv index 67e24642..79b533d1 100644 --- a/xtask/data/fusion-ids.tsv +++ b/xtask/data/fusion-ids.tsv @@ -179,3 +179,4 @@ FusionBossCommand plastic.boss FusionSnapFitCommand plastic.snap_fit FusionLipCommand plastic.lip FusionRestCommand plastic.rest +ConstructionPlanePerpendicularToPlaneCommand construct.plane.perpendicular