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.
This commit is contained in:
phanoob authored and GitHub committed 2026-10-14 13:24:23 -04:00
1 parent ed943a74e6
commit 6ebfcd16b6
3 files changed
+63 -3

No files matched your search

+21 -3
View File
@@ -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<Value> {
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<String> {
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<Value> {
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.
@@ -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");
}
+5
View File
@@ -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.