diff --git a/assets/locales/de.json b/assets/locales/de.json index 50cd7a9d..73b59ba0 100644 --- a/assets/locales/de.json +++ b/assets/locales/de.json @@ -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…", diff --git a/assets/locales/en.json b/assets/locales/en.json index b9897704..f29d6140 100644 --- a/assets/locales/en.json +++ b/assets/locales/en.json @@ -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…", diff --git a/assets/locales/ja.json b/assets/locales/ja.json index a9483a34..823200df 100644 --- a/assets/locales/ja.json +++ b/assets/locales/ja.json @@ -98,6 +98,7 @@ "Enable rule": "ルールを有効化", "Enabled": "有効", "Explode": "分解", + "Explode Text": "テキストを分解", "Export Animation…": "アニメーションをエクスポート…", "Export Current View as DXF": "現在の表示をDXFでエクスポート", "Export Current View as DXF…": "現在の表示をDXFでエクスポート…", diff --git a/assets/locales/ko.json b/assets/locales/ko.json index 43a55ee1..f6438fef 100644 --- a/assets/locales/ko.json +++ b/assets/locales/ko.json @@ -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로 내보내기…", diff --git a/assets/locales/zh.json b/assets/locales/zh.json index df2c00d6..1dac15f9 100644 --- a/assets/locales/zh.json +++ b/assets/locales/zh.json @@ -98,6 +98,7 @@ "Enable rule": "启用规则", "Enabled": "已启用", "Explode": "爆炸", + "Explode Text": "分解文字", "Export Animation…": "导出动画…", "Export Current View as DXF": "将当前视图导出为DXF", "Export Current View as DXF…": "将当前视图导出为DXF…", diff --git a/crates/ui-egui/src/context_menu.rs b/crates/ui-egui/src/context_menu.rs index 5bd9d269..db0ad1ad 100644 --- a/crates/ui-egui/src/context_menu.rs +++ b/crates/ui-egui/src/context_menu.rs @@ -719,6 +719,13 @@ fn component_items(app: &SolveApp, id: u64) -> Vec { 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 { + 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 { @@ -754,6 +761,7 @@ fn sketch_entity_items(app: &SolveApp) -> Vec { 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 { } 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")); diff --git a/crates/ui-egui/src/sketch_text.rs b/crates/ui-egui/src/sketch_text.rs index e03df889..3eb30bdc 100644 --- a/crates/ui-egui/src/sketch_text.rs +++ b/crates/ui-egui/src/sketch_text.rs @@ -183,6 +183,13 @@ pub(crate) fn selected_entity(app: &SolveApp) -> Option { selected } +/// The link of the one text the selection belongs to (see [`selected_entity`]). +pub(crate) fn selected_link(app: &SolveApp) -> Option { + 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 { 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)> = 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::>(), before.iter().map(|c| &c.0).collect::>(), "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(); diff --git a/docs/book/src/guide/sketching.md b/docs/book/src/guide/sketching.md index 3ca3c4ef..18f82b38 100644 --- a/docs/book/src/guide/sketching.md +++ b/docs/book/src/guide/sketching.md @@ -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