mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 10:16:59 -06:00
Effects dialog: the Angle with Use Global Light sets the global light
The Angle field was greyed out with Use Global Light on. It now shows the document's global light angle and changes it for every effect that uses it, applied with the effects in one undo step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f90d286721
commit
c11006c832
@@ -358,6 +358,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
|
||||
|
||||
Newest first.
|
||||
|
||||
- **2026-10-23** · Effects dialog: with Use Global Light on, the Angle field (and the Bevel and Emboss one) stays editable and shows the document's global light angle; a change there sets the global light for every effect that uses it, applied with the effects in one undo step (`object.effects` takes `globalLight`). Before, the field was greyed out and the angle couldn't be changed in the dialog. No readiness percentage change.
|
||||
- **2026-10-24** · Custom-positioned anchored objects honour Relative to Spine (`anchored.options {spineRelative}`, IDML `SpineRelative`): the settings describe a left-hand page and mirror on a right-hand page, so marginal notes stay in the outside margin on both pages of a spread. On right-hand pages they took the left-hand placement and landed in the inside margin, across the spine or over the body text. No readiness percentage change.
|
||||
- **2026-10-23** · Save and export dialogs (Save As, Save a Copy, Export PDF and the other exports, Package, libraries and books) start in the active document's folder once it has been saved, and for a document never saved in the folder of the last file saved or exported, remembered across sessions (#1653). A folder deleted since falls back to the system's choice. No readiness percentage change.
|
||||
- **2026-10-22** · Autoflow (Shift-click with the loaded text cursor, autoflow on Place, `text.thread {autoflow}`) fills the pages that follow before it adds any, as InDesign does: one frame per column on each following page, whatever else the page holds, then new pages at the end of the document. It inserted pages right after the story's last frame, in front of the document's own pages. Frames it set past the end of the text go again; a page the document had stays, and a page autoflow added only for such a frame goes with it. Smart Text Reflow of the primary text frame is unchanged. No readiness percentage change.
|
||||
|
||||
@@ -584,7 +584,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Effects",
|
||||
[],
|
||||
None,
|
||||
"{effects?: {dropShadow?: {on?, color?, opacity?, angle?, globalLight?, distance?, size?, spread?}, innerShadow?, outerGlow?, innerGlow? {on?, color?, opacity?, size?, choke?, center?, mode? (blend mode: Screen, Multiply, …; Screen by default), technique?: softer|precise}, bevel?, satin?, feather? (width), featherOptions? {choke? (%), corners?: sharp|rounded|diffused}, directionalFeather?, gradientFeather?} (the fields given replace the object's, as the Effects dialog sets them), opacity?: 0..1, blend?, ids?} — every effect at once, one undo step",
|
||||
"{effects?: {dropShadow?: {on?, color?, opacity?, angle?, globalLight?, distance?, size?, spread?}, innerShadow?, outerGlow?, innerGlow? {on?, color?, opacity?, size?, choke?, center?, mode? (blend mode: Screen, Multiply, …; Screen by default), technique?: softer|precise}, bevel?, satin?, feather? (width), featherOptions? {choke? (%), corners?: sharp|rounded|diffused}, directionalFeather?, gradientFeather?} (the fields given replace the object's, as the Effects dialog sets them), opacity?: 0..1, blend?, globalLight? (the document's global light angle, as an Angle field with Use Global Light sets it), ids?} — every effect at once, one undo step",
|
||||
has_selection,
|
||||
effects_all
|
||||
),
|
||||
@@ -2293,8 +2293,16 @@ fn effects_all(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Some(BlendMode::parse(raw).ok_or_else(|| bad(ID, format!("unknown blend mode `{raw}`")))?)
|
||||
}
|
||||
};
|
||||
// The document's global light angle, changed from an Angle field with Use Global Light on.
|
||||
let global_light = match p.get("globalLight") {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(v) => Some(v.as_f64().filter(|a| a.is_finite()).ok_or_else(|| bad(ID, "globalLight must be a number"))?),
|
||||
};
|
||||
let ids = targets(s, p)?;
|
||||
s.edit(|d, _| {
|
||||
if let Some(a) = global_light {
|
||||
d.settings.global_light = (a + 180.0).rem_euclid(360.0) - 180.0;
|
||||
}
|
||||
let mut changed = 0;
|
||||
for id in &ids {
|
||||
let Some(it) = d.item_mut(*id) else { continue };
|
||||
@@ -4099,6 +4107,26 @@ mod light_tests {
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(angle(&s), 45.0);
|
||||
}
|
||||
|
||||
/// The Effects dialog's Angle with Use Global Light on changes the global light, with the
|
||||
/// effects, in one undo step.
|
||||
#[test]
|
||||
fn effects_sets_the_global_light_with_the_effects() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let a = s.execute("frame.create", &json!({"rect": [10, 10, 50, 50]})).unwrap()["id"].as_u64().unwrap();
|
||||
let b = s.execute("frame.create", &json!({"rect": [60, 10, 90, 50]})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("object.dropShadow", &json!({"on": true, "globalLight": true, "ids": [b]})).unwrap();
|
||||
let before = s.doc().unwrap().doc.settings.global_light;
|
||||
s.execute("object.effects", &json!({"ids": [a], "effects": {"dropShadow": {"on": true, "globalLight": true}}, "globalLight": 30})).unwrap();
|
||||
let d = &s.doc().unwrap().doc;
|
||||
assert_eq!(d.settings.global_light, 30.0);
|
||||
let other = &d.item(ItemId(b)).unwrap().effects.drop_shadow;
|
||||
assert_eq!(d.light_angle(other.angle, other.global_light), 30.0, "every shadow on Global Light follows");
|
||||
assert!(s.execute("object.effects", &json!({"ids": [a], "globalLight": "up"})).is_err());
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.settings.global_light, before, "one undo step");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -116,10 +116,17 @@ fn is_on(fx: &Value, page: &str) -> bool {
|
||||
|
||||
/// The `object.effects` params the dialog stands for.
|
||||
pub fn params(d: &Dialog) -> Value {
|
||||
json!({"ids": d.fields.get("ids").cloned().unwrap_or(json!([])), "effects": d.fields.get("fx").cloned().unwrap_or(json!({})),
|
||||
"opacity": d.fields.get("opacity").and_then(Value::as_f64).unwrap_or(1.0), "blend": d.fields.get("blend").and_then(Value::as_str).unwrap_or("Normal")})
|
||||
let mut p = json!({"ids": d.fields.get("ids").cloned().unwrap_or(json!([])), "effects": d.fields.get("fx").cloned().unwrap_or(json!({})),
|
||||
"opacity": d.fields.get("opacity").and_then(Value::as_f64).unwrap_or(1.0), "blend": d.fields.get("blend").and_then(Value::as_str).unwrap_or("Normal")});
|
||||
if let Some(a) = d.fields.get(GLOBAL_LIGHT).and_then(Value::as_f64) {
|
||||
p["globalLight"] = json!(a);
|
||||
}
|
||||
p
|
||||
}
|
||||
|
||||
/// The global light angle as changed in the dialog (an Angle field with Use Global Light on).
|
||||
const GLOBAL_LIGHT: &str = "globalLight";
|
||||
|
||||
/// Apply: the preview goes and the settings are made once, as one undo step.
|
||||
pub fn confirm(app: &mut DesignApp, d: &Dialog) -> Result<Value, String> {
|
||||
if d.fields.contains_key(PREVIEWED) {
|
||||
@@ -202,12 +209,19 @@ fn percent(ui: &mut egui::Ui, d: &mut Dialog, path: &[&str]) {
|
||||
}
|
||||
}
|
||||
|
||||
fn angle(ui: &mut egui::Ui, d: &mut Dialog, path: &[&str], enabled: bool, global: f64) {
|
||||
let mut v = if enabled { num(d, path) } else { global };
|
||||
let r = ui.add_enabled(enabled, egui::DragValue::new(&mut v).range(-180.0..=180.0).suffix("°").max_decimals(1));
|
||||
/// The effect's own angle at `path`, or with Use Global Light (`own` false) the document's
|
||||
/// global light angle: editing it then changes that one, for every effect that uses it.
|
||||
fn angle(ui: &mut egui::Ui, d: &mut Dialog, path: &[&str], own: bool, global: f64) -> egui::Response {
|
||||
let mut v = if own { num(d, path) } else { global };
|
||||
let r = ui.add(egui::DragValue::new(&mut v).range(-180.0..=180.0).suffix("°").max_decimals(1));
|
||||
if r.changed() {
|
||||
set(d, path, json!(v));
|
||||
if own {
|
||||
set(d, path, json!(v));
|
||||
} else {
|
||||
d.fields.insert(GLOBAL_LIGHT.into(), json!(v));
|
||||
}
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
fn flag(ui: &mut egui::Ui, d: &mut Dialog, path: &[&str], label: &str) {
|
||||
@@ -273,7 +287,9 @@ fn page_ui(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog, page: &str) {
|
||||
let lang = app.ui.language.clone();
|
||||
let tr = |s: &str| crate::i18n::tr(&lang, s).to_string();
|
||||
let unit = app.session.active().map_or(Unit::Points, |st| st.doc.settings.horizontal_units);
|
||||
let global = app.session.active().map_or(120.0, |st| st.doc.settings.global_light);
|
||||
// The global light angle as the dialog will set it (an edit in it shows at once).
|
||||
let global =
|
||||
d.fields.get(GLOBAL_LIGHT).and_then(Value::as_f64).unwrap_or_else(|| app.session.active().map_or(120.0, |st| st.doc.settings.global_light));
|
||||
let row = |ui: &mut egui::Ui, l: &str| crate::rtl::label(ui, tr(l));
|
||||
crate::rtl::label(ui, egui::RichText::new(tr(PAGES.iter().find(|p| p.0 == page).map_or("Transparency", |p| p.1))).font(semibold(14.0)));
|
||||
let grid = |id: &str| egui::Grid::new(("fx_grid", id.to_string())).num_columns(4).spacing([8.0, 6.0]);
|
||||
@@ -657,6 +673,32 @@ mod tests {
|
||||
texts
|
||||
}
|
||||
|
||||
/// With Use Global Light on, the Angle is the document's global light and stays editable:
|
||||
/// a change there is applied to the document with the effects (it was greyed out).
|
||||
#[test]
|
||||
fn the_angle_with_global_light_sets_the_global_light() {
|
||||
let (mut app, _) = app_with_frame();
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx, "en");
|
||||
crate::menus::activate(&mut app, "object.dropShadow", &Value::Null);
|
||||
let d = app.ui.dialog.as_mut().unwrap();
|
||||
set(d, &["dropShadow", "globalLight"], json!(true));
|
||||
frame(&mut app, &ctx);
|
||||
// The field shows the global angle and is enabled: a DragValue that isn't disabled.
|
||||
let global = app.session.doc().unwrap().doc.settings.global_light;
|
||||
let mut d = app.ui.dialog.clone().unwrap();
|
||||
let ui_ctx = egui::Context::default();
|
||||
let mut enabled = false;
|
||||
let mut out = ui_ctx.run_ui(egui::RawInput::default(), |ui| {
|
||||
enabled = angle(ui, &mut d, &["dropShadow", "angle"], false, global).enabled();
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
assert!(enabled, "the Angle stays editable with Use Global Light");
|
||||
d.fields.insert(GLOBAL_LIGHT.into(), json!(15.0));
|
||||
confirm(&mut app, &d).unwrap();
|
||||
assert_eq!(app.session.doc().unwrap().doc.settings.global_light, 15.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_makes_the_path_and_ignores_an_empty_one() {
|
||||
let mut d = Dialog::new(ID, json!({"fx": {}}));
|
||||
|
||||
Reference in New Issue
Block a user