mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 13:17:19 -06:00
Effect dialogs keep a typed distance within the effect's range (#4637)
This commit is contained in:
@@ -42,6 +42,20 @@ pub fn is_length(id: &str, key: &str, relative: bool) -> bool {
|
||||
catalog_index().get(id).is_some_and(|info| info.lengths.keys(relative).any(|k| k == key))
|
||||
}
|
||||
|
||||
/// The range in points that distance parameter `key` of effect `id` is kept within, as its
|
||||
/// parameter documentation gives it (`{maxRadius: pt 4..127 (8), …}`); `None` when it gives none.
|
||||
pub fn length_range(id: &str, key: &str) -> Option<std::ops::RangeInclusive<f64>> {
|
||||
let doc = catalog_index().get(id)?.params;
|
||||
let starts = |at: usize| doc.get(..at).and_then(|s| s.chars().last()).is_some_and(|c| c == '{' || c == ' ');
|
||||
let rest = doc.match_indices(key).map(|(at, _)| at).filter(|at| starts(*at)).find_map(|at| {
|
||||
let rest = doc.get(at + key.len()..)?;
|
||||
rest.strip_prefix(": pt ").or_else(|| rest.strip_prefix("?: pt "))
|
||||
})?;
|
||||
let (lo, hi) = rest.split_whitespace().next()?.split_once("..")?;
|
||||
let (lo, hi) = (lo.parse::<f64>().ok()?, hi.trim_end_matches(',').parse::<f64>().ok()?);
|
||||
(lo.is_finite() && hi.is_finite() && lo <= hi).then_some(lo..=hi)
|
||||
}
|
||||
|
||||
/// The distance parameters of effect `id` (none for unknown effects).
|
||||
fn lengths_of(id: &str) -> Lengths {
|
||||
let always = |always| Lengths { always, absolute: &[] };
|
||||
|
||||
@@ -94,7 +94,7 @@ mod catalog;
|
||||
mod geometry;
|
||||
mod params;
|
||||
|
||||
pub use catalog::{EffectInfo, Lengths, WARP_STYLES, effect_catalog, effect_info, is_length, plugin_effects};
|
||||
pub use catalog::{EffectInfo, Lengths, WARP_STYLES, effect_catalog, effect_info, is_length, length_range, plugin_effects};
|
||||
pub(crate) use geometry::{Budget, apply_one};
|
||||
pub use geometry::{GeomContext, apply_geometry, apply_geometry_bez, apply_geometry_with};
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1207,3 +1207,17 @@ fn a_repeat_of_a_symbol_whose_art_is_a_repeat_shares_the_instance_budget() {
|
||||
let small = Node::new(NodeId(0), NodeKind::Repeat(RepeatSpec::mirror(vec![instance("Small")], 90.0, 5.0)));
|
||||
assert_eq!(expand_live(Some(&d), &small).len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_distance_parameters_range_comes_from_its_documentation() {
|
||||
assert_eq!(length_range("pixelate.colorHalftone", "maxRadius"), Some(4.0..=127.0));
|
||||
// The effect applies the range's floor to a radius below it.
|
||||
assert_eq!(
|
||||
crate::pixel::parse("pixelate.colorHalftone", &json!({"maxRadius": 1})),
|
||||
crate::pixel::parse("pixelate.colorHalftone", &json!({"maxRadius": 4}))
|
||||
);
|
||||
// A distance documented without a range, and unknown effects and parameters, have none.
|
||||
assert_eq!(length_range("path.offsetPath", "offset"), None);
|
||||
assert_eq!(length_range("pixelate.colorHalftone", "channel1"), None);
|
||||
assert_eq!(length_range("no.such", "maxRadius"), None);
|
||||
}
|
||||
|
||||
@@ -44,8 +44,22 @@ pub(super) fn length_field(ui: &mut egui::Ui, d: &mut Dialog, key: &str, label:
|
||||
/// "5 mm", wins), kept in points (a string with a unit, "12 pt", also reads). Returns true when it
|
||||
/// changed.
|
||||
pub(super) fn length(ui: &mut egui::Ui, d: &mut Dialog, key: &str, unit: Unit, width: f32) -> bool {
|
||||
length_within(ui, d, key, unit, width, None)
|
||||
}
|
||||
|
||||
/// [`length`] whose value is kept within `range` (points): a value typed outside it becomes the
|
||||
/// nearer end, so the field shows the distance the command applies (#4637).
|
||||
pub(super) fn length_within(
|
||||
ui: &mut egui::Ui,
|
||||
d: &mut Dialog,
|
||||
key: &str,
|
||||
unit: Unit,
|
||||
width: f32,
|
||||
range: Option<std::ops::RangeInclusive<f64>>,
|
||||
) -> bool {
|
||||
let value = d.fields.contains_key(key).then(|| d.f64(key, 0.0));
|
||||
let Some(v) = crate::widgets::num_field(ui, ("dlg-len", key), value, unit, width) else { return false };
|
||||
let v = range.map_or(v, |r| v.max(*r.start()).min(*r.end()));
|
||||
d.fields.insert(key.into(), json!(v));
|
||||
true
|
||||
}
|
||||
@@ -365,7 +379,9 @@ pub(super) fn param_fields(
|
||||
return;
|
||||
}
|
||||
if is_length(&k) {
|
||||
changed |= length(ui, d, &k, unit, 140.0);
|
||||
// An effect's documented range keeps the field to the distance it applies.
|
||||
let range = vectorcraft_effects::length_range(&d.str("__effect"), &k);
|
||||
changed |= length_within(ui, d, &k, unit, 140.0, range);
|
||||
return;
|
||||
}
|
||||
if let Some(options) = choices(&k) {
|
||||
|
||||
@@ -2486,3 +2486,30 @@ fn artboard_options_selected_fit_spans_other_boards_and_off_board_art() {
|
||||
app.run("edit.redo", json!({})).unwrap();
|
||||
assert_eq!(app.session.doc().unwrap().doc, after);
|
||||
}
|
||||
|
||||
/// An effect dialog keeps a distance typed outside the effect's range to the range, so the field
|
||||
/// shows what the effect applies: Color Halftone's Max Radius below 4 pt is 4 pt (#4637).
|
||||
#[test]
|
||||
fn an_effect_dialog_keeps_a_typed_distance_within_the_effects_range() {
|
||||
let enter = || egui::Event::Key { key: egui::Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() };
|
||||
for (typed, applied) in [("1 pt", 4.0), ("0.5 px", 4.0), ("200 pt", 127.0), ("10 pt", 10.0)] {
|
||||
let mut app = app();
|
||||
let id = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap()["id"].as_u64().unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
theme::install_fonts(&ctx);
|
||||
theme::apply(&ctx, Default::default());
|
||||
let frame = |app: &mut VectorcraftApp, events: Vec<egui::Event>| {
|
||||
let mut out = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| show(app, ui.ctx()));
|
||||
out.textures_delta.clear();
|
||||
};
|
||||
app.run("select.all", json!({})).unwrap();
|
||||
app.run("effect.dialog", json!({"effect": "pixelate.colorHalftone"})).unwrap();
|
||||
// Max Radius, the first field, is focused: type over it and press Enter.
|
||||
frame(&mut app, vec![]);
|
||||
frame(&mut app, vec![egui::Event::Text(typed.into())]);
|
||||
frame(&mut app, vec![enter()]);
|
||||
assert!(app.ui.dialog.is_none(), "{typed}");
|
||||
let node = app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().clone();
|
||||
assert_eq!(node.appearance.effects.last().map(|e| e.params["maxRadius"].clone()), Some(json!(applied)), "{typed}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user