From 511d105dcb5ffc74881eb9d089c235d7390c7085 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20L=C3=B3pez?= Date: Mon, 12 Oct 2026 20:29:08 +0200 Subject: [PATCH] Project faces split by hidden seams as a whole (#352) Projecting a cylinder's wall or the face of a round along a circular edge failed with "face projection has an open or invalid boundary": these surfaces are stored in halves joined by hidden seams, and the projection dropped the seams but kept only the picked half. It now projects the whole patch, the face as drawn, through the same surface_patch the viewport uses to highlight it. --- crates/doc/src/project_order.rs | 6 +++++ crates/engine/src/cmd/sketch_project_tests.rs | 27 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/crates/doc/src/project_order.rs b/crates/doc/src/project_order.rs index 173ed93f..18bcdef6 100644 --- a/crates/doc/src/project_order.rs +++ b/crates/doc/src/project_order.rs @@ -9,7 +9,13 @@ use crate::{DocError, Result}; type Key = (i64, i64, i64); +/// Boundary of the picked face as drawn: a closed surface cut into pieces (a cylinder or a +/// round's torus in halves) projects as the whole patch, since its seams are hidden. pub(super) fn face_edges(mesh: &Mesh, face: u32, plane: &Plane) -> Result> { + let patch = mesh.surface_patch(face as usize); + if patch.len() > 1 { + return family_edges(mesh, &patch.into_iter().collect(), plane); + } let edges = mesh.face_edges(face).into_iter().filter(|e| !mesh.seams.get(*e).copied().unwrap_or(false)).collect(); boundary_edges(mesh, edges, plane) } diff --git a/crates/engine/src/cmd/sketch_project_tests.rs b/crates/engine/src/cmd/sketch_project_tests.rs index 6dbb6eff..12565ea4 100644 --- a/crates/engine/src/cmd/sketch_project_tests.rs +++ b/crates/engine/src/cmd/sketch_project_tests.rs @@ -499,3 +499,30 @@ fn project_and_include_from_other_bodies() { assert!(row["error"].is_null(), "{row}"); assert!(row["warning"].as_str().is_some_and(|w| w.contains("lost")), "{row}"); } + +/// A cylinder's wall and the torus of a rounded rim are drawn as one face but stored in halves +/// joined by hidden seams; projecting one projects the whole face, not an open half. +#[test] +fn faces_split_by_hidden_seams_project_whole() { + let mut s = Session::default(); + run(&mut s, "sketch.create", json!({"plane":"XY"})); + run(&mut s, "sketch.circle.center", json!({"center":[0,0],"radius":20})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.extrude", json!({"distance":10})); + run(&mut s, "sketch.create", json!({"plane":"XZ"})); + run(&mut s, "sketch.project", json!({"refs":[{"face":[20,0,5]}]})); + let wall = run(&mut s, "sketch.inspect", json!({})); + let heights: Vec = + wall["curves"].as_array().unwrap().iter().flat_map(|c| [c["start_at"][1].as_f64().unwrap(), c["end_at"][1].as_f64().unwrap()]).collect(); + assert!(heights.iter().any(|y| y.abs() < 1e-6) && heights.iter().any(|y| (y.abs() - 10.).abs() < 1e-6), "{wall}"); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.fillet", json!({"edges":[[20,0,10]],"radius":3})); + run(&mut s, "sketch.create", json!({"plane":"XY"})); + let t = 0.5f64.sqrt() * 3.; + run(&mut s, "sketch.project", json!({"refs":[{"face":[17. + t, 0, 7. + t]}]})); + let round = run(&mut s, "sketch.inspect", json!({})); + let mut radii: Vec = round["curves"].as_array().unwrap().iter().map(|c| c["radius"].as_f64().unwrap()).collect(); + radii.sort_by(f64::total_cmp); + assert_eq!(radii.len(), 4, "{round}"); + assert!(radii[..2].iter().all(|r| (r - 17.).abs() < 1e-6) && radii[2..].iter().all(|r| (r - 20.).abs() < 1e-6), "{round}"); +}