Paragraph style edits: Reset To Base with a new Based On resolves over it, is checked against it and previewed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
paaspaas00andClaude Opus 5.5 committed 2026-10-21 13:47:05 +02:00
1 parent 151f54ee46
commit f972cebb96
4 files changed
+72 -9

No files matched your search

+1 -1
View File
@@ -11,7 +11,7 @@
DesignCraft aims at full Adobe InDesign parity — and to be better: faster, open (documented JSON format + IDML), scriptable by agents (MCP), and available on the web.
## Status (2026-10-01)
## Status (2026-10-21)
**Working today**
- Document model: facing/non-facing spreads, parent pages with page-number markers, sections, layers, frames (text / graphic / unassigned), threaded stories, paragraph/character/object styles with based-on, swatches (process/spot/tints/gradients), corner options, text wrap, drop shadow, opacity/blend.
+9 -5
View File
@@ -955,6 +955,15 @@ fn edit_para(s: &mut Session, p: &Value) -> Result<Value> {
st.para = ParaAttrs::default();
st.chars = CharAttrs::default();
}
// A new Based On first, so the fields below resolve over the new parent.
if based.is_some() {
let st = d.styles_mut().para_mut(&name).ok_or_else(|| bad("style.paragraph.edit", format!("no style `{name}`")))?;
match &based {
Some(Value::String(b)) => st.based_on = Some(b.clone()),
Some(Value::Null) => st.based_on = None,
_ => {}
}
}
// A rule object changes only the rule fields it names, over the style's resolved rule.
let (current, _) = d.styles.resolve_para_style(&name);
let mut para: ParaAttrs = attrs("style.paragraph.edit", "para", p, |a: &mut ParaAttrs, k, v| a.set_json_over(k, v, &current))?;
@@ -964,11 +973,6 @@ fn edit_para(s: &mut Session, p: &Value) -> Result<Value> {
let st = d.styles_mut().para_mut(&name).ok_or_else(|| bad("style.paragraph.edit", format!("no style `{name}`")))?;
st.para.merge(&para);
st.chars.merge(&chars);
match based {
Some(Value::String(b)) => st.based_on = Some(b),
Some(Value::Null) => st.based_on = None,
_ => {}
}
match next {
Some(Value::String(n)) => st.next_style = Some(n),
Some(Value::Null) => st.next_style = None,
+15
View File
@@ -1650,3 +1650,18 @@ fn a_line_fits_when_its_baseline_does() {
assert!(!overset(&mut s, 78.5), "the line is set; its descenders hang below the frame");
assert!(overset(&mut s, 50.0), "a baseline below the frame still oversets the line");
}
/// Reset To Base with a new Based On: a partial rule edit resolves over the new parent, not the
/// old one.
#[test]
fn reset_to_base_with_a_new_parent_resolves_rule_edits_over_it() {
let mut s = session();
let rule = |weight: f64, offset: f64| json!({"on": true, "weight": weight, "color": "[Black]", "offset": offset});
s.execute("style.paragraph.create", &json!({"name": "Old", "para": {"ruleBelow": rule(3.0, 1.0)}})).unwrap();
s.execute("style.paragraph.create", &json!({"name": "New", "para": {"ruleBelow": rule(1.0, 9.0)}})).unwrap();
s.execute("style.paragraph.create", &json!({"name": "Child", "basedOn": "Old"})).unwrap();
s.execute("style.paragraph.edit", &json!({"name": "Child", "resetToBase": true, "basedOn": "New", "para": {"ruleBelow": {"weight": 2}}}))
.unwrap();
let r = s.doc().unwrap().doc.styles.resolve_para_style("Child").0.rule_below;
assert_eq!((r.weight, r.offset), (2.0, 9.0), "the offset is the new parent's");
}
+47 -3
View File
@@ -3646,9 +3646,25 @@ fn grep_error(p: &str) -> Option<String> {
/// The Justification spacing check of Paragraph Style Options, against what the section showed:
/// the style's own values, or a new style's Based On.
fn check_paragraph_style_justification(app: &DesignApp, d: &Dialog) -> Result<(), String> {
let shown = if d.b("new") { d.s("basedOn") } else { d.s("name") };
let base =
app.session.active().map(|st| serde_json::to_value(st.doc.styles.resolve_para_style(&shown).0).unwrap_or_default()).unwrap_or_default();
// After Reset To Base the style starts from its Based On (the one chosen here, if any).
let base_of = |st: &designcraft_engine::DocState| {
let chosen = d.s("basedOn");
if chosen.is_empty() { st.doc.styles.para(&d.s("name")).and_then(|s| s.based_on.clone()).unwrap_or_default() } else { chosen }
};
let base = app
.session
.active()
.map(|st| {
let shown = if d.b("new") {
d.s("basedOn")
} else if d.b("resetToBase") {
base_of(st)
} else {
d.s("name")
};
serde_json::to_value(st.doc.styles.resolve_para_style(&shown).0).unwrap_or_default()
})
.unwrap_or_default();
check_justification(d, "p.", &base)
}
@@ -3777,6 +3793,10 @@ fn style_options_preview(app: &mut DesignApp, d: &mut Dialog) {
_ => based.clone(),
};
}
// Reset To Base shows on the page too.
if paragraph && d.b("resetToBase") {
params["resetToBase"] = json!(true);
}
if shown.as_ref() == Some(&params) {
return;
}
@@ -7614,6 +7634,30 @@ mod tests {
assert_eq!(s.based_on.as_deref(), Some(designcraft_doc::BASIC_PARAGRAPH));
}
/// With Reset To Base, the justification fields are checked against the base style (the
/// style's own minimum no longer applies), and Preview shows the reset on the page.
#[test]
fn reset_to_base_checks_and_previews_against_the_base() {
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
app.run("file.new", json!({})).unwrap();
let f = app.run("frame.create", json!({"rect": [36, 36, 400, 300], "content": "text", "text": "Loose words"})).unwrap();
app.run("text.select", json!({"story": f["story"], "anchor": 0, "focus": 5})).unwrap();
app.run("style.paragraph.create", json!({"name": "Loose", "basedOn": designcraft_doc::BASIC_PARAGRAPH, "para": {"wordSpaceMin": 1.2, "wordSpaceDesired": 1.3}, "chars": {"size": 30}}))
.unwrap();
app.run("style.paragraph.apply", json!({"name": "Loose"})).unwrap();
let mut d = Dialog::new("paragraphStyleOptions", json!({"name": "Loose"}));
d.fields.insert("p.wordSpaceDesired".into(), json!(1.1));
assert!(check_paragraph_style_justification(&app, &d).is_err(), "below the style's own minimum");
d.fields.insert("resetToBase".into(), json!(true));
assert_eq!(check_paragraph_style_justification(&app, &d), Ok(()), "the base's minimum is 100%");
d.fields.insert("preview".into(), json!(true));
style_options_preview(&mut app, &mut d);
assert_eq!(d.fields[PREVIEWED]["resetToBase"], true);
let size = app.run("type.selectionAttrs", json!({})).unwrap()["chars"]["size"].clone();
assert_eq!(size, json!(12.0), "the page shows the style reset to its base");
app.session.cancel_interaction();
}
#[test]
fn paragraph_style_options_edit_one_rule_field() {
let (mut app, rule) = ruled_paragraph();