Sketch: Explode Text turns a text's outlines into ordinary curves (#1842) (#1921)

Thanks @DuBearte.
This commit is contained in:
DuBearte authored and GitHub committed 2026-10-17 04:52:41 -04:00
1 parent e49770bd24
commit fd073620ae
8 files changed
+65

No files matched your search

+1
View File
@@ -32,6 +32,7 @@
"Duration": "Dauer",
"Edges": "Kanten",
"Edit Profile Sketch": "Profile-Skizze bearbeiten",
"Explode Text": "Text auflösen",
"Export Animation…": "Animation exportieren…",
"Export Current View as DXF": "Aktuelle Ansicht als DXF exportieren",
"Export Current View as DXF…": "Aktuelle Ansicht als DXF exportieren…",
+1
View File
@@ -98,6 +98,7 @@
"Enable rule": "Enable rule",
"Enabled": "Enabled",
"Explode": "Explode",
"Explode Text": "Explode Text",
"Export Animation…": "Export Animation…",
"Export Current View as DXF": "Export Current View as DXF",
"Export Current View as DXF…": "Export Current View as DXF…",
+1
View File
@@ -98,6 +98,7 @@
"Enable rule": "ルールを有効化",
"Enabled": "有効",
"Explode": "分解",
"Explode Text": "テキストを分解",
"Export Animation…": "アニメーションをエクスポート…",
"Export Current View as DXF": "現在の表示をDXFでエクスポート",
"Export Current View as DXF…": "現在の表示をDXFでエクスポート…",
+1
View File
@@ -32,6 +32,7 @@
"Duration": "지속 시간",
"Edges": "모서리",
"Edit Profile Sketch": "윤곽 스케치 편집",
"Explode Text": "텍스트 분해",
"Export Animation…": "애니메이션 내보내기…",
"Export Current View as DXF": "현재 뷰를 DXF로 내보내기",
"Export Current View as DXF…": "현재 뷰를 DXF로 내보내기…",
+1
View File
@@ -98,6 +98,7 @@
"Enable rule": "启用规则",
"Enabled": "已启用",
"Explode": "爆炸",
"Explode Text": "分解文字",
"Export Animation…": "导出动画…",
"Export Current View as DXF": "将当前视图导出为DXF",
"Export Current View as DXF…": "将当前视图导出为DXF…",
+9
View File
@@ -719,6 +719,13 @@ fn component_items(app: &SolveApp, id: u64) -> Vec<Item> {
v
}
/// Explode Text (#1842): the selected text's outlines become normal sketch lines, arcs and
/// curves, no longer tied to the text, so they can be picked, trimmed and edited one by one.
fn explode_text(app: &SolveApp) -> Option<Item> {
let link = crate::sketch_text::selected_link(app)?;
Some(cmd(app, "sketch.break_link", "Explode Text").with(json!({ "link": link })))
}
/// Sketch entity items: construction, fix, delete, dimension and the constraints the selection
/// can take.
fn sketch_entity_items(app: &SolveApp) -> Vec<Item> {
@@ -754,6 +761,7 @@ fn sketch_entity_items(app: &SolveApp) -> Vec<Item> {
if curves.is_empty() && points.is_empty() {
if crate::sketch_text::selected_entity(app).is_some() {
v.push(cmd(app, "sketch.edit_text", "Edit Text"));
v.extend(explode_text(app));
}
v.push(cmd(app, "sketch.line", "Line"));
v.push(cmd(app, "sketch.rectangle.two_point", "Rectangle"));
@@ -773,6 +781,7 @@ fn sketch_entity_items(app: &SolveApp) -> Vec<Item> {
}
if crate::sketch_text::selected_entity(app).is_some() {
v.push(cmd(app, "sketch.edit_text", "Edit Text"));
v.extend(explode_text(app));
}
v.push(cmd(app, "sketch.move", "Move/Copy").key("M"));
v.push(act("ui.cut", "Cut", "").key("Ctrl+X"));
+46
View File
@@ -183,6 +183,13 @@ pub(crate) fn selected_entity(app: &SolveApp) -> Option<String> {
selected
}
/// The link of the one text the selection belongs to (see [`selected_entity`]).
pub(crate) fn selected_link(app: &SolveApp) -> Option<String> {
let entity = selected_entity(app)?;
let sk = app.session.doc.sketch(app.session.active_sketch?).ok()?;
sk.curves.iter().find(|curve| curve.id == entity)?.link.clone()
}
/// Show every outline affected by the open editor without changing feature selections.
pub(crate) fn highlighted(app: &SolveApp) -> Vec<Sel> {
let Some(Kind::Text(tx)) = app.dialog.as_ref().map(|dialog| &dialog.kind) else { return Vec::new() };
@@ -752,6 +759,45 @@ mod tests {
assert!(highlighted(&h.app).is_empty());
}
/// Explode Text (#1842): the text's outlines stay where they are as ordinary sketch curves,
/// the text itself goes, and one undo brings it back.
#[test]
fn explode_text_leaves_its_outlines_as_normal_curves() {
let (mut h, link, _) = setup();
h.app.run("sketch.edit_text", json!({"link":link,"text":"TO"})).unwrap();
let sid = h.app.session.active_sketch.unwrap();
let outline = |h: &Harness| {
let sk = h.app.session.doc.sketch(sid).unwrap();
let mut ids: Vec<(String, Option<String>)> = sk.curves.iter().map(|c| (c.id.clone(), c.link.clone())).collect();
ids.sort();
ids
};
let before = outline(&h);
assert!(before.len() > 4 && before.iter().all(|(_, l)| l.as_deref() == Some(link.as_str())));
let profiles = h.app.session.world_state().sketch(sid).unwrap().profiles.len();
h.app.session.selection = vec![Sel::SketchCurve { id: before[0].0.clone() }];
let undo = h.app.session.undo.len();
let item = crate::context_menu::items(&h.app, &crate::context_menu::Target::Viewport)
.into_iter()
.find(|item| item.label == "Explode Text")
.expect("Explode Text is offered for a selected text");
assert!(item.enabled);
crate::context_menu::run_item(&mut h.app, &item, egui::Pos2::ZERO);
h.frames(2);
let after = outline(&h);
assert_eq!(after.iter().map(|c| &c.0).collect::<Vec<_>>(), before.iter().map(|c| &c.0).collect::<Vec<_>>(), "every outline stays");
assert!(after.iter().all(|(_, l)| l.is_none()), "no outline belongs to the text");
let sk = h.app.session.doc.sketch(sid).unwrap();
assert!(sk.link(&link).is_none(), "the text is gone");
assert_eq!(h.app.session.world_state().sketch(sid).unwrap().profiles.len(), profiles, "the letters still make the same profiles");
assert_eq!(h.app.session.undo.len(), undo + 1);
// No longer text: nothing to edit or explode.
assert!(selected_entity(&h.app).is_none());
assert!(!crate::context_menu::items(&h.app, &crate::context_menu::Target::Viewport).iter().any(|item| item.label == "Explode Text"));
h.app.run("edit.undo", json!({})).unwrap();
assert_eq!(outline(&h), before, "undo brings the text back");
}
#[test]
fn edit_text_tool_accepts_a_letter_interior_without_preselection() {
let (mut h, link, _) = setup();
+5
View File
@@ -66,6 +66,11 @@ keeps the original text. Position and height use the document's units. The four
expressions: one that names a parameter (`plate_width / 2`) stays an expression, is shown as typed
when the dialog is opened again, and keeps the text following the parameter.
To work on the letters' shapes themselves, right-click the text and choose **Explode Text**
(`sketch.break_link` with the text's `link`). Its outlines stay where they are as ordinary
lines, arcs and curves that can be picked, trimmed, dimensioned and extruded one by one; the
text itself is gone, so it can no longer be edited as text or follow parameters. Undo brings it back.
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