From 6ebfcd16b6b7f45fe9ef2eb68909694bc1c61a49 Mon Sep 17 00:00:00 2001 From: phanoob Date: Wed, 14 Oct 2026 19:24:23 +0200 Subject: [PATCH] A text's path becomes construction (#1119) sketch.text and sketch.edit_text make the curve a text follows construction in the same undo step, so it no longer cuts the letters into pieces; the reply names the curve as guide. --- crates/engine/src/cmd/sketch_create.rs | 24 ++++++++++-- .../engine/src/cmd/sketch_text_move_tests.rs | 37 +++++++++++++++++++ docs/book/src/guide/sketching.md | 5 +++ 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/cmd/sketch_create.rs b/crates/engine/src/cmd/sketch_create.rs index 3aac1027..c6eb0178 100644 --- a/crates/engine/src/cmd/sketch_create.rs +++ b/crates/engine/src/cmd/sketch_create.rs @@ -8,7 +8,7 @@ use solidcraft_sketch::{ConstraintKind, CurveKind, LinkKind, LinkSource, Sketch, /// The style parameters of `sketch.text` and `sketch.edit_text`. macro_rules! text_style_doc { () => { - "bold?, italic?: bool (an installed family's bold or italic face; italic without one is slanted), align?: left|center|right (the lines against `at`, or along the path), letter_spacing?: extra mm between letters, line_spacing?: times the font's line distance (default 1), path?: a curve id of the sketch the baseline follows (\"\": none; `at` and `angle` then do not place the text), reverse?: bool (the other way along the path)" + "bold?, italic?: bool (an installed family's bold or italic face; italic without one is slanted), align?: left|center|right (the lines against `at`, or along the path), letter_spacing?: extra mm between letters, line_spacing?: times the font's line distance (default 1), path?: a curve id of the sketch the baseline follows (\"\": none; `at` and `angle` then do not place the text; the curve becomes construction, a guide the letters stand on), reverse?: bool (the other way along the path)" }; } @@ -209,11 +209,28 @@ fn text(s: &mut Session, p: &Value) -> Result { if geom.is_empty() { return Err(bad(cmd, "the text makes no outline")); } + let guide = guide(&mut sk, &src); let l = sk.add_link(LinkKind::Text, src, &geom)?; let curves = ids_of(&sk, &sk.link_curves(&l)); *doc.sketch_mut(id)? = sk; *s.doc_mut() = doc; - Ok(json!({"link": l, "curves": curves})) + Ok(json!({"link": l, "curves": curves, "guide": guide})) +} + +/// A text's path is its guide: the curve it follows becomes construction. Left as it was, it +/// would cut the letters standing on it (and the ones hanging below it) into pieces, each +/// profile a piece of a letter and of the curve, and picking the text would pick those pieces. +/// Taking the text off the curve leaves the curve as it is. The curve made construction, if any. +fn guide(sk: &mut Sketch, src: &LinkSource) -> Option { + let LinkSource::Text { style, .. } = src else { return None }; + let id = style.path.as_ref()?; + let curve = sk.curve_index(id).and_then(|c| sk.curves.get_mut(c))?; + if curve.construction { + return None; + } + curve.construction = true; + curve.centerline = false; + Some(id.clone()) } /// The text source owning an outline or one of its linked points. @@ -290,10 +307,11 @@ fn apply_text_edit(sk: &mut Sketch, p: &Value) -> Result { if geom.is_empty() { return Err(bad(cmd, "the text makes no outline")); } + let guide = guide(sk, &src); sk.set_link_source(&l, src.clone()); sk.update_link(&l, &geom)?; let curves = ids_of(sk, &sk.link_curves(&l)); - Ok(json!({"link": l, "curves": curves})) + Ok(json!({"link": l, "curves": curves, "guide": guide})) } /// The curve ending at point `pi` and the direction leaving it there. diff --git a/crates/engine/src/cmd/sketch_text_move_tests.rs b/crates/engine/src/cmd/sketch_text_move_tests.rs index b9bb2e24..c8f3aa7c 100644 --- a/crates/engine/src/cmd/sketch_text_move_tests.rs +++ b/crates/engine/src/cmd/sketch_text_move_tests.rs @@ -333,3 +333,40 @@ fn sketch_text_inputs_left_out_or_null_keep_their_defaults() { assert_eq!((text.as_str(), height, angle), ("II", 20.0, 0.0)); assert!(at.dist(Vec2::new(3.0, 4.0)) < 1e-12, "{at:?}"); } + +/// A text set on a spline stands on it, its descenders below it: the spline becomes +/// construction, so it doesn't cut the letters into pieces. Every profile is then the text's +/// alone (picking the text picks letters, not pieces of letters and curve), and one undo puts +/// the text and the curve back as they were. +#[test] +fn a_text_on_a_spline_turns_its_path_into_a_guide() { + let mut s = Session::default(); + run(&mut s, "sketch.create", json!({"plane":"XY"})); + let spline = run(&mut s, "sketch.spline.fit_point", json!({"points":[[0,0],[30,15],[60,0],[90,10]]}))["created"]["curves"][0] + .as_str() + .unwrap() + .to_string(); + let construction = |s: &Session| { + let sk = s.doc.sketch(s.active_sketch.unwrap()).unwrap(); + sk.curves.iter().find(|c| c.id == spline).unwrap().construction + }; + let t = run(&mut s, "sketch.text", json!({"text":"Phago","at":[0,0],"height":10,"path":spline})); + assert_eq!(t["guide"], json!(spline)); + assert!(construction(&s)); + let state = s.world_state(); + let ss = state.sketch(s.active_sketch.unwrap()).unwrap(); + for (i, p) in ss.profiles.iter().enumerate() { + let alone = p.curve_ids().iter().all(|id| ss.sketch.curves.iter().any(|c| &c.id == id && c.link.is_some())); + assert!(alone, "profile {i} is bounded by the spline too"); + } + // Taken off the curve, the text leaves it a guide; set on it again, nothing changes. + let link = t["link"].as_str().unwrap().to_string(); + run(&mut s, "sketch.edit_text", json!({"link":link,"path":""})); + assert!(construction(&s)); + assert_eq!(run(&mut s, "sketch.edit_text", json!({"link":link,"path":spline}))["guide"], json!(null)); + // One undo for each step, back to the text's first state, then to before it. + for _ in 0..3 { + run(&mut s, "edit.undo", json!({})); + } + assert!(!construction(&s), "undoing the text gives the curve back"); +} diff --git a/docs/book/src/guide/sketching.md b/docs/book/src/guide/sketching.md index d51ae61a..8199f490 100644 --- a/docs/book/src/guide/sketching.md +++ b/docs/book/src/guide/sketching.md @@ -57,6 +57,11 @@ geometry cannot identify one text: use **Select Other** to choose its outline in Change the text, font, baseline X/Y, height or angle, then confirm with **OK**; **Cancel** keeps the original text. Position and height use the document's units. +A text can follow a curve of its sketch (its **Path**). That curve becomes construction: the +letters stand on it and their descenders cross it, so as a normal curve it would cut them into +pieces, and picking the text would pick those pieces. Taking the text off the curve leaves it +construction; **Normal/Construction** turns it back. + Drag a text outline to move all its letters together. Let go to finish in one undo step, or press **Esc** during the drag to restore the original text. The text stays editable. For an exact displacement, select an outline and choose **Move/Copy** from its context menu.