Sketch Text (MTextCmd, sketch.edit_text): glyph outlines of the embedded Liberation Sans (SIL OFL 1.1, attributed) as lines and conics that close into extrudable profiles; editable text, height, position and angle
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
79a4194f6e
commit
2491c40fef
16 files changed
+393
-7
No files matched your search
@@ -3,7 +3,7 @@
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solvecraft_geom::Vec2;
|
||||
use solvecraft_sketch::{ConstraintKind, CurveKind, Sketch};
|
||||
use solvecraft_sketch::{ConstraintKind, CurveKind, LinkKind, LinkSource, Sketch};
|
||||
|
||||
use super::sketch::{add_c, circumcircle, edit, ids_of, mark_construction, req_parg, result, slot};
|
||||
use super::{CommandSpec, in_sketch};
|
||||
@@ -70,8 +70,75 @@ pub static COMMANDS: &[CommandSpec] =
|
||||
.icon("conic")
|
||||
.enabled(in_sketch)
|
||||
.params("start, end, apex: [x,y] or point refs; rho?: 0…1 (default 0.5: parabola)"),
|
||||
CommandSpec::new("MTextCmd", "Text", text)
|
||||
.at("SKETCH", "CREATE")
|
||||
.icon("text")
|
||||
.enabled(in_sketch)
|
||||
.params("text: string (\\n breaks lines), at: baseline start [x,y], height?: capital height mm (default 5), angle?: deg"),
|
||||
CommandSpec::new("sketch.edit_text", "Edit Text", edit_text)
|
||||
.enabled(in_sketch)
|
||||
.params("link: the text's link id (or entity: one of its curves), text?, at?, height?, angle?: deg"),
|
||||
];
|
||||
|
||||
fn text_source(p: &Value, cmd: &str, old: Option<(String, Vec2, f64, f64)>) -> Result<LinkSource> {
|
||||
let (t0, at0, h0, a0) = old.unwrap_or((String::new(), Vec2::ZERO, 5.0, 0.0));
|
||||
let text = str_(p, "text").map(str::to_string).unwrap_or(t0);
|
||||
if text.trim().is_empty() {
|
||||
return Err(bad(cmd, "`text` must not be empty"));
|
||||
}
|
||||
if text.chars().count() > solvecraft_sketch::MAX_TEXT_CHARS {
|
||||
return Err(bad(cmd, "text too long"));
|
||||
}
|
||||
let at = p.get("at").and_then(crate::params::vec2).unwrap_or(at0);
|
||||
let height = num(p, "height").unwrap_or(h0);
|
||||
if !(height > 1e-6 && height < 1e5) {
|
||||
return Err(bad(cmd, "`height` must be positive"));
|
||||
}
|
||||
let angle = num(p, "angle").map(f64::to_radians).unwrap_or(a0);
|
||||
Ok(LinkSource::Text { text, at, height, angle })
|
||||
}
|
||||
|
||||
fn text(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "MTextCmd";
|
||||
if p.get("at").is_none() {
|
||||
return Err(bad(cmd, "`at` must be the baseline start [x, y]"));
|
||||
}
|
||||
let src = text_source(p, cmd, None)?;
|
||||
let id = s.active_sketch.ok_or_else(|| bad(cmd, "no sketch is being edited"))?;
|
||||
let mut doc = (*s.doc).clone();
|
||||
let mut sk = doc.sketch(id)?.clone();
|
||||
let l = super::sketch_project::add_link(s, &doc, id, &mut sk, LinkKind::Text, src)?;
|
||||
let curves = ids_of(&sk, &sk.link_curves(&l));
|
||||
*doc.sketch_mut(id)? = sk;
|
||||
*s.doc_mut() = doc;
|
||||
Ok(json!({"link": l, "curves": curves}))
|
||||
}
|
||||
|
||||
fn edit_text(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "sketch.edit_text";
|
||||
let id = s.active_sketch.ok_or_else(|| bad(cmd, "no sketch is being edited"))?;
|
||||
let mut doc = (*s.doc).clone();
|
||||
let mut sk = doc.sketch(id)?.clone();
|
||||
let l = match (str_(p, "link"), str_(p, "entity")) {
|
||||
(Some(l), _) => l.to_string(),
|
||||
(None, Some(e)) => sk.curves.iter().find(|c| c.id == e).and_then(|c| c.link.clone()).ok_or_else(|| bad(cmd, format!("`{e}` is not text")))?,
|
||||
_ => return Err(bad(cmd, "give `link` or `entity`")),
|
||||
};
|
||||
let old = match sk.link(&l).map(|x| (x.kind, x.source.clone())) {
|
||||
Some((LinkKind::Text, LinkSource::Text { text, at, height, angle })) => (text, at, height, angle),
|
||||
_ => return Err(bad(cmd, format!("`{l}` is not text"))),
|
||||
};
|
||||
let src = text_source(p, cmd, Some(old))?;
|
||||
let LinkSource::Text { text, at, height, angle } = &src else { return Err(bad(cmd, "text")) };
|
||||
let geom = solvecraft_sketch::text_geometry(text, *at, *height, *angle)?;
|
||||
sk.set_link_source(&l, src.clone());
|
||||
sk.update_link(&l, &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}))
|
||||
}
|
||||
|
||||
fn ellipse(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "CircleElipse";
|
||||
let (c, m) = (req_vec2(cmd, p, "center")?, req_vec2(cmd, p, "major")?);
|
||||
|
||||
@@ -260,3 +260,27 @@ fn ellipse_splines_conic() {
|
||||
let r = run(&mut s, "MirrorSketchCommand", json!({"entities": [eid, sid], "line": m}));
|
||||
assert_eq!(ids(&r["curves"]).len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_makes_profiles_that_extrude_and_edit() {
|
||||
let mut s = new_sketch();
|
||||
let r = run(&mut s, "MTextCmd", json!({"text": "SO", "at": [0, 0], "height": 10}));
|
||||
let link = r["link"].as_str().unwrap().to_string();
|
||||
let a = profiles(&s);
|
||||
// S (one region) + O (ring = outer with a hole, plus the counter as its own region).
|
||||
assert!(a.len() >= 2, "{a:?}");
|
||||
let si = run(&mut s, "sketch.inspect", json!({}));
|
||||
assert_eq!(si["dof"], 0);
|
||||
// Edit the text: the outlines change, the link stays.
|
||||
let r2 = run(&mut s, "sketch.edit_text", json!({"link": link, "text": "I", "height": 20}));
|
||||
assert_eq!(r2["link"], json!(link));
|
||||
let a = profiles(&s);
|
||||
assert_eq!(a.len(), 1, "{a:?}");
|
||||
run(&mut s, "SketchStop", json!({}));
|
||||
run(&mut s, "Extrude", json!({"distance": 2}));
|
||||
let m = run(&mut s, "MeasureCommand", json!({}));
|
||||
let v = m["total"]["volume_mm3"].as_f64().unwrap();
|
||||
// An "I" 20 mm tall is a bar a couple of mm wide.
|
||||
assert!(v > 40.0 && v < 200.0, "{v}");
|
||||
assert!(s.execute("MTextCmd", &json!({"text": "", "at": [0, 0]})).is_err());
|
||||
}
|
||||
Reference in new issue
Block a user