Project: a refusal that names no body names the kind alone (#10626) (#10859)

The refusal of sketch.project ended in "face of " with nothing after the
"of" when the reference named no body: a picked face, edge or vertex is
found by its geometry, so the body it belongs to is not in the reference.

LinkSource::describe() now drops the "of <body>" when there is no body to
name and names the kind alone ("face", "edge", "vertex", "spun
profile", "isoparametric curve"). A source that names a body keeps its
name.

Only the text changes: the command refuses exactly what it refused before.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Homero Thompson
2026-11-02 13:49:26 +00:00
committed by GitHub
parent 9b431fea16
commit 6649cf4cfe
3 changed files with 36 additions and 6 deletions
@@ -932,9 +932,13 @@ fn projecting_what_the_face_sketch_already_shows_adds_no_second_copy() {
let border = shown(&mut s);
assert_eq!(border.0, 8, "four sides and four rounded corners");
let corner = 5.0 - 5.0 * std::f64::consts::FRAC_1_SQRT_2;
for refs in [json!([{"face":[20,15,10]}]), json!([{"edge":[20,0,10]}]), json!([{"edge":[corner,corner,10]}])] {
// These references name no body, so the refusal names the kind alone: it must not end in
// "face of " with nothing after the "of" (#4929).
let cases = [(json!([{"face":[20,15,10]}]), "face"), (json!([{"edge":[20,0,10]}]), "edge"), (json!([{"edge":[corner,corner,10]}]), "edge")];
for (refs, what) in cases {
let e = s.execute("sketch.project", &json!({"refs": refs})).unwrap_err().to_string();
assert!(e.contains("already projected"), "{refs}: {e}");
assert!(e.ends_with(&format!("already projected into this sketch: {what}")), "{refs}: {e}");
assert_eq!(shown(&mut s), border, "{refs}");
}
// Something new still projects, and alongside it a reference already shown is skipped.
+12 -5
View File
@@ -134,10 +134,10 @@ impl LinkSource {
/// Short description for messages and the browser.
pub fn describe(&self) -> String {
match self {
LinkSource::Edge { body, .. } => format!("edge of {body}"),
LinkSource::Face { body, .. } => format!("face of {body}"),
LinkSource::Edge { body, .. } => of_body("edge", body),
LinkSource::Face { body, .. } => of_body("face", body),
LinkSource::Body { body } => body.clone(),
LinkSource::Vertex { body, .. } => format!("vertex of {body}"),
LinkSource::Vertex { body, .. } => of_body("vertex", body),
LinkSource::SketchCurve { curve, .. } => format!("sketch curve {curve}"),
LinkSource::SketchPoint { point, .. } => format!("sketch point {point}"),
LinkSource::Origin => "origin".into(),
@@ -146,8 +146,8 @@ impl LinkSource {
LinkSource::Plane { name } => format!("{name} plane"),
LinkSource::OnSurface { curve, .. } => format!("{curve} on a face"),
LinkSource::Intersection { a, b } => format!("intersection of {} and {}", a.describe(), b.describe()),
LinkSource::Spun { body, .. } => format!("spun profile of {body}"),
LinkSource::Iso { body, .. } => format!("isoparametric curve of {body}"),
LinkSource::Spun { body, .. } => of_body("spun profile", body),
LinkSource::Iso { body, .. } => of_body("isoparametric curve", body),
LinkSource::Text { text, .. } => format!("text \"{}\"", text.chars().take(20).collect::<String>()),
}
}
@@ -178,6 +178,13 @@ impl LinkSource {
}
}
/// The description of a source that names a body: "face of Body1", or the kind alone when the
/// reference names no body (a picked face is found by its geometry, and the body it belongs
/// to is not named in the reference).
fn of_body(kind: &str, body: &str) -> String {
if body.is_empty() { kind.to_string() } else { format!("{kind} of {body}") }
}
/// Half length of the line a sketch axis link produces.
pub const SKETCH_AXIS_HALF: f64 = 100.0;
+19
View File
@@ -949,6 +949,25 @@ fn links_update_in_place_rebuild_and_break() {
assert_eq!(back.link(&j3).unwrap().kind, LinkKind::Intersect);
}
/// A reference that names no body describes itself by its kind alone: the message must not
/// end in "face of " with nothing after the "of" (#4929).
#[test]
fn a_source_without_a_body_name_names_its_kind_alone() {
use solidcraft_geom::Vec3;
let at = Vec3::ZERO;
for (source, expected) in [
(LinkSource::Edge { body: String::new(), at }, "edge"),
(LinkSource::Face { body: String::new(), at }, "face"),
(LinkSource::Vertex { body: String::new(), at }, "vertex"),
(LinkSource::Iso { body: String::new(), at, dir: "u".into() }, "isoparametric curve"),
(LinkSource::Spun { body: String::new(), origin: at, dir: Vec3::X }, "spun profile"),
] {
assert_eq!(source.describe(), expected, "{source:?}");
}
// A named body keeps its name.
assert_eq!(LinkSource::Face { body: "Body1".into(), at }.describe(), "face of Body1");
}
/// Many independent constrained rectangles solve quickly (per-component work).
#[test]
fn large_sketch_solves_fast() {