feat: a sketch text's height, place and angle can be expressions (#1453)

A sketch text's height, place (x and y) and angle can each be an expression over the design's parameters, and the text follows parameter changes, undo and reopening. Closes #1362.

Co-authored-by: NOWN-DEV <[email protected]>
This commit is contained in:
NOWN-DEVandNOWN-DEV authored and GitHub committed 2026-10-15 14:57:36 -04:00
1 parent e20cf1858a
commit b6806b16cf
23 files changed
+751 -70

No files matched your search

+3
View File
@@ -235,6 +235,9 @@ impl crate::Feature {
mapped_name(n, parameters);
}
}
for e in sketch.text_exprs_mut() {
*e = clone_expression(e, parameters);
}
for link in &mut sketch.links {
remap_link(&mut link.source, ids, names, bodies, 0);
for n in link.face_name.iter_mut().chain(&mut link.edge_name) {
+34
View File
@@ -2792,6 +2792,7 @@ impl Document {
}
if let FeatureKind::Sketch { sketch, .. } = &f.kind {
used.extend(sketch.constraints.iter().filter_map(|c| c.param.clone()));
used.extend(sketch.text_exprs().into_iter().flat_map(|(_, e)| expr::references(e)));
}
}
for p in &self.params {
@@ -2990,6 +2991,39 @@ impl Document {
Ok(())
}
/// Give the texts of a sketch the numbers their expressions come to (in place): a text's
/// height, place and angle that were typed as expressions. A text's outlines are rebuilt from
/// its numbers when the sketch's links are refreshed, so this comes before that. A number
/// typed as a number is left as it is.
pub fn apply_text_values(&self, vals: &std::collections::BTreeMap<String, expr::Value>, sk: &mut Sketch) -> Result<()> {
use crate::eval::val;
for link in &mut sk.links {
let solidcraft_sketch::LinkSource::Text { at, height, angle, exprs, .. } = &mut link.source else { continue };
let number = |e: &Option<String>, kind: Kind, what: &str| -> Result<Option<f64>> {
match e {
None => Ok(None),
Some(e) => val(vals, e, kind).map(Some).map_err(|error| DocError::Expr(format!("{what}: {error}"))),
}
};
if let Some(v) = number(&exprs.x, Kind::Length, "text X")? {
at.x = v;
}
if let Some(v) = number(&exprs.y, Kind::Length, "text Y")? {
at.y = v;
}
if let Some(v) = number(&exprs.height, Kind::Length, "text height")? {
*height = v;
}
if let Some(v) = number(&exprs.angle, Kind::Angle, "text angle")? {
*angle = v;
}
if exprs.height.is_some() && !(*height > 1e-6 && *height < 1e5) {
return Err(DocError::Invalid("a text's height must be positive".into()));
}
}
Ok(())
}
/// Apply parameter values to a sketch's dimensions and solve it. `stored` are the values
/// its geometry is solved for (the stored sketch's): what the solve cannot reach in one
/// jump it reaches by stepping the changed values from there, and an edit that would turn
+7 -2
View File
@@ -737,11 +737,13 @@ pub struct Model {
pub no_cache: bool,
}
/// Parameters a feature uses directly (inputs and sketch dimensions), sorted.
/// Parameters a feature uses directly (inputs, sketch dimensions and the expressions of a
/// sketch's texts), sorted.
fn param_names(f: &Feature) -> Vec<String> {
let mut names: Vec<String> = f.kind.expressions().iter().flat_map(|e| expr::references(e)).collect();
if let FeatureKind::Sketch { sketch, .. } = &f.kind {
names.extend(sketch.constraints.iter().filter_map(|c| c.param.clone()));
names.extend(sketch.text_exprs().into_iter().flat_map(|(_, e)| expr::references(e)));
}
names.sort();
names.dedup();
@@ -1307,7 +1309,7 @@ fn point(vals: &BTreeMap<String, Value>, p: &crate::PointExpr) -> Result<Vec3> {
if [v.x, v.y, v.z].iter().all(|c| c.is_finite() && c.abs() < 1e9) { Ok(v) } else { Err(DocError::Invalid("the position is out of range".into())) }
}
fn val(vals: &BTreeMap<String, Value>, e: &str, k: Kind) -> Result<f64> {
pub(crate) fn val(vals: &BTreeMap<String, Value>, e: &str, k: Kind) -> Result<f64> {
let scale = vals.get(DESIGN_LEN).map(|x| x.v).unwrap_or(1.0);
if k == Kind::Length && scale != 1.0 && !expr::is_literal(e) {
let v = expr::eval_with(e, &|n| vals.get(n).copied().ok_or_else(|| DocError::Expr(format!("unknown parameter `{n}`"))))?;
@@ -4082,6 +4084,9 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
(None, other) => (doc.resolve_plane_in(vals, other, 0, Some(&fst))?, None),
};
let mut sk = sketch.clone();
// A text's outlines are rebuilt from its numbers when the links are refreshed: the
// ones its expressions give come first.
doc.apply_text_values(vals, &mut sk)?;
let mut warns = match anchor {
Some(anchor) => crate::project::refresh_face_anchor_links(doc, vals, &fst, &plane, &mut sk, anchor),
None => crate::project::refresh_links(doc, vals, &fst, &plane, &mut sk),
+5
View File
@@ -217,6 +217,11 @@
//! `crown` (0 by default, and then not saved: a bump that vanishes on the loop, so the face
//! still meets the picked edges). Version 21 designs need no transformation; 0.12.0 asks for an
//! update instead of meeting a feature it can't read or dropping a patch's crown.
//! Also under 22, a sketch text's link source can say that its place, height or angle are
//! expressions (`exprs`: `x`, `y`, `height`, `angle`, each an expression over the design's
//! parameters); the source's `at`, `height` and `angle` then hold the numbers they last came
//! to. A text without `exprs` is set as it always was; 0.12.0 asks for an update instead of
//! dropping the expressions and keeping the text at its last numbers.
use serde_json::Value;
+14
View File
@@ -584,6 +584,11 @@ impl Document {
out.push(ParamUser::Dimension { feature: f.id, name: f.name.clone(), constraint: c.id.clone() });
}
}
for (what, e) in sketch.text_exprs() {
if expr::references(e).iter().any(|r| r == name) {
out.push(ParamUser::Feature { feature: f.id, name: f.name.clone(), input: what.to_string() });
}
}
}
}
out
@@ -662,6 +667,13 @@ impl Document {
n += 1;
}
}
for e in sketch.text_exprs_mut() {
let r = expr::rename_reference(e, old, new);
if r != *e {
*e = r;
n += 1;
}
}
}
}
if self.favorites.remove(old) {
@@ -717,6 +729,7 @@ impl Document {
let mut names: BTreeSet<String> = f.param_names.iter().filter(|n| !n.is_empty()).cloned().collect();
if let FeatureKind::Sketch { sketch, .. } = &f.kind {
names.extend(sketch.constraints.iter().filter_map(|c| c.param.clone()));
names.extend(sketch.text_exprs().into_iter().flat_map(|(_, e)| expr::references(e)));
}
out.extend(names.into_iter().map(|n| (format!("Feature:{}", f.name), n)));
}
@@ -744,6 +757,7 @@ impl Document {
hit |= f.kind.expressions().iter().any(|e| expr::references(e).iter().any(|r| affected.contains(r)));
if let FeatureKind::Sketch { sketch, .. } = &f.kind {
hit |= sketch.constraints.iter().any(|c| c.param.as_ref().is_some_and(|p| affected.contains(p)));
hit |= sketch.text_exprs().iter().any(|(_, e)| expr::references(e).iter().any(|r| affected.contains(r)));
}
if hit {
out.push(f.id);
+1 -1
View File
@@ -255,7 +255,7 @@ fn resolve_uncached(
LinkSource::SketchAxis { name } => {
vec![solidcraft_sketch::sketch_axis_geom(name).ok_or_else(|| DocError::Unknown(format!("sketch axis `{name}`")))?]
}
LinkSource::Text { text, at, height, angle, font, style } if style.path.is_none() => {
LinkSource::Text { text, at, height, angle, font, style, .. } if style.path.is_none() => {
solidcraft_sketch::text_geometry_styled(font.as_deref(), text, *at, *height, *angle, style, None)?
}
// Its path is a curve of its own sketch: set with the sketch (`Sketch::text_source_geometry`).