diff --git a/crates/engine/src/cmd/textedit.rs b/crates/engine/src/cmd/textedit.rs index 8ce750a5e..ac0af5867 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; diff --git a/crates/engine/src/tests/charstroke.rs b/crates/engine/src/tests/charstroke.rs index ba948cb66..2c34e697e 100644 --- a/crates/engine/src/tests/charstroke.rs +++ b/crates/engine/src/tests/charstroke.rs @@ -91,6 +91,21 @@ fn range_stroke_options_split_runs() { assert!(s.execute("text.setRangeStyle", &json!({"id": id.0, "strokeOptions": 3})).is_err()); } +#[test] +fn range_stroke_gains_a_default_weight_and_scales_explicit_weight() { + let mut s = session(); + let id = NodeId(run(&mut s, "text.create", json!({"x": 10, "y": 50, "text": "ABCD"}))["id"].as_u64().unwrap()); + let weights = |s: &Session| text(s, id).runs.iter().map(|r| r.style.stroke_width).collect::>(); + run(&mut s, "text.setRangeStyle", json!({"id": id.0, "start": 0, "end": 2, "stroke": "#ff0000"})); + assert_eq!(weights(&s), [1.0, 0.0], "a newly painted range has a visible default stroke"); + + run(&mut s, "object.scale", json!({"ids": [id.0], "sx": 200, "sy": 200})); + run(&mut s, "text.setRangeStyle", json!({"id": id.0, "start": 0, "end": 2, "strokeWidth": 2.0})); + assert_eq!(weights(&s), [1.0, 0.0], "two document points become one text-space point at 200% scale"); + run(&mut s, "text.setRangeStyle", json!({"id": id.0, "start": 2, "end": 4, "stroke": "#0000ff", "strokeWidth": 4.0})); + assert_eq!(weights(&s), [1.0, 2.0], "a different range receives its scaled weight"); +} + #[test] fn eyedropper_and_outlines_carry_the_character_stroke() { let mut s = session(); diff --git a/docs/mcp.md b/docs/mcp.md index 8a2551ffc..5fab5134a 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -2404,6 +2404,8 @@ dash?, dashOffset?, alignDashes?}}`. `text.setStyle {ids?, stroke?, strokeWidth? and weight of the characters of each object given (without `start` and `end`: all of its text), with its other keys in one undo step; `strokeWidth` is in document points, as `stroke.set` gives the weight of scaled type. A `stroke` color without `strokeWidth` gives characters that have no stroke weight 1 pt, as `paint.setStroke` does. +`text.setRangeStyle` does the same for its range, and its `strokeWidth` and `strokeOptions` lengths are in document +points too. The 1 pt default of both commands is in the type's own units, so on type scaled to 200% it draws 2 pt wide. `align: "inside"|"outside"` keeps the characters' stroke on that side of the glyph outlines, drawn at twice the weight and clipped, while the type stays editable (beyond Illustrator); SVG export writes the text live and that stroke as glyph outlines over it. `inspect_document` reports each object's stroke as `strokeOptions` (type: its first