mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
G4: Scale Text Attributes bakes transform scale into type sizes
After scaling groups, type.scaleTextAttributes matches panel sizes to what draws on canvas without leaving scale in the affine. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
committed by
bflatastic
co-authored by
Cursor
parent
240d8d4891
commit
07a49d447d
@@ -1402,6 +1402,36 @@ impl TextObject {
|
||||
Some(style)
|
||||
}
|
||||
|
||||
/// Bakes the object's scale ([`style_scale`]) into character attributes and removes that
|
||||
/// scale from `xf`, so panels show document-point sizes without a separate transform factor.
|
||||
/// Returns `false` when there is nothing to bake (identity scale or an unrepresentable
|
||||
/// transform).
|
||||
pub fn scale_text_attributes(&mut self) -> bool {
|
||||
let Some(scale) = self.style_scale() else { return false };
|
||||
if (scale.points - 1.0).abs() <= 1e-12 && (scale.horizontal - 1.0).abs() <= 1e-12 {
|
||||
return false;
|
||||
}
|
||||
let [a, b, c, d, _, _] = self.xf.as_coeffs();
|
||||
let (sx, sy) = (a.hypot(b), c.hypot(d));
|
||||
if sx <= 1e-12 || sy <= 1e-12 {
|
||||
return false;
|
||||
}
|
||||
let mut baked = Vec::with_capacity(self.runs.len());
|
||||
for run in &self.runs {
|
||||
let Some(eff) = self.effective_char_style(&run.style) else { return false };
|
||||
baked.push(eff);
|
||||
}
|
||||
for (run, style) in self.runs.iter_mut().zip(baked) {
|
||||
run.style = style;
|
||||
}
|
||||
// Character strokes are drawn in text space: widen them by the vertical scale we remove.
|
||||
self.scale_char_strokes(1.0 / sy);
|
||||
self.xf *= Affine::scale_non_uniform(1.0 / sx, 1.0 / sy);
|
||||
self.cached_bounds = None;
|
||||
self.cached_baselines.clear();
|
||||
true
|
||||
}
|
||||
|
||||
pub fn point(origin: Point, text: &str, style: CharStyle) -> Self {
|
||||
Self {
|
||||
vertical: false,
|
||||
|
||||
@@ -34,6 +34,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
has_selection,
|
||||
create_outlines
|
||||
),
|
||||
cmd!(
|
||||
"type.scaleTextAttributes",
|
||||
"Scale Text Attributes",
|
||||
["Type"],
|
||||
None,
|
||||
"{ids?|id?} bake each selected type object's transform scale into its character sizes (leading, baseline shift and horizontal scale included), leaving rotation and position in the affine → {ids, changed}",
|
||||
scale_text_attributes_enabled,
|
||||
scale_text_attributes
|
||||
),
|
||||
cmd!(
|
||||
"type.opticalMarginAlignment",
|
||||
"Optical Margin Alignment",
|
||||
@@ -386,6 +395,42 @@ fn text_ids(d: &Document, ids: &[NodeId]) -> Vec<NodeId> {
|
||||
out
|
||||
}
|
||||
|
||||
fn scale_text_attributes_enabled(s: &Session) -> std::result::Result<(), String> {
|
||||
has_selection(s)?;
|
||||
let st = s.active().ok_or("no document open")?;
|
||||
let ids = text_ids(&st.doc, &st.selection.objects);
|
||||
if ids.iter().any(|id| {
|
||||
st.doc.node(*id).is_some_and(|n| {
|
||||
let NodeKind::Text(t) = &n.kind else { return false };
|
||||
t.style_scale().is_some_and(|sc| (sc.points - 1.0).abs() > 1e-12 || (sc.horizontal - 1.0).abs() > 1e-12)
|
||||
})
|
||||
}) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Scale Text Attributes: selected type has no scale to bake".into())
|
||||
}
|
||||
}
|
||||
|
||||
fn scale_text_attributes(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "type.scaleTextAttributes";
|
||||
let ids = text_targets(s, p, C)?;
|
||||
let changed = s.edit("Scale Text Attributes", |d, _| {
|
||||
let mut n = 0usize;
|
||||
for id in &ids {
|
||||
let Some(NodeKind::Text(t)) = d.node_mut(*id).map(|node| &mut node.kind) else { continue };
|
||||
if t.scale_text_attributes() {
|
||||
refresh_bounds(t);
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
if n == 0 {
|
||||
return Err(EngineError::Other("Scale Text Attributes: selected type has no scale to bake".into()));
|
||||
}
|
||||
Ok(n)
|
||||
})?;
|
||||
Ok(json!({ "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>(), "changed": changed }))
|
||||
}
|
||||
|
||||
fn create_outlines(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let roots = selected_roots(s)?;
|
||||
let ids = s.edit("Create Outlines", |d, sel| {
|
||||
|
||||
@@ -300,3 +300,24 @@ fn imported_type_with_sizes_outside_the_panels_range_opens_unchanged() {
|
||||
s.execute("document.open", &json!({"name": "sizes.vectorcraft", "dataBase64": saved})).unwrap();
|
||||
assert_eq!(texts(&s), imported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scale_text_attributes_bakes_group_scale_and_undoes() {
|
||||
let mut s = session();
|
||||
let id = create(&mut s, 12.0);
|
||||
s.execute("object.group", &json!({})).unwrap();
|
||||
s.execute("object.scale", &json!({"sx": 200})).unwrap();
|
||||
near(em(&text(&s, id), 0).1, 24.0);
|
||||
assert_eq!(text(&s, id).runs[0].style.size, 12.0);
|
||||
s.execute("type.scaleTextAttributes", &json!({})).unwrap();
|
||||
near(em(&text(&s, id), 0).1, 24.0);
|
||||
near(text(&s, id).runs[0].style.size, 24.0);
|
||||
assert!(text(&s, id).style_scale().is_none_or(|sc| (sc.points - 1.0).abs() <= 1e-9 && (sc.horizontal - 1.0).abs() <= 1e-9));
|
||||
s.execute("text.setStyle", &json!({"size": 10})).unwrap();
|
||||
near(em(&text(&s, id), 0).1, 10.0);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
near(em(&text(&s, id), 0).1, 24.0);
|
||||
near(text(&s, id).runs[0].style.size, 24.0);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
near(text(&s, id).runs[0].style.size, 12.0);
|
||||
}
|
||||
|
||||
@@ -2887,6 +2887,7 @@ pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec<Item>)> {
|
||||
cp("Smart Punctuation…", "type.smartPunctuation", json!({"quotes": true, "dashes": true, "ellipsis": true, "scope": "selection"})),
|
||||
Sep,
|
||||
c("Create Outlines", "type.createOutlines"),
|
||||
c("Scale Text Attributes", "type.scaleTextAttributes"),
|
||||
c("Optical Margin Alignment", "type.opticalMarginAlignment"),
|
||||
Sep,
|
||||
c("Fill with Placeholder Text", "type.fillPlaceholder"),
|
||||
|
||||
Reference in New Issue
Block a user