From b5301ca74d0d926e6750ebc23e3bb287fedee872 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Tue, 3 Nov 2026 18:24:56 -0800 Subject: [PATCH] Fix range text stroke weight defaults and scaling --- crates/engine/src/cmd/textedit.rs | 33 ++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/cmd/textedit.rs b/crates/engine/src/cmd/textedit.rs index 8ce750a5e..df19ec002 100644 --- a/crates/engine/src/cmd/textedit.rs +++ b/crates/engine/src/cmd/textedit.rs @@ -351,6 +351,8 @@ pub(crate) struct CharChange { stroke: Option, /// Weight (`strokeWidth`), cap, join, miter limit and dashes of the character stroke. stroke_opts: StrokeChange, + /// Document points per text-space point for a stroke edit. + stroke_scale: Option, underline: Option, strikethrough: Option, all_caps: Option, @@ -641,6 +643,7 @@ impl CharChange { fill: paint_param(p, "fill", cmd)?, stroke: paint_param(p, "stroke", cmd)?, stroke_opts, + stroke_scale: None, underline: flag("underline"), strikethrough: flag("strikethrough"), all_caps: flag("allCaps"), @@ -723,6 +726,7 @@ impl CharChange { self.tcy_up_down = self.tcy_up_down.map(|v| local_type_value(t, v, (-1296.0, 1296.0), false, cmd)).transpose()?; self.tcy_left_right = self.tcy_left_right.map(|v| local_type_value(t, v, (-1296.0, 1296.0), false, cmd)).transpose()?; self.h_scale = self.h_scale.map(|v| local_type_value(t, v, (1.0, 10000.0), true, cmd)).transpose()?; + self.stroke_scale = Some(vectorcraft_doc::Scaling::factor(t.xf).filter(|k| k.is_finite() && *k > 1e-12).unwrap_or(1.0)); Ok(()) } @@ -809,10 +813,13 @@ impl CharChange { st.fill = v.clone(); } if let Some(v) = &self.stroke { + if !v.is_none() { + super::gradient::run_stroke_weight(st); + } st.stroke = v.clone(); } if !self.stroke_opts.is_empty() { - self.stroke_opts.apply_char(st); + self.stroke_opts.apply_char_scaled(st, self.stroke_scale.unwrap_or(1.0)); } if let Some(v) = self.underline { st.underline = v; @@ -1609,3 +1616,27 @@ fn discard_empty(s: &mut Session, p: &Value) -> Result { s.edit("Discard Empty Type", |d, _| d.remove(id).map(|_| ()).map_err(|_| EngineError::NoNode(id)))?; Ok(json!({"removed": true})) } + +#[cfg(test)] +mod range_stroke_tests { + use super::*; + + #[test] + fn range_stroke_gains_a_default_weight_and_scales_explicit_weight() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); + let id = s.execute("text.create", &json!({"x": 10, "y": 50, "text": "ABCD"})).unwrap()["id"].as_u64().unwrap(); + let weights = |s: &Session| { + let NodeKind::Text(t) = &s.doc().unwrap().doc.node(NodeId(id)).unwrap().kind else { panic!("text missing") }; + t.runs.iter().map(|r| r.style.stroke_width).collect::>() + }; + s.execute("text.setRangeStyle", &json!({"id": id, "start": 0, "end": 2, "stroke": "#ff0000"})).unwrap(); + assert_eq!(weights(&s), [1.0, 0.0], "a newly painted range has a visible default stroke"); + + s.execute("object.scale", &json!({"ids": [id], "sx": 200, "sy": 200})).unwrap(); + s.execute("text.setRangeStyle", &json!({"id": id, "start": 0, "end": 2, "strokeWidth": 2.0})).unwrap(); + assert_eq!(weights(&s), [1.0, 0.0], "two document points become one text-space point at 200% scale"); + s.execute("text.setRangeStyle", &json!({"id": id, "start": 2, "end": 4, "stroke": "#0000ff", "strokeWidth": 4.0})).unwrap(); + assert_eq!(weights(&s), [1.0, 2.0], "a different range receives its scaled weight"); + } +}