M4: object styles (create from selection, apply fill/stroke/text frame options/paragraph style)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-01 12:26:53 -04:00
co-authored by Claude Opus 5.5
parent 86588e8347
commit 6eb2cf2f83
2 changed files with 76 additions and 0 deletions
+60
View File
@@ -40,6 +40,8 @@ pub fn specs() -> Vec<CommandSpec> {
"object": st.doc.styles.object.iter().map(|p| &p.name).collect::<Vec<_>>(),
}))
}),
cmd!("style.object.apply", "Apply Object Style", [], None, "{name, ids?}", super::has_selection, apply_object),
cmd!("style.object.create", "New Object Style…", [], None, "{name, fromSelection?: true, fill?, paragraphStyle?}", has_doc, create_object),
cmd!(
"swatch.create",
"New Color Swatch…",
@@ -276,3 +278,61 @@ pub fn parse_color(v: &Value) -> Option<designcraft_color::Color> {
_ => None,
}
}
fn apply_object(s: &mut Session, p: &Value) -> Result<Value> {
let name = str_param(p, "name").ok_or_else(|| bad("style.object.apply", "missing name"))?.to_string();
let os = s.doc()?.doc.styles.object_style(&name).cloned().ok_or_else(|| bad("style.object.apply", format!("no object style `{name}`")))?;
let ids = super::targets(s, p)?;
s.edit(|d, _| {
let mut stories = Vec::new();
for id in &ids {
let Some(it) = d.item_mut(*id) else { continue };
it.object_style = name.clone();
if let Some(f) = &os.fill {
it.fill = f.clone();
}
if let Some(st) = &os.stroke {
it.stroke = st.clone();
}
if let Some(tf) = it.text_frame_mut() {
if let Some(o) = &os.text_frame {
tf.options = o.clone();
}
stories.push(tf.story);
}
}
if let Some(ps) = &os.paragraph_style {
for sid in stories {
if let Some(st) = d.story_mut(sid) {
let len = st.len();
st.format_paras(0..len, |f| f.style = ps.clone());
}
}
}
Ok(Value::Null)
})
}
fn create_object(s: &mut Session, p: &Value) -> Result<Value> {
let base = str_param(p, "name").unwrap_or("Object Style 1").to_string();
let from_sel = p.get("fromSelection").and_then(Value::as_bool).unwrap_or(true);
let src = if from_sel { s.doc()?.selection.items.first().and_then(|i| s.doc().ok()?.doc.item(*i).cloned()) } else { None };
let mut os = designcraft_doc::ObjectStyle::default();
if let Some(it) = &src {
os.fill = Some(it.fill.clone());
os.stroke = Some(it.stroke.clone());
os.text_frame = it.text_frame().map(|t| t.options.clone());
}
if let Some(sw) = str_param(p, "fill") {
os.fill = Some(designcraft_doc::Fill::swatch(sw));
}
if let Some(ps) = str_param(p, "paragraphStyle") {
os.paragraph_style = Some(ps.to_string());
}
s.edit(|d, _| {
let name = Styles::unique_name(|n| d.styles.object_style(n).is_some(), &base);
os.name = name.clone();
d.styles_mut().object.push(os.clone());
Ok(json!({"name": name}))
})
}
+16
View File
@@ -271,6 +271,22 @@ fn snippets_roundtrip_between_documents() {
st.doc.check().unwrap();
}
#[test]
fn object_styles_create_and_apply() {
let mut s = session();
let a = s.execute("frame.create", &json!({"rect": [36, 36, 136, 136], "content": "unassigned"})).unwrap()["id"].as_u64().unwrap();
s.execute("object.fill", &json!({"swatch": "C=0 M=100 Y=0 K=0"})).unwrap();
s.execute("object.stroke", &json!({"weight": 4.0})).unwrap();
s.execute("style.object.create", &json!({"name": "Magenta Box"})).unwrap();
let b = s.execute("frame.create", &json!({"rect": [200, 36, 300, 136], "content": "unassigned"})).unwrap()["id"].as_u64().unwrap();
s.execute("style.object.apply", &json!({"name": "Magenta Box"})).unwrap();
let d = &s.doc().unwrap().doc;
let (ia, ib) = (d.item(designcraft_doc::ItemId(a)).unwrap(), d.item(designcraft_doc::ItemId(b)).unwrap());
assert_eq!(ia.fill, ib.fill);
assert_eq!(ib.stroke.weight, 4.0);
assert_eq!(ib.object_style, "Magenta Box");
}
/// Unicode text per page, through hayro's interpreter (ToUnicode / ActualText).
fn pdf_text(bytes: &[u8]) -> Vec<String> {
use hayro_interpret::font::Glyph;