mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 21:57:12 -06:00
G4: System Layout spaces every text object in whole points
Squashed from #3412 onto main.
This commit is contained in:
@@ -441,6 +441,7 @@ fn import_text(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Err(bad(C, "the text is empty"));
|
||||
}
|
||||
let mut t = TextObject::point(Point::ZERO, text, super::super::create::new_type_style(s, &Value::Null));
|
||||
t.system_layout = s.doc()?.doc.setup.system_layout;
|
||||
crate::cmd::typecmd::refresh_bounds(&mut t);
|
||||
let n = Node::new(NodeId(1), NodeKind::Text(Box::new(t)));
|
||||
let count = s.load_clipboard(Clipboard { nodes: vec![n], ..Default::default() }, p)?;
|
||||
|
||||
@@ -506,6 +506,7 @@ fn text_create(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
t.kind = vectorcraft_doc::TextKind::Area { frame: shapes::rectangle(Rect::new(0.0, 0.0, w, h)) };
|
||||
t.area.fit = new_area_fit(s, a, "text.create")?;
|
||||
}
|
||||
t.system_layout = s.doc()?.doc.setup.system_layout;
|
||||
if bool_or(p, "placeholder", false) {
|
||||
super::typemenu::fill_with_placeholder(&mut t);
|
||||
} else {
|
||||
|
||||
@@ -353,6 +353,7 @@ fn artwork(doc: &mut Document, d: &Dimension, old: &[NodeId]) -> Result<Vec<Arc<
|
||||
};
|
||||
let mut text = TextObject::point(Point::ZERO, &label, style);
|
||||
text.para.justify = vectorcraft_doc::Justify::Center;
|
||||
text.system_layout = doc.setup.system_layout;
|
||||
super::typecmd::refresh_bounds(&mut text);
|
||||
// Centre the label's box on its anchor point, along the line.
|
||||
let bounds = text.cached_bounds.unwrap_or_else(|| text.estimate_bounds());
|
||||
|
||||
@@ -373,6 +373,7 @@ impl Gen<'_> {
|
||||
let style = label_style(self.styles, role);
|
||||
let mut t = TextObject::point(at, s, style);
|
||||
t.para.justify = justify;
|
||||
t.system_layout = self.d.setup.system_layout;
|
||||
refresh_bounds(&mut t);
|
||||
Arc::new(Node::new(self.d.alloc_id(), NodeKind::Text(Box::new(t))))
|
||||
}
|
||||
|
||||
@@ -228,6 +228,7 @@ pub(super) fn area_text(story: Story, frame: Rect, setup: &vectorcraft_doc::DocS
|
||||
t.set_paragraph_styles(story.paras);
|
||||
}
|
||||
t.kind = vectorcraft_doc::TextKind::Area { frame: shapes::rectangle(Rect::from_origin_size(Point::ORIGIN, frame.size())) };
|
||||
t.system_layout = setup.system_layout;
|
||||
crate::cmd::typecmd::refresh_bounds(&mut t);
|
||||
t
|
||||
}
|
||||
|
||||
@@ -862,6 +862,7 @@ fn create_in_path(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
path_spacing: 0.0,
|
||||
wrap: Vec::new(),
|
||||
optical_margin: false,
|
||||
system_layout: false,
|
||||
fractional_widths: true,
|
||||
cached_bounds: None,
|
||||
cached_baselines: Vec::new(),
|
||||
@@ -870,6 +871,7 @@ fn create_in_path(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if !on_path {
|
||||
t.area.fit = super::create::new_area_fit(s, p, C)?;
|
||||
}
|
||||
t.system_layout = s.doc()?.doc.setup.system_layout;
|
||||
if bool_or(p, "placeholder", false) {
|
||||
super::typemenu::fill_with_placeholder(&mut t);
|
||||
} else {
|
||||
@@ -1085,7 +1087,8 @@ fn headline_tracking(t: &TextObject, target: f64) -> Option<f64> {
|
||||
path_spacing: 0.0,
|
||||
wrap: Vec::new(),
|
||||
optical_margin: false,
|
||||
fractional_widths: true,
|
||||
system_layout: t.system_layout,
|
||||
fractional_widths: t.fractional_widths,
|
||||
cached_bounds: None,
|
||||
cached_baselines: Vec::new(),
|
||||
};
|
||||
|
||||
@@ -205,7 +205,7 @@ pub(crate) fn with_threads(d: &Document, ids: &[NodeId]) -> Vec<NodeId> {
|
||||
}
|
||||
|
||||
/// A closed path converted to an empty area-type frame with `style`.
|
||||
fn frame_from_path(n: &Node, style: vectorcraft_doc::CharStyle) -> Option<Node> {
|
||||
fn frame_from_path(n: &Node, style: vectorcraft_doc::CharStyle, system_layout: bool) -> Option<Node> {
|
||||
let NodeKind::Path { path, .. } = &n.kind else { return None };
|
||||
if !path.subpaths.first().is_some_and(|s| s.closed) {
|
||||
return None;
|
||||
@@ -213,6 +213,7 @@ fn frame_from_path(n: &Node, style: vectorcraft_doc::CharStyle) -> Option<Node>
|
||||
let mut t = TextObject::point(vectorcraft_geom::Point::ZERO, "", style);
|
||||
t.kind = TextKind::Area { frame: path.clone() };
|
||||
t.xf = vectorcraft_geom::Affine::IDENTITY;
|
||||
t.system_layout = system_layout;
|
||||
refresh_bounds(&mut t);
|
||||
Some(Node::new(n.id, NodeKind::Text(Box::new(t))))
|
||||
}
|
||||
@@ -222,7 +223,8 @@ fn create(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let ids = selected_roots(s)?;
|
||||
let d = &s.doc()?.doc;
|
||||
let style = ids.iter().find_map(|i| text_of(d, *i)).map(|t| t.first_style()).unwrap_or_default();
|
||||
let ok = ids.iter().all(|i| text_of(d, *i).is_some() || d.node(*i).is_some_and(|n| frame_from_path(n, style.clone()).is_some()));
|
||||
let system = d.setup.system_layout;
|
||||
let ok = ids.iter().all(|i| text_of(d, *i).is_some() || d.node(*i).is_some_and(|n| frame_from_path(n, style.clone(), system).is_some()));
|
||||
if ids.len() < 2 || !ok || !ids.iter().any(|i| text_of(d, *i).is_some()) {
|
||||
return Err(bad(C, "select area type and closed paths (at least one area type object)"));
|
||||
}
|
||||
@@ -231,7 +233,7 @@ fn create(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
for id in &ids {
|
||||
if text_of(d, *id).is_none() {
|
||||
let n = d.node(*id).cloned().ok_or(EngineError::NoNode(*id))?;
|
||||
let f = frame_from_path(&n, style.clone()).ok_or_else(|| bad(C, "not a closed path"))?;
|
||||
let f = frame_from_path(&n, style.clone(), d.setup.system_layout).ok_or_else(|| bad(C, "not a closed path"))?;
|
||||
*d.node_mut(*id).ok_or(EngineError::NoNode(*id))? = f;
|
||||
}
|
||||
}
|
||||
@@ -271,7 +273,8 @@ fn add(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
(Some(v), None, None) => {
|
||||
let to = checked_id(s, v, C)?;
|
||||
let parent = d.parent_of(to).and_then(|i| d.node(i));
|
||||
let frame = text_of(d, to).is_some() || d.node(to).is_some_and(|n| frame_from_path(n, style.clone()).is_some());
|
||||
let system = d.setup.system_layout;
|
||||
let frame = text_of(d, to).is_some() || d.node(to).is_some_and(|n| frame_from_path(n, style.clone(), system).is_some());
|
||||
if to == from || !frame || parent.is_some_and(|n| matches!(n.kind, NodeKind::Compound { .. })) {
|
||||
return Err(bad(C, "`to` must be a closed path, area type or type on a path other than `from`"));
|
||||
}
|
||||
@@ -316,7 +319,8 @@ fn add(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
None => {
|
||||
if text_of(d, to).is_none() {
|
||||
let n = d.node(to).cloned().ok_or(EngineError::NoNode(to))?;
|
||||
*d.node_mut(to).ok_or(EngineError::NoNode(to))? = frame_from_path(&n, style).ok_or_else(|| bad(C, "not a closed path"))?;
|
||||
*d.node_mut(to).ok_or(EngineError::NoNode(to))? =
|
||||
frame_from_path(&n, style, d.setup.system_layout).ok_or_else(|| bad(C, "not a closed path"))?;
|
||||
}
|
||||
to
|
||||
}
|
||||
|
||||
@@ -52,6 +52,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
has_selection,
|
||||
optical_margin_alignment
|
||||
),
|
||||
cmd!(
|
||||
"type.systemLayout",
|
||||
"System Layout",
|
||||
[],
|
||||
None,
|
||||
"{on?: bool} turn System Layout on or off for the document (on omitted: the opposite of the document's setting) and every text object in it. On, each character that takes any space advances by a whole number of points, at least one, so small type does not run together; a character with no width stays at none. Off (the default, and what a document from before it reads as), spacing uses fractions of a point. New point, area and path type inherits the document's setting → {on}",
|
||||
has_doc,
|
||||
system_layout
|
||||
),
|
||||
cmd!(
|
||||
"type.fractionalWidths",
|
||||
"Fractional Widths",
|
||||
@@ -155,6 +164,31 @@ fn optical_margin_alignment(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({"on": on, "ids": ids.iter().map(|id| id.0).collect::<Vec<_>>()}))
|
||||
}
|
||||
|
||||
/// Character panel menu › System Layout: set `on` (without it, the opposite of the document's
|
||||
/// setting) on the document and every text object, in one undo step. Off is the default.
|
||||
fn system_layout(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let current = s.doc()?.doc.setup.system_layout;
|
||||
let on = p.get("on").and_then(Value::as_bool).unwrap_or(!current);
|
||||
s.edit("System Layout", |d, _| {
|
||||
d.setup.system_layout = on;
|
||||
let mut ids = vec![];
|
||||
d.walk(|n| {
|
||||
if matches!(n.kind, NodeKind::Text(_)) {
|
||||
ids.push(n.id);
|
||||
}
|
||||
});
|
||||
for id in ids {
|
||||
let Some(NodeKind::Text(t)) = d.node_mut(id).map(|n| &mut n.kind) else {
|
||||
continue;
|
||||
};
|
||||
t.system_layout = on;
|
||||
refresh_bounds(t);
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(json!({ "on": on }))
|
||||
}
|
||||
|
||||
/// Character panel menu › Fractional Widths: set `on` (without it, the opposite of the first
|
||||
/// object's setting) on every selected text object and every frame of their threads, in one undo
|
||||
/// step. On is the default.
|
||||
@@ -1167,6 +1201,55 @@ mod area_tests {
|
||||
assert!(!serde_json::from_value::<TextObject>(old).unwrap().optical_margin);
|
||||
}
|
||||
|
||||
/// Character panel menu › System Layout: `type.systemLayout` sets the document and every text
|
||||
/// object in one undo step. New type inherits it. Off by default and in a document from before
|
||||
/// the flag, and saved only when on. On, each positive advance is a whole number of points.
|
||||
#[test]
|
||||
fn system_layout_sets_the_document_and_every_text_object() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
|
||||
let id = s.execute("text.create", &json!({"x": 10, "y": 40, "size": 13, "text": "Hi"})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("text.setStyle", &json!({"id": id, "tracking": 50})).unwrap();
|
||||
let other = s.execute("text.create", &json!({"x": 10, "y": 80, "text": "Yo"})).unwrap()["id"].as_u64().unwrap();
|
||||
let text = |s: &Session, id: u64| match &s.doc().unwrap().doc.node(NodeId(id)).unwrap().kind {
|
||||
NodeKind::Text(t) => (**t).clone(),
|
||||
_ => panic!("text"),
|
||||
};
|
||||
let setup = |s: &Session| s.doc().unwrap().doc.setup.clone();
|
||||
let advances = |s: &Session| {
|
||||
vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &text(s, id)).glyphs.iter().map(|g| g.advance).collect::<Vec<_>>()
|
||||
};
|
||||
assert!(!setup(&s).system_layout);
|
||||
assert!(!text(&s, id).system_layout && !text(&s, other).system_layout);
|
||||
assert!(serde_json::to_value(setup(&s)).unwrap().get("systemLayout").is_none());
|
||||
assert!(serde_json::to_value(text(&s, id)).unwrap().get("systemLayout").is_none());
|
||||
let fractional = advances(&s);
|
||||
assert!(fractional.iter().any(|a| (a - a.round()).abs() > 1e-6), "{fractional:?}");
|
||||
assert_eq!(s.execute("type.systemLayout", &json!({})).unwrap()["on"], true);
|
||||
assert!(setup(&s).system_layout);
|
||||
assert!(text(&s, id).system_layout && text(&s, other).system_layout);
|
||||
let whole = advances(&s);
|
||||
assert!(whole.iter().all(|a| *a >= 1.0 && (a - a.round()).abs() < 1e-9), "{whole:?}");
|
||||
assert!(fractional.iter().zip(&whole).any(|(a, b)| (a - b).abs() > 0.05), "{fractional:?} {whole:?}");
|
||||
assert_eq!(serde_json::to_value(setup(&s)).unwrap()["systemLayout"], true);
|
||||
assert_eq!(serde_json::to_value(text(&s, id)).unwrap()["systemLayout"], true);
|
||||
// New type inherits the document's setting.
|
||||
let made = s.execute("text.create", &json!({"x": 10, "y": 120, "text": "Z"})).unwrap()["id"].as_u64().unwrap();
|
||||
assert!(text(&s, made).system_layout);
|
||||
// A boolean given as text is ignored, so the setting toggles off.
|
||||
assert_eq!(s.execute("type.systemLayout", &json!({"on": "no"})).unwrap()["on"], false);
|
||||
assert!(!setup(&s).system_layout && !text(&s, id).system_layout && !text(&s, made).system_layout);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(setup(&s).system_layout && text(&s, id).system_layout);
|
||||
let back = vectorcraft_format::load(&vectorcraft_format::save(&s.doc().unwrap().doc, false)).unwrap();
|
||||
assert!(back.setup.system_layout);
|
||||
let NodeKind::Text(t) = &back.node(NodeId(id)).unwrap().kind else { panic!("text") };
|
||||
assert!(t.system_layout);
|
||||
let mut old = serde_json::to_value(setup(&s)).unwrap();
|
||||
old.as_object_mut().unwrap().remove("systemLayout");
|
||||
assert!(!serde_json::from_value::<vectorcraft_doc::DocSetup>(old).unwrap().system_layout);
|
||||
}
|
||||
|
||||
/// Character panel menu › Fractional Widths: `type.fractionalWidths` sets or toggles the flag
|
||||
/// on the selected type in one undo step. New type and a document from before the flag are on,
|
||||
/// and the flag is saved only when off. Off, each positive advance is a whole number of points.
|
||||
|
||||
Reference in New Issue
Block a user