mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Keep stroke weight in document space on transformed type
Fixes #3241 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
bflatastic
co-authored by
Claude Haiku 5.5
parent
92a9b91255
commit
4444450a44
@@ -4,14 +4,14 @@
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::{
|
||||
ArrowAlign, Arrowhead, CharStyle, Dash, LineCap, LineJoin, Node, NodeKind, SavedProfile, StrokeAlign, StrokeLayer, Unit, WidthProfile,
|
||||
ArrowAlign, Arrowhead, CharStyle, Dash, LineCap, LineJoin, Node, NodeKind, SavedProfile, Scaling, StrokeAlign, StrokeLayer, Unit, WidthProfile,
|
||||
};
|
||||
|
||||
use super::appearance::{ItemTarget, edit_items, item_target};
|
||||
use super::paint::painted;
|
||||
use super::*;
|
||||
use crate::DocState;
|
||||
use crate::inspect::{ALIGNS, CAPS, JOINS, StrokeMixed, node_stroke};
|
||||
use crate::inspect::{ALIGNS, CAPS, JOINS, StrokeMixed, node_stroke, scale_layer};
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
@@ -171,6 +171,16 @@ impl StrokeChange {
|
||||
self.apply(&mut st);
|
||||
style.set_stroke_layer(&st);
|
||||
}
|
||||
|
||||
/// [`Self::apply_char`] to type drawn at mean scale `k` (its `xf`'s): the weight and dash
|
||||
/// lengths are document-space, the character stroke is drawn in text space.
|
||||
pub(crate) fn apply_char_scaled(&self, style: &mut CharStyle, k: f64) {
|
||||
let mut st = style.stroke_layer();
|
||||
scale_layer(&mut st, k);
|
||||
self.apply(&mut st);
|
||||
scale_layer(&mut st, 1.0 / k);
|
||||
style.set_stroke_layer(&st);
|
||||
}
|
||||
}
|
||||
|
||||
/// Does a Stroke panel edit find nothing selected (and no `ids`)? It then sets up new art.
|
||||
@@ -227,7 +237,9 @@ fn stroke_set(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if index.is_none()
|
||||
&& let NodeKind::Text(t) = &mut n.kind
|
||||
{
|
||||
t.runs.iter_mut().for_each(|r| change.apply_char(&mut r.style));
|
||||
// Character strokes are drawn in text space, scaled by the type's transform.
|
||||
let k = Scaling::factor(t.xf).filter(|k| k.is_finite() && *k > 1e-12).unwrap_or(1.0);
|
||||
t.runs.iter_mut().for_each(|r| change.apply_char_scaled(&mut r.style, k));
|
||||
return Ok(());
|
||||
}
|
||||
if index.is_none() && n.appearance.stroke().is_none() {
|
||||
|
||||
@@ -347,11 +347,26 @@ fn name_of<T: PartialEq>(table: &[(T, &'static str)], v: T) -> &'static str {
|
||||
/// else the topmost stroke of its own (groups and layers too). None without a stroke.
|
||||
pub fn node_stroke(n: &Node) -> Option<StrokeLayer> {
|
||||
match &n.kind {
|
||||
NodeKind::Text(t) => Some(t.runs.first().map_or_else(|| vectorcraft_doc::CharStyle::default().stroke_layer(), |r| r.style.stroke_layer())),
|
||||
NodeKind::Text(t) => {
|
||||
let mut st = t.runs.first().map_or_else(|| vectorcraft_doc::CharStyle::default().stroke_layer(), |r| r.style.stroke_layer());
|
||||
// Character strokes are drawn in text space: report the document-space weight.
|
||||
if let Some(k) = vectorcraft_doc::Scaling::factor(t.xf).filter(|k| k.is_finite()) {
|
||||
scale_layer(&mut st, k);
|
||||
}
|
||||
Some(st)
|
||||
}
|
||||
_ => n.appearance.stroke().cloned(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Scale a stroke's weight and dash lengths by `s`.
|
||||
pub(crate) fn scale_layer(st: &mut StrokeLayer, s: f64) {
|
||||
st.width *= s;
|
||||
if let Some(d) = &mut st.dash {
|
||||
d.scale(s);
|
||||
}
|
||||
}
|
||||
|
||||
/// A stroke's options in `stroke.set` terms: cap, join, miterLimit, align, dash (null when
|
||||
/// solid), dashOffset, alignDashes, startArrow/endArrow (null for none), arrowAlign and profile,
|
||||
/// plus widthPoints: the profile's `[t, left, right]` points (width factors; null when uniform)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
//! A stroke weight set on type is a document-space weight, whether the type was enlarged by its
|
||||
//! font size or by a transform (issue #3241).
|
||||
#![allow(clippy::unwrap_used, clippy::panic)]
|
||||
|
||||
use serde_json::json;
|
||||
use vectorcraft_doc::{NodeId, NodeKind};
|
||||
use vectorcraft_engine::Session;
|
||||
|
||||
const MM: f64 = 72.0 / 25.4;
|
||||
|
||||
fn text(s: &mut Session, x: f64) -> NodeId {
|
||||
NodeId(s.execute("text.create", &json!({"x": x, "y": 100, "text": "Ab", "size": 30.0 * MM})).unwrap()["id"].as_u64().unwrap())
|
||||
}
|
||||
|
||||
/// The weights the type's character strokes draw at in document space.
|
||||
fn drawn(s: &Session, id: NodeId) -> Vec<f64> {
|
||||
let NodeKind::Text(t) = &s.doc().unwrap().doc.node(id).unwrap().kind else { panic!("not type") };
|
||||
let k = t.xf.determinant().abs().sqrt();
|
||||
t.runs.iter().map(|r| r.style.stroke_width * k).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stroke_weight_on_scaled_type_matches_type_resized_by_size() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 800, "height": 600})).unwrap();
|
||||
let a = text(&mut s, 10.0);
|
||||
let b = text(&mut s, 300.0);
|
||||
s.execute("text.setStyle", &json!({"ids": [a.0], "size": 60.0 * MM})).unwrap();
|
||||
s.execute("object.scale", &json!({"ids": [b.0], "sx": 200, "sy": 200})).unwrap();
|
||||
s.execute("stroke.set", &json!({"ids": [a.0, b.0], "weight": MM, "dash": [MM, 2.0 * MM]})).unwrap();
|
||||
for w in drawn(&s, a).into_iter().chain(drawn(&s, b)) {
|
||||
assert!((w - MM).abs() < 1e-9, "drawn stroke weight {w}, want {MM}");
|
||||
}
|
||||
let NodeKind::Text(t) = &s.doc().unwrap().doc.node(b).unwrap().kind else { panic!("not type") };
|
||||
let dash = t.runs[0].style.stroke_dash.as_ref().unwrap().pattern.iter().map(|d| d * 2.0).collect::<Vec<_>>();
|
||||
assert!((dash[0] - MM).abs() < 1e-9 && (dash[1] - 2.0 * MM).abs() < 1e-9, "drawn dash {dash:?}");
|
||||
|
||||
s.execute("select.set", &json!({"ids": [b.0]})).unwrap();
|
||||
let shown = s.shown_stroke().unwrap();
|
||||
assert!((shown.width - MM).abs() < 1e-9, "Stroke panel shows {}, want {MM}", shown.width);
|
||||
assert!((shown.dash.unwrap().pattern[0] - MM).abs() < 1e-9);
|
||||
}
|
||||
Reference in New Issue
Block a user