mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 11:11:30 +00:00
Merge pull request #6179 from dotCooCoo/fix/ids-param-strict
Commands: an ids list with an entry that is not an id is refused instead of dropping the entry
This commit is contained in:
@@ -420,8 +420,22 @@ pub(crate) fn text_param<'a>(p: &'a Value, key: &str, cmd: &str) -> Result<Optio
|
||||
pub(crate) fn id_param(p: &Value, key: &str) -> Option<NodeId> {
|
||||
p.get(key).and_then(Value::as_u64).map(NodeId)
|
||||
}
|
||||
pub(crate) fn ids_param(p: &Value, key: &str) -> Option<Vec<NodeId>> {
|
||||
p.get(key).and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_u64).map(NodeId).collect())
|
||||
/// The object ids `p` gives for `key`, or `None` when `p` leaves it out or gives null. A value that
|
||||
/// is not an array, or an entry that is not a non-negative integer, is an error with the message
|
||||
/// [`checked_ids_param`] gives, so no entry is dropped. Whether each id names an object is left to
|
||||
/// the caller.
|
||||
pub(crate) fn ids_param(p: &Value, key: &str, cmd: &str) -> Result<Option<Vec<NodeId>>> {
|
||||
let values = match p.get(key) {
|
||||
None | Some(Value::Null) => return Ok(None),
|
||||
Some(Value::Array(a)) => a,
|
||||
Some(_) => return Err(bad(cmd, format!("{key} must be an array of non-negative integer object ids"))),
|
||||
};
|
||||
values.iter().enumerate().map(|(index, value)| id_entry(value, key, index, cmd)).collect::<Result<Vec<_>>>().map(Some)
|
||||
}
|
||||
/// Entry `index` (from 0) of the `key` list as an object id: anything but a non-negative integer is
|
||||
/// an error naming the key, the entry and its value.
|
||||
fn id_entry(value: &Value, key: &str, index: usize, cmd: &str) -> Result<NodeId> {
|
||||
value.as_u64().map(NodeId).ok_or_else(|| bad(cmd, format!("`{key}` entry {index} is {value}, not a non-negative integer object id")))
|
||||
}
|
||||
/// The objects `p` gives `command`: its `ids`, else its `id` as `ids` of one (as [`targets`] reads
|
||||
/// them), each checked ([`checked_id`]): a malformed one, or one naming no object, is an error,
|
||||
@@ -447,7 +461,7 @@ pub(crate) fn index(v: u64) -> usize {
|
||||
pub(crate) fn checked_ids_param(s: &Session, p: &Value, key: &str, command: &str) -> Result<Vec<NodeId>> {
|
||||
let values =
|
||||
p.get(key).and_then(Value::as_array).ok_or_else(|| bad(command, format!("{key} must be an array of non-negative integer object ids")))?;
|
||||
values.iter().map(|value| checked_id(s, value, command)).collect()
|
||||
values.iter().enumerate().map(|(index, value)| id_entry(value, key, index, command).and_then(|_| checked_id(s, value, command))).collect()
|
||||
}
|
||||
|
||||
/// The objects a command acts on: `ids` when given (every one checked, and those inside another
|
||||
|
||||
@@ -174,7 +174,7 @@ fn filter(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let label = effects::effect_info(id).map_or("Apply as Filter", |e| e.label.trim_end_matches('…'));
|
||||
let kinds = effects::addons::duotone::swatch_kinds(&s.doc()?.doc);
|
||||
let swatches = |name: &str| kinds.get(name).copied();
|
||||
recolor_links(s, &given, label, &|c| map.apply(c), &|c, link, tint| map.apply_link(c, link, tint, &swatches))
|
||||
recolor_links(s, &given, C, label, &|c| map.apply(c), &|c, link, tint| map.apply_link(c, link, tint, &swatches))
|
||||
}
|
||||
|
||||
/// Apply Fill to Stroke (`to_stroke`) or Apply Stroke to Fill: each target copies one of its own
|
||||
|
||||
@@ -189,7 +189,7 @@ pub(super) fn apply(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let given = json!({
|
||||
"ids": ids.iter().map(|i| i.0).collect::<Vec<_>>(), "fill": fill, "stroke": stroke, "includeImages": false, "includePatterns": false,
|
||||
});
|
||||
let r = recolor_links(s, &given, "Apply Nearest Swatch", &|c| to(c).and_then(Paint::color).unwrap_or(c), &links)?;
|
||||
let r = recolor_links(s, &given, APPLY, "Apply Nearest Swatch", &|c| to(c).and_then(Paint::color).unwrap_or(c), &links)?;
|
||||
squash(s, undo, "Apply Nearest Swatch");
|
||||
Ok(json!({ "changed": r["changed"], "swatches": names, "added": added }))
|
||||
}
|
||||
|
||||
@@ -1230,7 +1230,7 @@ fn copy_from(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let selected = if defaults_only {
|
||||
vec![]
|
||||
} else {
|
||||
match ids_param(p, "ids") {
|
||||
match ids_param(p, "ids", C)? {
|
||||
Some(v) => outermost(&s.doc()?.doc, v),
|
||||
None => selected_roots(s)?,
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
None,
|
||||
"{fill?: true, stroke?: true, includeImages?: true, includePatterns?: true, ids?} replace each colour by its RGB inverse (keeps the colour model) in fills, strokes, text, gradient meshes (with fills), colorized image fills (their pixels stay unchanged), other embedded images (a recoloured copy) and pattern tiles (a new pattern swatch; the original stays), as one undo step → {changed}",
|
||||
has_selection,
|
||||
|s, p| recolor(s, p, "Invert Colors", &invert)
|
||||
|s, p| recolor(s, p, "edit.colors.invert", "Invert Colors", &invert)
|
||||
),
|
||||
cmd!(
|
||||
"edit.colors.toCMYK",
|
||||
@@ -41,7 +41,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
None,
|
||||
"{fill?, stroke?, includeImages?, includePatterns?, ids?} (as edit.colors.invert) → {changed}",
|
||||
has_selection,
|
||||
|s, p| recolor(s, p, "Convert to CMYK", &to_cmyk)
|
||||
|s, p| recolor(s, p, "edit.colors.toCMYK", "Convert to CMYK", &to_cmyk)
|
||||
),
|
||||
cmd!(
|
||||
"edit.colors.toGrayscale",
|
||||
@@ -50,7 +50,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
None,
|
||||
"{fill?, stroke?, includeImages?, includePatterns?, ids?} (as edit.colors.invert) → {changed}",
|
||||
has_selection,
|
||||
|s, p| recolor(s, p, "Convert to Grayscale", &to_gray)
|
||||
|s, p| recolor(s, p, "edit.colors.toGrayscale", "Convert to Grayscale", &to_gray)
|
||||
),
|
||||
cmd!(
|
||||
"edit.colors.toRGB",
|
||||
@@ -60,7 +60,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"{fill?, stroke?, includeImages?, includePatterns?, ids?} (as edit.colors.invert) → {changed}",
|
||||
has_selection,
|
||||
|s, p| {
|
||||
recolor(s, p, "Convert to RGB", &|c| {
|
||||
recolor(s, p, "edit.colors.toRGB", "Convert to RGB", &|c| {
|
||||
let [r, g, b] = c.to_rgb();
|
||||
Color::rgb(r, g, b)
|
||||
})
|
||||
@@ -331,26 +331,27 @@ impl ColorVisitor for Visit<'_, '_> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn recolor(s: &mut Session, p: &Value, label: &str, f: &dyn Fn(Color) -> Color) -> Result<Value> {
|
||||
run_pass(s, p, label, Recolor::new(Scope::of(p), f))
|
||||
pub(crate) fn recolor(s: &mut Session, p: &Value, cmd: &str, label: &str, f: &dyn Fn(Color) -> Color) -> Result<Value> {
|
||||
run_pass(s, p, cmd, label, Recolor::new(Scope::of(p), f))
|
||||
}
|
||||
|
||||
/// [`recolor`] with solid colours and gradient stops mapped through `links`, with their swatch
|
||||
/// link and tint.
|
||||
pub(crate) fn recolor_links(s: &mut Session, p: &Value, label: &str, f: &dyn Fn(Color) -> Color, links: &LinkMap) -> Result<Value> {
|
||||
run_pass(s, p, label, Recolor::with_links(Scope::of(p), f, links))
|
||||
pub(crate) fn recolor_links(s: &mut Session, p: &Value, cmd: &str, label: &str, f: &dyn Fn(Color) -> Color, links: &LinkMap) -> Result<Value> {
|
||||
run_pass(s, p, cmd, label, Recolor::with_links(Scope::of(p), f, links))
|
||||
}
|
||||
|
||||
/// Run `pass` on the `ids` param's objects (default: the selection) as one undo step.
|
||||
fn run_pass(s: &mut Session, p: &Value, label: &str, pass: Recolor) -> Result<Value> {
|
||||
let ids = match ids_param(p, "ids") {
|
||||
/// Run `pass` on the `ids` param's objects (default: the selection) as one undo step named `label`;
|
||||
/// errors name the command `cmd`.
|
||||
fn run_pass(s: &mut Session, p: &Value, cmd: &str, label: &str, pass: Recolor) -> Result<Value> {
|
||||
let ids = match ids_param(p, "ids", cmd)? {
|
||||
Some(v) => v,
|
||||
None => selected_roots(s)?,
|
||||
};
|
||||
let colorized_ok = super::paint::check_colorized(&s.doc()?.doc).is_ok();
|
||||
let n = s.edit(label, |d, _| {
|
||||
let n = pass.run(d, &ids);
|
||||
super::paint::check_colorized_kept(colorized_ok, d).map_err(|e| bad(label, e))?;
|
||||
super::paint::check_colorized_kept(colorized_ok, d).map_err(|e| bad(cmd, e))?;
|
||||
Ok(n)
|
||||
})?;
|
||||
Ok(json!({ "changed": n }))
|
||||
@@ -358,7 +359,7 @@ fn run_pass(s: &mut Session, p: &Value, label: &str, pass: Recolor) -> Result<Va
|
||||
|
||||
fn saturate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let i = (f64_req(p, "intensity", "edit.colors.saturate")?.clamp(-100.0, 100.0) / 100.0) as f32;
|
||||
recolor(s, p, "Saturate", &move |c| {
|
||||
recolor(s, p, "edit.colors.saturate", "Saturate", &move |c| {
|
||||
let [h, sat, v] = c.to_hsb();
|
||||
let to = (sat * (1.0 + i)).clamp(0.0, 1.0);
|
||||
// A color whose saturation stays the same is returned as it is, so it keeps its swatch link.
|
||||
@@ -416,12 +417,12 @@ fn adjust_balance(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
(*tint, *c) = (t, base.tinted(t));
|
||||
true
|
||||
};
|
||||
return run_pass(s, p, "Adjust Colors", Recolor::links(Scope::of(p), &shift));
|
||||
return run_pass(s, p, C, "Adjust Colors", Recolor::links(Scope::of(p), &shift));
|
||||
}
|
||||
other => return Err(bad(C, format!("unknown mode `{other}` (rgb|cmyk|gray|global)"))),
|
||||
};
|
||||
let convert = bool_or(p, "convert", false);
|
||||
recolor(s, p, "Adjust Colors", &move |c| {
|
||||
recolor(s, p, C, "Adjust Colors", &move |c| {
|
||||
let out = adjust(c);
|
||||
if convert { out } else { keep_model(c, out) }
|
||||
})
|
||||
|
||||
@@ -601,7 +601,7 @@ fn set_current(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
fn highlight(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "layer.highlight";
|
||||
let mut ids = ids_param(p, "ids").ok_or_else(|| bad(C, "missing ids"))?;
|
||||
let mut ids = ids_param(p, "ids", C)?.ok_or_else(|| bad(C, "missing ids"))?;
|
||||
ids.truncate(MAX_ROWS);
|
||||
let mode = str_param(p, "mode").unwrap_or("set");
|
||||
let st = s.doc_mut()?;
|
||||
@@ -740,7 +740,7 @@ enum Place {
|
||||
/// Drag rows in the Layers panel.
|
||||
fn move_rows(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "layer.move";
|
||||
let mut ids = ids_param(p, "ids").or_else(|| id_param(p, "id").map(|i| vec![i])).ok_or_else(|| bad(C, "missing ids"))?;
|
||||
let mut ids = ids_param(p, "ids", C)?.or_else(|| id_param(p, "id").map(|i| vec![i])).ok_or_else(|| bad(C, "missing ids"))?;
|
||||
ids.truncate(MAX_ROWS);
|
||||
let target = id_param(p, "target").ok_or_else(|| bad(C, "missing target"))?;
|
||||
let place = match str_param(p, "place").unwrap_or("inside") {
|
||||
|
||||
@@ -966,7 +966,7 @@ enum Item {
|
||||
/// The selected objects (or `ids`) as a library graphic (its id and name set by the caller).
|
||||
fn graphic_of(s: &Session, p: &Value, cmd: &str) -> Result<LibraryGraphic> {
|
||||
let st = s.doc()?;
|
||||
let ids = match ids_param(p, "ids") {
|
||||
let ids = match ids_param(p, "ids", cmd)? {
|
||||
Some(ids) => ids,
|
||||
None => st.selection.in_paint_order(&st.doc),
|
||||
};
|
||||
@@ -1006,7 +1006,7 @@ fn color_of(s: &Session, p: &Value, stroke: bool, cmd: &str) -> Result<NewColor>
|
||||
let given = NewColor { name: color.to_hex(), color, spot: None, tint: 1.0 };
|
||||
return Ok(spot_color(s, given, swatch, tint));
|
||||
}
|
||||
match selected_paint(s, p, stroke)? {
|
||||
match selected_paint(s, p, stroke, cmd)? {
|
||||
Paint::Solid { color, swatch, tint } => {
|
||||
let swatch = swatch.filter(|n| !n.is_empty());
|
||||
let name = swatch.clone().unwrap_or_else(|| color.to_hex());
|
||||
@@ -1029,9 +1029,9 @@ fn spot_color(s: &Session, c: NewColor, swatch: Option<String>, tint: f32) -> Ne
|
||||
|
||||
/// The first selected object's (or `ids`') fill (`stroke`: stroke) paint (type's: its first
|
||||
/// character's, as the proxy shows it), else the default one.
|
||||
fn selected_paint(s: &Session, p: &Value, stroke: bool) -> Result<Paint> {
|
||||
fn selected_paint(s: &Session, p: &Value, stroke: bool, cmd: &str) -> Result<Paint> {
|
||||
let st = s.doc()?;
|
||||
let ids = match ids_param(p, "ids") {
|
||||
let ids = match ids_param(p, "ids", cmd)? {
|
||||
Some(ids) => ids,
|
||||
None => st.selection.objects.clone(),
|
||||
};
|
||||
@@ -1071,7 +1071,7 @@ fn gradient_of(s: &Session, p: &Value, stroke: bool, cmd: &str) -> Result<Librar
|
||||
let sw = s.doc()?.doc.swatch(name).filter(|w| matches!(w.paint, Paint::Gradient(_)));
|
||||
(Some(name.to_string()), sw.ok_or_else(|| bad(cmd, format!("no gradient swatch `{name}`")))?.paint.clone())
|
||||
}
|
||||
None => (None, selected_paint(s, p, stroke)?),
|
||||
None => (None, selected_paint(s, p, stroke, cmd)?),
|
||||
};
|
||||
let Paint::Gradient(mut g) = paint else {
|
||||
return Err(bad(cmd, if stroke { "the selection has no gradient stroke" } else { "the selection has no gradient fill" }));
|
||||
@@ -1094,7 +1094,7 @@ fn pattern_of(s: &Session, p: &Value, stroke: bool, cmd: &str) -> Result<Library
|
||||
_ if d.pattern(name).is_some() => name.to_string(),
|
||||
_ => return Err(bad(cmd, format!("no pattern swatch `{name}`"))),
|
||||
},
|
||||
None => match selected_paint(s, p, stroke)? {
|
||||
None => match selected_paint(s, p, stroke, cmd)? {
|
||||
Paint::Pattern { pattern, .. } => pattern,
|
||||
_ => return Err(bad(cmd, if stroke { "the selection has no pattern stroke" } else { "the selection has no pattern fill" })),
|
||||
},
|
||||
|
||||
@@ -1079,7 +1079,7 @@ pub(crate) fn with_found_paths(d: &mut Document, doc_path: &str) {
|
||||
|
||||
fn check(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let st = s.doc()?;
|
||||
let ids = ids_param(p, "ids");
|
||||
let ids = ids_param(p, "ids", "links.check")?;
|
||||
let (mut missing, mut modified) = (0, 0);
|
||||
let mut links = vec![];
|
||||
for g in groups(&st.doc, ids.as_deref()).iter().filter(|g| !g.ids.is_empty()) {
|
||||
@@ -1100,7 +1100,7 @@ fn check(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
fn update(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
update_where(s, ids_param(p, "ids"), None)
|
||||
update_where(s, ids_param(p, "ids", "links.update")?, None)
|
||||
}
|
||||
|
||||
/// Update Links in the active document of `s` for objects `ids` (default: the linked files that
|
||||
@@ -1225,7 +1225,7 @@ fn relink(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
_ => None,
|
||||
})
|
||||
};
|
||||
let mut ids: Vec<NodeId> = match (&each, ids_param(p, "ids")) {
|
||||
let mut ids: Vec<NodeId> = match (&each, ids_param(p, "ids", C)?) {
|
||||
(Some(each), _) => each.iter().flat_map(|(ids, _)| ids.iter().copied()).collect(),
|
||||
(None, Some(ids)) => ids,
|
||||
(None, None) => {
|
||||
@@ -1603,7 +1603,7 @@ fn embed(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Some(NodeKind::PlacedDocument(_)) => true,
|
||||
_ => false,
|
||||
};
|
||||
let ids = ids_param(p, "ids").unwrap_or_else(|| st.selection.objects.iter().copied().filter(linked).collect());
|
||||
let ids = ids_param(p, "ids", C)?.unwrap_or_else(|| st.selection.objects.iter().copied().filter(linked).collect());
|
||||
if let Some(id) = ids.iter().find(|id| !matches!(kind(id), Some(NodeKind::Image(_) | NodeKind::PlacedDocument(_)))) {
|
||||
return Err(bad(C, format!("object {} is not an image or a placed document", id.0)));
|
||||
}
|
||||
@@ -1792,7 +1792,8 @@ fn info(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
fn placement_options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "links.placementOptions";
|
||||
let st = s.doc()?;
|
||||
let ids = ids_param(p, "ids").unwrap_or_else(|| st.selection.objects.iter().copied().filter(|id| placement_of(&st.doc, *id).is_some()).collect());
|
||||
let ids =
|
||||
ids_param(p, "ids", C)?.unwrap_or_else(|| st.selection.objects.iter().copied().filter(|id| placement_of(&st.doc, *id).is_some()).collect());
|
||||
if let Some(id) = ids.iter().find(|id| placement_of(&st.doc, **id).is_none()) {
|
||||
return Err(bad(C, format!("object {} is not an image or a linked file", id.0)));
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ fn apply_transform_moving_guides(
|
||||
let c = xf.as_coeffs();
|
||||
let is_translation = (c[0] - 1.0).abs() < 1e-9 && c[1].abs() < 1e-9 && c[2].abs() < 1e-9 && (c[3] - 1.0).abs() < 1e-9;
|
||||
let delta = guides.unwrap_or(Vec2::new(c[4], c[5]));
|
||||
let with_guides = objects && ids_param(p, "ids").is_none() && is_translation;
|
||||
let with_guides = objects && p.get("ids").is_none_or(Value::is_null) && is_translation;
|
||||
let has_guides = with_guides && !s.doc()?.selection.guides.is_empty();
|
||||
// A transform of ids that name no object, or an identity transform without a copy, moves
|
||||
// nothing: the document and its undo history stay as they are, and Transform Again repeats it.
|
||||
@@ -254,7 +254,7 @@ pub(super) fn transform(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if p.get("ids").is_none() && crate::cmd::brushsym::selected_part(s)?.is_some() {
|
||||
return Ok(json!({ "warning": crate::cmd::brushsym::part_only().to_string() }));
|
||||
}
|
||||
let (ids, anchors) = match ids_param(p, "ids") {
|
||||
let (ids, anchors) = match ids_param(p, "ids", "object.transform")? {
|
||||
Some(v) => (outermost(&s.doc()?.doc, v), false),
|
||||
None => (selected_roots_to_transform(s)?, bool_or(p, "anchors", false)),
|
||||
};
|
||||
|
||||
@@ -210,7 +210,7 @@ fn make(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Some(id) => Err(bad(C, format!("object {} is hidden or locked, or on a locked artboard", id.0))),
|
||||
None => Ok(()),
|
||||
};
|
||||
let mut ids = match ids_param(p, "ids") {
|
||||
let mut ids = match ids_param(p, "ids", C)? {
|
||||
Some(ids) => {
|
||||
if let Some(id) = ids.iter().find(|id| d.node(**id).is_none()) {
|
||||
return Err(EngineError::NoNode(*id));
|
||||
|
||||
@@ -229,7 +229,7 @@ pub(crate) fn overprint_black(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
rich: bool_or(p, "includeCmyBlacks", false),
|
||||
spot: bool_or(p, "includeSpotBlacks", false),
|
||||
};
|
||||
let ids = match ids_param(p, "ids") {
|
||||
let ids = match ids_param(p, "ids", "edit.colors.overprintBlack")? {
|
||||
Some(v) => v,
|
||||
None => selected_roots(s)?,
|
||||
};
|
||||
|
||||
@@ -950,7 +950,7 @@ fn proxy_recolor(s: &mut Session, p: &Value, cmd: &str, label: &str, f: &dyn Fn(
|
||||
// The proxy recolors its paints, including a colorized image's fill, without changing image pixels or pattern tiles.
|
||||
ItemTarget::Top => {
|
||||
let q = json!({"fill": !stroke, "stroke": stroke, "includeImages": false, "includePatterns": false, "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>()});
|
||||
(super::colorcmds::recolor(s, &q, label, f)?, None)
|
||||
(super::colorcmds::recolor(s, &q, cmd, label, f)?, None)
|
||||
}
|
||||
};
|
||||
let shown_ids = if shown_item.is_some() { ids } else { leaf_targets(s, &ids)? };
|
||||
|
||||
@@ -597,7 +597,7 @@ pub(super) fn join(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
},
|
||||
};
|
||||
|
||||
if let Some(ids) = ids_param(p, "ids") {
|
||||
if let Some(ids) = ids_param(p, "ids", C)? {
|
||||
if ends.is_some() && ids.len() > 2 {
|
||||
return Err(bad(C, "`ends` applies to joining exactly two paths"));
|
||||
}
|
||||
@@ -1101,7 +1101,7 @@ fn average_join_ends(s: &Session, p: &Value, c: &str) -> Result<Option<[End; 2]>
|
||||
let doc = &st.doc;
|
||||
let subpath = |id: NodeId, si: usize| doc.node(id)?.path_data()?.subpaths.get(si).filter(|sp| !sp.closed && !sp.anchors.is_empty());
|
||||
let last = |sp: &SubPath| sp.anchors.len().saturating_sub(1);
|
||||
let ids = ids_param(p, "ids");
|
||||
let ids = ids_param(p, "ids", c)?;
|
||||
let ends = if ids.is_none() && !st.selection.anchors.is_empty() {
|
||||
let picked: Vec<End> = st.selection.anchors.iter().flat_map(|(&id, set)| set.iter().map(move |&(si, ai)| (id, si, ai))).collect();
|
||||
match picked.as_slice() {
|
||||
|
||||
@@ -219,8 +219,8 @@ fn ids_json(ids: &[NodeId]) -> Value {
|
||||
json!({ "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>() })
|
||||
}
|
||||
|
||||
fn roots(s: &Session, p: &Value) -> Result<Vec<NodeId>> {
|
||||
match ids_param(p, "ids") {
|
||||
fn roots(s: &Session, p: &Value, cmd: &str) -> Result<Vec<NodeId>> {
|
||||
match ids_param(p, "ids", cmd)? {
|
||||
Some(ids) => {
|
||||
let st = s.doc()?;
|
||||
let mut sel = Selection::default();
|
||||
@@ -306,7 +306,7 @@ fn pattern_make(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if editing(s).is_some() {
|
||||
return Err(bad(C, "already in pattern editing mode"));
|
||||
}
|
||||
let roots = roots(s, p)?;
|
||||
let roots = roots(s, p, C)?;
|
||||
if roots.is_empty() {
|
||||
return Err(bad(C, "select the art for the pattern tile"));
|
||||
}
|
||||
@@ -665,9 +665,9 @@ fn pattern_transform(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// ---------- Repeat ----------
|
||||
|
||||
/// Make a repeat from the selection, or switch the selected repeat's kind.
|
||||
fn make_repeat(s: &mut Session, p: &Value, label: &str, make: impl Fn(Vec<Arc<Node>>) -> RepeatSpec) -> Result<Value> {
|
||||
fn make_repeat(s: &mut Session, p: &Value, cmd: &str, label: &str, make: impl Fn(Vec<Arc<Node>>) -> RepeatSpec) -> Result<Value> {
|
||||
let existing = selected_repeats(s);
|
||||
if ids_param(p, "ids").is_none()
|
||||
if ids_param(p, "ids", cmd)?.is_none()
|
||||
&& let [r] = existing.as_slice()
|
||||
{
|
||||
let r = *r;
|
||||
@@ -680,7 +680,7 @@ fn make_repeat(s: &mut Session, p: &Value, label: &str, make: impl Fn(Vec<Arc<No
|
||||
})?;
|
||||
return Ok(json!({ "id": r.0 }));
|
||||
}
|
||||
let roots = roots(s, p)?;
|
||||
let roots = roots(s, p, cmd)?;
|
||||
if roots.is_empty() {
|
||||
return Err(EngineError::Other("select the art to repeat".into()));
|
||||
}
|
||||
@@ -699,14 +699,14 @@ fn repeat_radial(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Err(bad("object.repeat.radial", "instances must be 1..1000"));
|
||||
}
|
||||
let radius = p.get("radius").and_then(Value::as_f64).filter(|r| r.is_finite() && *r >= 0.0);
|
||||
make_repeat(s, p, "Radial Repeat", |src| RepeatSpec::radial(src, n as u32, radius))
|
||||
make_repeat(s, p, "object.repeat.radial", "Radial Repeat", |src| RepeatSpec::radial(src, n as u32, radius))
|
||||
}
|
||||
|
||||
fn repeat_grid(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (hs, vs) = (p.get("hSpacing").and_then(Value::as_f64), p.get("vSpacing").and_then(Value::as_f64));
|
||||
let rows = p.get("rows").and_then(Value::as_u64).map(|v| v.clamp(1, 500) as u32);
|
||||
let cols = p.get("cols").and_then(Value::as_u64).map(|v| v.clamp(1, 500) as u32);
|
||||
make_repeat(s, p, "Grid Repeat", |src| {
|
||||
make_repeat(s, p, "object.repeat.grid", "Grid Repeat", |src| {
|
||||
let b = vectorcraft_doc::live::nodes_bounds(&src).unwrap_or(Rect::new(0.0, 0.0, 10.0, 10.0));
|
||||
let mut r = RepeatSpec::grid(src, hs.unwrap_or(b.width() / 4.0), vs.unwrap_or(b.height() / 4.0));
|
||||
if let RepeatKind::Grid { rows: rr, cols: cc, .. } = &mut r.kind {
|
||||
@@ -720,7 +720,7 @@ fn repeat_grid(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
fn repeat_mirror(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let angle = f64_or(p, "angle", 90.0);
|
||||
let offset = f64_or(p, "offset", 10.0);
|
||||
make_repeat(s, p, "Mirror Repeat", |src| RepeatSpec::mirror(src, angle, offset))
|
||||
make_repeat(s, p, "object.repeat.mirror", "Mirror Repeat", |src| RepeatSpec::mirror(src, angle, offset))
|
||||
}
|
||||
|
||||
fn repeat_release(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
|
||||
@@ -141,7 +141,7 @@ impl FilterRun {
|
||||
let st = s.doc()?;
|
||||
// While an object of a dynamic symbol instance's art is selected, the selection is refused,
|
||||
// as `selected_roots` refuses it.
|
||||
let selected = match ids_param(p, "ids") {
|
||||
let selected = match ids_param(p, "ids", C)? {
|
||||
Some(ids) => ids,
|
||||
None => {
|
||||
super::edit::refuse_symbol_part(s)?;
|
||||
|
||||
@@ -204,7 +204,7 @@ impl Session {
|
||||
/// The slices a command acts on: `slices` (ids that must all be slices), else the selected ones.
|
||||
fn slice_targets(s: &Session, p: &Value, cmd: &str) -> Result<Vec<NodeId>> {
|
||||
let st = s.doc()?;
|
||||
match ids_param(p, "slices") {
|
||||
match ids_param(p, "slices", cmd)? {
|
||||
Some(ids) => match ids.iter().find(|id| !st.doc.is_slice(**id)) {
|
||||
Some(id) => Err(bad(cmd, format!("{id} is not a slice"))),
|
||||
None => Ok(dedup(ids)),
|
||||
@@ -290,7 +290,7 @@ pub(crate) fn options_json(o: &SliceOptions) -> Value {
|
||||
// ---------- Object → Slice ----------
|
||||
|
||||
fn make(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let ids = match ids_param(p, "ids") {
|
||||
let ids = match ids_param(p, "ids", "object.slice.make")? {
|
||||
Some(ids) => super::edit::outermost(&s.doc()?.doc, ids),
|
||||
None => super::edit::selected_roots(s)?,
|
||||
};
|
||||
@@ -734,7 +734,8 @@ fn delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
fn select(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let ids = slice_targets(s, &json!({ "slices": p.get("slices").cloned().unwrap_or(json!([])) }), "object.slice.select")?;
|
||||
let slices = p.get("slices").filter(|v| !v.is_null()).cloned().unwrap_or(json!([]));
|
||||
let ids = slice_targets(s, &json!({ "slices": slices }), "object.slice.select")?;
|
||||
let toggle = bool_or(p, "toggle", false);
|
||||
s.select(|_, sel| {
|
||||
let mut now = if toggle { sel.slices.clone() } else { vec![] };
|
||||
|
||||
@@ -65,7 +65,8 @@ pub(crate) fn enter(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if st.doc.pattern_edit.is_some() {
|
||||
return Err(bad(C, "finish editing the pattern first"));
|
||||
}
|
||||
let given = object_param(p, "id", C)?.map(|i| vec![i]).or_else(|| super::ids_param(p, "ids").filter(|v| !v.is_empty()));
|
||||
let ids = super::ids_param(p, "ids", C)?;
|
||||
let given = object_param(p, "id", C)?.map(|i| vec![i]).or_else(|| ids.filter(|v| !v.is_empty()));
|
||||
let (name, xf, instance) = match given {
|
||||
Some(ids) => {
|
||||
let symbols = ids.iter().map(|id| instance_of(&st.doc, *id)).collect::<Option<Vec<_>>>();
|
||||
|
||||
@@ -121,13 +121,14 @@ pub(crate) const SELECTS_GIVEN_IDS: &[&str] = &[
|
||||
/// The non-empty `ids` given to a command whose params description names `ids` as a key (the
|
||||
/// key callers outside the app may pass it, [`cmd::params::takes_key`]), or its `id` as `ids` of
|
||||
/// one when no `ids` are given and the description takes `id` so
|
||||
/// ([`cmd::params::id_stands_for_ids`]), as [`cmd::targets`] reads them.
|
||||
/// ([`cmd::params::id_stands_for_ids`]), as [`cmd::targets`] reads them. Entries of `ids` that are
|
||||
/// not ids are left out of this list; `Session::execute` refuses them before it reads it.
|
||||
pub(crate) fn given_ids(description: &'static str, params: &Value) -> Option<Vec<NodeId>> {
|
||||
if !cmd::params::takes_key(description, "ids") {
|
||||
return None;
|
||||
}
|
||||
match params.get("ids").filter(|v| !v.is_null()) {
|
||||
Some(_) => cmd::ids_param(params, "ids"),
|
||||
Some(v) => v.as_array().map(|a| a.iter().filter_map(Value::as_u64).map(NodeId).collect()),
|
||||
None => cmd::id_param(params, "id").filter(|_| cmd::params::id_stands_for_ids(description)).map(|id| vec![id]),
|
||||
}
|
||||
.filter(|ids| !ids.is_empty())
|
||||
|
||||
@@ -15,11 +15,14 @@ impl Session {
|
||||
r
|
||||
}
|
||||
|
||||
/// Error, under the command's own id, at the first of `ids` that is malformed, names no node, or
|
||||
/// names a layer or sublayer: a command in [`SELECTS_GIVEN_IDS`] acts on objects.
|
||||
/// Error, under the command's own id, at the first of `ids` that is malformed, else at the first
|
||||
/// that names no node or names a layer or sublayer: a command in [`SELECTS_GIVEN_IDS`] acts on
|
||||
/// objects.
|
||||
fn check_given_ids(&self, id: &str, params: &Value) -> Result<()> {
|
||||
let (Some(st), Some(ids)) = (self.active(), params.get("ids")) else { return Ok(()) };
|
||||
let Some(given) = ids.as_array() else { return cmd::checked_ids_param(self, params, "ids", id).map(drop) };
|
||||
// A malformed entry anywhere in the list is refused first, under the key and the entry.
|
||||
cmd::ids_param(params, "ids", id)?;
|
||||
for value in given {
|
||||
let node = cmd::checked_id(self, value, id)?;
|
||||
if st.doc.node(node).is_some_and(|n| n.is_layer()) {
|
||||
@@ -65,6 +68,13 @@ impl Session {
|
||||
if SELECTS_GIVEN_IDS.contains(&id) {
|
||||
self.check_given_ids(id, params)?;
|
||||
}
|
||||
// An `ids` list with an entry that is not an object id, or an `ids` that is not a list, is
|
||||
// refused under the command's id before the menu check, which would otherwise run on the
|
||||
// other entries or on the selection. This holds with no document open too, where commands
|
||||
// such as `effect.list` still run.
|
||||
if cmd::params::takes_key(spec.params, "ids") {
|
||||
cmd::ids_param(params, "ids", id)?;
|
||||
}
|
||||
// Clear's menu needs a selection; an agent supplying explicit targets only needs a
|
||||
// document. The command validates those targets before making any edit.
|
||||
let given = given_ids(spec.params, params);
|
||||
|
||||
@@ -135,8 +135,9 @@ fn id_does_what_ids_of_one_does_with_nothing_selected() {
|
||||
|
||||
/// Every command that takes `id` as `ids` of one ([`cmd::params::id_stands_for_ids`]), with an
|
||||
/// object selected: an `id` that is malformed (a string, negative, a fraction, an object) or names
|
||||
/// no object is refused under the command's id, as the same value in `ids` is, and the document
|
||||
/// and the selection stay as they were. It never stands for the selection instead. (A string `id`
|
||||
/// no object is refused under the command's id, as the same value in `ids` is (whose error for a
|
||||
/// malformed entry also names the key and the entry), and the document and the selection stay as
|
||||
/// they were. It never stands for the selection instead. (A string `id`
|
||||
/// given to `effect.apply` without `effect` names the effect, which "bad" isn't: refused too.)
|
||||
#[test]
|
||||
fn a_bad_id_is_refused_as_the_same_ids_are_and_changes_nothing() {
|
||||
@@ -158,7 +159,10 @@ fn a_bad_id_is_refused_as_the_same_ids_are_and_changes_nothing() {
|
||||
let st = s.doc().unwrap();
|
||||
(r, st.doc == before && st.selection.objects == vec![NodeId(d)])
|
||||
};
|
||||
let (with_id, with_ids) = (run("id", bad.clone()), run("ids", json!([bad.clone()])));
|
||||
let (with_id, mut with_ids) = (run("id", bad.clone()), run("ids", json!([bad.clone()])));
|
||||
// The error for an `ids` entry names the key and the entry; the one for `id` names the value.
|
||||
let entry = format!("`ids` entry 0 is {bad}, not a non-negative integer object id");
|
||||
with_ids.0 = with_ids.0.map_err(|e| e.replace(&entry, &format!("invalid object id {bad}: expected a non-negative integer")));
|
||||
let effect_named = spec.id == "effect.apply" && bad.is_string();
|
||||
if with_id.0.is_ok() || !with_id.1 || (with_id != with_ids && !effect_named) {
|
||||
failures.push(format!(
|
||||
@@ -301,3 +305,205 @@ fn ids_of_an_object_and_a_group_that_holds_it_do_what_that_selection_does() {
|
||||
assert!(failures.is_empty(), "{} differ:\n{}", failures.len(), failures.join("\n"));
|
||||
assert!(succeeded >= 150, "only {succeeded} runs succeeded");
|
||||
}
|
||||
|
||||
/// What `ids` (or `slices`) left out or null gives a command: the selection, or the command's own
|
||||
/// default.
|
||||
enum Unset {
|
||||
/// The selected objects: a result, the same as the list of the selection gives.
|
||||
Selected,
|
||||
/// The selected objects, which the command refuses with this error, as it refuses the list of
|
||||
/// the selection.
|
||||
SelectionRefused(&'static str),
|
||||
/// This error, which the list of the selection does not give.
|
||||
Fails(&'static str),
|
||||
/// This result, which the list of the selection does not give.
|
||||
Gives(serde_json::Value),
|
||||
}
|
||||
|
||||
/// The commands that read their `ids` (or `slices`) list themselves or pass it to a colour pass,
|
||||
/// with the rectangle apart (`d`): the key, the other params, and what leaving the key out gives.
|
||||
fn ids_cases(d: u64) -> Vec<(&'static str, &'static str, serde_json::Value, Unset)> {
|
||||
use Unset::*;
|
||||
vec![
|
||||
("appearance.copyFrom", "ids", json!({ "source": d }), Selected),
|
||||
("edit.colors.invert", "ids", json!({}), Selected),
|
||||
("edit.colors.toCMYK", "ids", json!({}), Selected),
|
||||
("edit.colors.toGrayscale", "ids", json!({}), Selected),
|
||||
("edit.colors.toRGB", "ids", json!({}), Selected),
|
||||
("edit.colors.saturate", "ids", json!({"intensity": 50}), Selected),
|
||||
("edit.colors.adjustBalance", "ids", json!({"r": 20}), Selected),
|
||||
("paint.invert", "ids", json!({}), Selected),
|
||||
("paint.complement", "ids", json!({}), Selected),
|
||||
("addon.color.filter", "ids", json!({"effect": "addon.color.invert"}), Selected),
|
||||
("addon.color.applyNearestSwatch", "ids", json!({}), Selected),
|
||||
("edit.colors.overprintBlack", "ids", json!({}), Selected),
|
||||
("edit.findReplace", "ids", json!({"find": "A", "replace": "B"}), Gives(json!({"count": 1}))),
|
||||
("layer.highlight", "ids", json!({}), Fails("missing ids")),
|
||||
("layer.move", "ids", json!({ "target": d }), Fails("missing ids")),
|
||||
("library.add", "ids", json!({"kind": "graphic"}), Selected),
|
||||
("library.add", "ids", json!({"kind": "fillColor"}), Selected),
|
||||
("links.check", "ids", json!({}), Selected),
|
||||
("links.update", "ids", json!({}), Selected),
|
||||
("links.relink", "ids", json!({"path": "missing.png"}), Fails("select the images to relink, or pass ids")),
|
||||
("links.embed", "ids", json!({}), Fails("select the linked images or placed documents, or pass ids")),
|
||||
("links.placementOptions", "ids", json!({}), Fails("select the images, or pass ids")),
|
||||
("object.transform", "ids", json!({"matrix": [1, 0, 0, 1, 10, 0]}), Selected),
|
||||
("object.pattern.make", "ids", json!({"name": "P"}), Selected),
|
||||
("object.repeat.radial", "ids", json!({}), Selected),
|
||||
("object.repeat.grid", "ids", json!({}), Selected),
|
||||
("object.repeat.mirror", "ids", json!({}), Selected),
|
||||
("object.slice.make", "ids", json!({}), Selected),
|
||||
("object.slice.move", "slices", json!({"dx": 1, "dy": 1}), Selected),
|
||||
("object.slice.delete", "slices", json!({}), Selected),
|
||||
("object.slice.select", "slices", json!({}), Gives(json!({"selected": []}))),
|
||||
("path.join", "ids", json!({}), SelectionRefused("path is already closed")),
|
||||
("path.averageJoin", "ids", json!({"to": "corner"}), SelectionRefused("select two open path ends, or two open paths, to average and join")),
|
||||
("symbol.edit", "ids", json!({}), Fails("the document has no symbols")),
|
||||
("transparency.makeOpacityMask", "ids", json!({}), Selected),
|
||||
]
|
||||
}
|
||||
|
||||
/// What `cmd` does in a fresh [`doc_with_type`] with `selected` selected (made an object slice
|
||||
/// first when `slice`): its result or error, the document, the selection and the undo history's
|
||||
/// length.
|
||||
fn outcome(
|
||||
cmd: &str,
|
||||
p: &serde_json::Value,
|
||||
selected: &[u64],
|
||||
slice: bool,
|
||||
) -> (std::result::Result<serde_json::Value, String>, vectorcraft_doc::Document, vectorcraft_doc::Selection, usize) {
|
||||
let (mut s, ..) = doc_with_type();
|
||||
if slice {
|
||||
s.execute("object.slice.make", &json!({ "ids": selected })).unwrap();
|
||||
}
|
||||
s.execute("select.set", &json!({ "ids": selected })).unwrap();
|
||||
let r = s.execute(cmd, p).map_err(|e| e.to_string());
|
||||
let st = s.doc().unwrap();
|
||||
(r, (*st.doc).clone(), st.selection.clone(), st.history.undo.len())
|
||||
}
|
||||
|
||||
/// With the rectangle apart (`d`) selected and with nothing selected, a list with an entry that is
|
||||
/// not a non-negative integer (a string, true, an object) is refused with an error naming the key,
|
||||
/// the entry and its value, a value that is not a list with an error naming the key, and the
|
||||
/// document, the selection and the undo history stay as they were. No entry is dropped. A list of
|
||||
/// the rectangle is not refused.
|
||||
#[test]
|
||||
fn an_ids_entry_that_is_not_an_id_is_refused_and_changes_nothing() {
|
||||
let (_, [.., d], _) = doc_with_type();
|
||||
let mut failures = vec![];
|
||||
for (cmd, key, params, _) in &ids_cases(d) {
|
||||
let bads = [json!(["abc"]), json!([d, true]), json!([{}]), json!("abc")];
|
||||
for (bad, selected) in bads.iter().flat_map(|b| [(b, true), (b, false)]) {
|
||||
// A fresh document each time, so no mode one case enters carries into the next.
|
||||
let (mut s, ..) = doc_with_type();
|
||||
if selected {
|
||||
s.execute("select.set", &json!({ "ids": [d] })).unwrap();
|
||||
}
|
||||
let st = s.doc().unwrap();
|
||||
let before = (st.doc.clone(), st.selection.clone(), st.history.undo.len());
|
||||
let mut p = params.clone();
|
||||
p[*key] = bad.clone();
|
||||
let r = s.execute(cmd, &p);
|
||||
let want = match bad.as_array() {
|
||||
Some(a) => {
|
||||
let (index, value) = a.iter().enumerate().find(|(_, v)| !v.is_u64()).unwrap();
|
||||
format!("`{key}` entry {index} is {value}, not a non-negative integer object id")
|
||||
}
|
||||
None => format!("{key} must be an array of non-negative integer object ids"),
|
||||
};
|
||||
match &r {
|
||||
Err(EngineError::BadParams { cmd: c, msg }) if c == cmd && *msg == want => {}
|
||||
r => failures.push(format!("{cmd} {key}: {bad} (selected: {selected}): {r:.160?}")),
|
||||
}
|
||||
let st = s.doc().unwrap();
|
||||
if (st.doc.clone(), st.selection.clone(), st.history.undo.len()) != before {
|
||||
failures.push(format!("{cmd} {key}: {bad} (selected: {selected}): the document, the selection or the undo history changed"));
|
||||
}
|
||||
}
|
||||
let (mut s, ..) = doc_with_type();
|
||||
s.execute("select.set", &json!({ "ids": [d] })).unwrap();
|
||||
let mut p = params.clone();
|
||||
p[*key] = json!([d]);
|
||||
if let Err(EngineError::BadParams { msg, .. }) = s.execute(cmd, &p)
|
||||
&& (msg.starts_with(&format!("`{key}` entry ")) || msg.starts_with(&format!("{key} must be an array")))
|
||||
{
|
||||
failures.push(format!("{cmd} {key}: [{d}] is refused: {msg}"));
|
||||
}
|
||||
}
|
||||
assert!(failures.is_empty(), "{} fail:\n{}", failures.len(), failures.join("\n"));
|
||||
}
|
||||
|
||||
/// With the rectangle apart (`d`) selected, the same commands with `ids` (or `slices`) left out and
|
||||
/// with it null do the same: what the selection's list does for the commands that take the
|
||||
/// selection, and the command's own default for the others (`object.slice.select` deselects the
|
||||
/// slices). `addon.color.filter` and `addon.color.applyNearestSwatch` read `ids` through
|
||||
/// [`cmd::checked_ids_or_selection`], which refuses null as a value that is not a list and changes
|
||||
/// nothing.
|
||||
#[test]
|
||||
fn ids_left_out_or_null_take_the_selection_or_the_default() {
|
||||
let (_, [.., d], _) = doc_with_type();
|
||||
let mut failures = vec![];
|
||||
for (cmd, key, params, unset) in ids_cases(d) {
|
||||
// The commands on `slices` act on the selected object once it is an object slice.
|
||||
let slice = key == "slices";
|
||||
let left_out = outcome(cmd, ¶ms, &[d], slice);
|
||||
let mut null = params.clone();
|
||||
null[key] = serde_json::Value::Null;
|
||||
let with_null = outcome(cmd, &null, &[d], slice);
|
||||
if matches!(cmd, "addon.color.filter" | "addon.color.applyNearestSwatch") {
|
||||
let (mut s, ..) = doc_with_type();
|
||||
s.execute("select.set", &json!({ "ids": [d] })).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
let refused = with_null.0.as_ref().is_err_and(|e| e.ends_with("ids must be an array of non-negative integer object ids"));
|
||||
if !refused || (&with_null.1, &with_null.2, with_null.3) != (&*st.doc, &st.selection, st.history.undo.len()) {
|
||||
failures.push(format!("{cmd} {key}: null is not refused with nothing changed ({:.160?})", with_null.0));
|
||||
}
|
||||
} else if with_null != left_out {
|
||||
failures.push(format!("{cmd} {key}: null does not do what leaving it out does ({:.160?})", left_out.0));
|
||||
}
|
||||
let mut listed = params.clone();
|
||||
listed[key] = json!([d]);
|
||||
let listed = outcome(cmd, &listed, &[d], slice);
|
||||
let ok = match &unset {
|
||||
Unset::Selected => left_out.0.is_ok() && listed == left_out,
|
||||
Unset::SelectionRefused(want) => left_out.0.as_ref().is_err_and(|e| e.ends_with(want)) && listed == left_out,
|
||||
Unset::Fails(want) => left_out.0.as_ref().is_err_and(|e| e.ends_with(want)) && listed.0 != left_out.0,
|
||||
Unset::Gives(want) => left_out.0.as_ref() == Ok(want) && listed.0 != left_out.0,
|
||||
};
|
||||
if !ok {
|
||||
failures.push(format!("{cmd} {key}: left out: {:.200?}; listed: {:.200?}", left_out.0, listed.0));
|
||||
}
|
||||
}
|
||||
assert!(failures.is_empty(), "{} fail:\n{}", failures.len(), failures.join("\n"));
|
||||
}
|
||||
|
||||
/// With no document open, every command that takes `ids` refuses a list with an entry that is not
|
||||
/// a non-negative integer, and a value that is not a list, with the same error as with a document
|
||||
/// open (`effect.list` and `links.findFiles` run without a document).
|
||||
#[test]
|
||||
fn an_ids_entry_that_is_not_an_id_is_refused_with_no_document() {
|
||||
let (mut failures, mut checked) = (vec![], 0);
|
||||
for spec in command_specs() {
|
||||
if !cmd::params::takes_key(spec.params, "ids") {
|
||||
continue;
|
||||
}
|
||||
checked += 1;
|
||||
for bad in [json!(["abc"]), json!([1, true]), json!("abc")] {
|
||||
let mut s = Session::new();
|
||||
let r = s.execute(spec.id, &json!({ "ids": bad }));
|
||||
let want = match bad.as_array() {
|
||||
Some(a) => {
|
||||
let (index, value) = a.iter().enumerate().find(|(_, v)| !v.is_u64()).unwrap();
|
||||
format!("`ids` entry {index} is {value}, not a non-negative integer object id")
|
||||
}
|
||||
None => "ids must be an array of non-negative integer object ids".to_string(),
|
||||
};
|
||||
match &r {
|
||||
Err(EngineError::BadParams { cmd: c, msg }) if c == spec.id && *msg == want => {}
|
||||
r => failures.push(format!("{} ids: {bad}: {r:.160?}", spec.id)),
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(failures.is_empty(), "{} fail:\n{}", failures.len(), failures.join("\n"));
|
||||
assert!(checked >= 100, "only {checked} commands take ids");
|
||||
}
|
||||
|
||||
@@ -2104,7 +2104,7 @@ fn type_menu_and_find_commands_refuse_a_param_of_the_wrong_type() {
|
||||
let switch = |k: &str| format!("`{k}` must be true or false");
|
||||
let scope = "`scope` must be one of selection, document".to_string();
|
||||
let not_a_list = "ids must be an array of non-negative integer object ids".to_string();
|
||||
let not_an_id = "invalid object id \"x\": expected a non-negative integer".to_string();
|
||||
let not_an_id = |entry: usize| format!("`ids` entry {entry} is \"x\", not a non-negative integer object id");
|
||||
for (cmd, p, want) in [
|
||||
("edit.findReplace", json!({"find": "cat", "replace": 5}), string("replace")),
|
||||
("edit.findReplace", json!({"find": 5, "replace": "dog"}), string("find")),
|
||||
@@ -2112,7 +2112,8 @@ fn type_menu_and_find_commands_refuse_a_param_of_the_wrong_type() {
|
||||
("edit.findReplace", json!({"find": "cat", "replace": "dog", "wholeWord": "yes"}), switch("wholeWord")),
|
||||
("edit.findReplace", json!({"find": "cat", "replace": "dog", "ids": t.0}), not_a_list),
|
||||
("edit.findReplace", json!({"find": "cat", "replace": "dog", "ids": [t.0, 999_999]}), "no such object id 999999".to_string()),
|
||||
("edit.findReplace", json!({"find": "cat", "replace": "dog", "ids": ["x"]}), not_an_id),
|
||||
("edit.findReplace", json!({"find": "cat", "replace": "dog", "ids": ["x"]}), not_an_id(0)),
|
||||
("edit.findReplace", json!({"find": "cat", "replace": "dog", "ids": [t.0, "x"]}), not_an_id(1)),
|
||||
("edit.findNext", json!({"find": ["cat"]}), string("find")),
|
||||
("edit.findNext", json!({"find": "cat", "matchCase": 1}), switch("matchCase")),
|
||||
("edit.findNext", json!({"find": "cat", "wholeWord": "false"}), switch("wholeWord")),
|
||||
|
||||
@@ -74,6 +74,52 @@ fn object_filter_recolours_the_selection_as_one_undo_step() {
|
||||
assert!(matches!(s.execute("plugin.run", &json!({"id": DESATURATE_ID, "amount": "all"})), Err(EngineError::BadParams { .. })));
|
||||
}
|
||||
|
||||
/// `plugin.run` refuses an `ids` list with an entry that is not an object id, with an error naming
|
||||
/// the key, the entry and its value, and changes nothing. With `ids` left out or null the filter
|
||||
/// runs on the selection, as it does on the list of the selection.
|
||||
#[test]
|
||||
fn object_filter_ids_entries_must_be_ids_and_ids_left_out_take_the_selection() {
|
||||
// A session with the plug-in, a red and a blue rectangle, and the red one selected. The
|
||||
// document has no creation date, so documents made in different seconds compare equal.
|
||||
let fresh = || {
|
||||
let mut s = Session::new();
|
||||
run(&mut s, "file.new", json!({"width": 400, "height": 200, "created": null}));
|
||||
install(&mut s, DESATURATE);
|
||||
let a = rect(&mut s, 10.0, "#ff0000");
|
||||
let b = rect(&mut s, 40.0, "#0000ff");
|
||||
run(&mut s, "select.set", json!({"ids": [a.0]}));
|
||||
(s, a, b)
|
||||
};
|
||||
let (_, a, b) = fresh();
|
||||
for (bad, entry) in [(json!(["abc"]), r#"entry 0 is "abc""#), (json!([a.0, true]), "entry 1 is true"), (json!([{}]), "entry 0 is {}")] {
|
||||
let (mut s, ..) = fresh();
|
||||
let st = s.doc().unwrap();
|
||||
let before = (st.doc.clone(), st.selection.clone(), st.history.undo.len());
|
||||
let r = s.execute("plugin.run", &json!({"id": DESATURATE_ID, "amount": 100, "ids": bad}));
|
||||
let want = format!("`ids` {entry}, not a non-negative integer object id");
|
||||
assert!(matches!(&r, Err(EngineError::BadParams { cmd, msg }) if cmd == "plugin.run" && *msg == want), "{bad}: {r:?}");
|
||||
let st = s.doc().unwrap();
|
||||
assert!((st.doc.clone(), st.selection.clone(), st.history.undo.len()) == before, "{bad}: something changed");
|
||||
}
|
||||
let gray = |s: &Session, id: NodeId| {
|
||||
let [r, g, b] = node(s, id).appearance.fill_paint().color().unwrap().to_rgb_uncalibrated();
|
||||
(r - g).abs() < 1e-6 && (g - b).abs() < 1e-6
|
||||
};
|
||||
let mut docs = vec![];
|
||||
for ids in [None, Some(Value::Null), Some(json!([a.0]))] {
|
||||
let (mut s, ..) = fresh();
|
||||
let mut p = json!({"id": DESATURATE_ID, "amount": 100});
|
||||
if let Some(ids) = &ids {
|
||||
p["ids"] = ids.clone();
|
||||
}
|
||||
let r = run(&mut s, "plugin.run", p);
|
||||
assert_eq!(r["ids"], json!([a.0]), "{ids:?}");
|
||||
assert!(gray(&s, a) && !gray(&s, b), "{ids:?}: only the selected rectangle turns gray");
|
||||
docs.push(s.doc().unwrap().doc.clone());
|
||||
}
|
||||
assert!(docs.windows(2).all(|w| w[0] == w[1]), "left out, null and the selection's list give the same document");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_filters_add_replace_and_generate_objects() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -208,7 +208,7 @@ fn id_errors_name_the_command_run() {
|
||||
for (given, want) in [
|
||||
(json!([999]), "no such object id 999".to_string()),
|
||||
(json!([999, layer]), "no such object id 999".to_string()),
|
||||
(json!([ids[0], ids[1], -1]), "invalid object id -1: expected a non-negative integer".to_string()),
|
||||
(json!([ids[0], ids[1], -1]), "`ids` entry 2 is -1, not a non-negative integer object id".to_string()),
|
||||
(json!(ids[0]), "ids must be an array of non-negative integer object ids".to_string()),
|
||||
] {
|
||||
let mut p = match *cmd {
|
||||
|
||||
@@ -4657,6 +4657,7 @@ axis Both И двете
|
||||
@msg object ids are non-negative integers, not {v} идентификаторите на обектите са неотрицателни цели числа, а не {v}
|
||||
@msg {key} must be an array of non-negative integer object ids {key} трябва да бъде масив от идентификатори на обекти, които са неотрицателни цели числа
|
||||
@msg invalid object id {value}: expected a non-negative integer невалиден идентификатор на обект {value}: очаква се неотрицателно цяло число
|
||||
@msg `{key}` entry {index} is {value}, not a non-negative integer object id елемент {index} на `{key}` е {value}, а не идентификатор на обект (неотрицателно цяло число)
|
||||
@msg no such object id {value} няма обект с идентификатор {value}
|
||||
@msg automation filesystem access is not granted: read authority is absent: {path} достъпът на автоматизацията до файловата система не е разрешен: липсва право за четене: {path}
|
||||
@msg automation filesystem access is not granted: write authority is absent: {path} достъпът на автоматизацията до файловата система не е разрешен: липсва право за запис: {path}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5014,6 +5014,7 @@ imageTrace Outline Obris
|
||||
@msg object ids are non-negative integers, not {v} ID-ovi objekata su nenegativni cijeli brojevi, a ne {v}
|
||||
@msg {key} must be an array of non-negative integer object ids {key} mora biti niz nenegativnih cjelobrojnih ID-ova objekata
|
||||
@msg invalid object id {value}: expected a non-negative integer nevažeći ID objekta {value}: očekuje se nenegativan cijeli broj
|
||||
@msg `{key}` entry {index} is {value}, not a non-negative integer object id stavka {index} u `{key}` je {value}, a ne ID objekta (nenegativan cijeli broj)
|
||||
@msg no such object id {value} nema objekta s ID-om {value}
|
||||
@msg id {value} is a layer, not an object id {value} je sloj, a ne objekat
|
||||
@msg id {value} is a layer, not an object; move layers with layer.move id {value} je sloj, a ne objekat; slojeve pomjerajte s layer.move
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5207,6 +5207,7 @@ digits Malayalam Malaiàlam
|
||||
@msg invalid image data for `{0}` dades d’imatge no vàlides per a `{0}`
|
||||
@msg invalid native panel set conjunt de panells natius no vàlid
|
||||
@msg invalid object id {value}: expected a non-negative integer id d’objecte {value} no vàlid: s’esperava un enter no negatiu
|
||||
@msg `{key}` entry {index} is {value}, not a non-negative integer object id l’entrada {index} de `{key}` és {value}, no un id d’objecte (un enter no negatiu)
|
||||
@msg Invalid panel layout; restored the default dock Disposició de panells no vàlida; s’ha restaurat l’acoblament per defecte
|
||||
@msg invalid parameters for `{cmd}`: {msg} paràmetres no vàlids per a `{cmd}`: {msg}
|
||||
@msg invalid PDF setting: {0} configuració del PDF no vàlida: {0}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5081,6 +5081,7 @@ imageTrace Outline Obrys
|
||||
@msg object ids are non-negative integers, not {v} ID objektů jsou nezáporná celá čísla, ne {v}
|
||||
@msg {key} must be an array of non-negative integer object ids {key} musí být pole ID objektů, která jsou nezáporná celá čísla
|
||||
@msg invalid object id {value}: expected a non-negative integer neplatné ID objektu {value}: očekává se nezáporné celé číslo
|
||||
@msg `{key}` entry {index} is {value}, not a non-negative integer object id položka {index} v `{key}` je {value}, nikoli ID objektu (nezáporné celé číslo)
|
||||
@msg no such object id {value} objekt s ID {value} neexistuje
|
||||
@msg id {value} is a layer, not an object ID {value} je vrstva, nikoli objekt
|
||||
@msg id {value} is a layer, not an object; move layers with layer.move ID {value} je vrstva, nikoli objekt; vrstvy přesunete pomocí layer.move
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4704,6 +4704,7 @@ imageTrace Outline Kontur
|
||||
@msg object ids are non-negative integers, not {v} objekt-id'er er ikke-negative heltal, ikke {v}
|
||||
@msg {key} must be an array of non-negative integer object ids {key} skal være et array af objekt-id'er, der er ikke-negative heltal
|
||||
@msg invalid object id {value}: expected a non-negative integer ugyldigt objekt-id {value}: et ikke-negativt heltal var forventet
|
||||
@msg `{key}` entry {index} is {value}, not a non-negative integer object id element {index} i `{key}` er {value}, ikke et objekt-id (et ikke-negativt heltal)
|
||||
@msg no such object id {value} der findes intet objekt med id {value}
|
||||
@msg automation filesystem access is not granted: read authority is absent: {path} automatiseringens adgang til filsystemet er ikke tildelt: læserettighed mangler: {path}
|
||||
@msg automation filesystem access is not granted: write authority is absent: {path} automatiseringens adgang til filsystemet er ikke tildelt: skriverettighed mangler: {path}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4735,6 +4735,7 @@ theme Appearance Erscheinungsbild
|
||||
@msg object ids are non-negative integers, not {v} Objekt-IDs sind nicht negative ganze Zahlen, nicht {v}
|
||||
@msg {key} must be an array of non-negative integer object ids {key} muss ein Array von Objekt-IDs sein, die nicht negative ganze Zahlen sind
|
||||
@msg invalid object id {value}: expected a non-negative integer ungültige Objekt-ID {value}: erwartet wird eine nicht negative ganze Zahl
|
||||
@msg `{key}` entry {index} is {value}, not a non-negative integer object id Eintrag {index} von `{key}` ist {value}, keine Objekt-ID (nicht negative ganze Zahl)
|
||||
@msg no such object id {value} kein Objekt mit der ID {value}
|
||||
@msg id {value} is a layer, not an object die ID {value} ist eine Ebene, kein Objekt
|
||||
@msg id {value} is a layer, not an object; move layers with layer.move die ID {value} ist eine Ebene, kein Objekt; verschieben Sie Ebenen mit layer.move
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4759,6 +4759,7 @@ digits Malayalam Μαλαγιάλαμ
|
||||
@msg object ids are non-negative integers, not {v} τα αναγνωριστικά αντικειμένων πρέπει να είναι μη αρνητικοί ακέραιοι, όχι {v}
|
||||
@msg {key} must be an array of non-negative integer object ids {key}: πρέπει να είναι πίνακας αναγνωριστικών αντικειμένων που είναι μη αρνητικοί ακέραιοι
|
||||
@msg invalid object id {value}: expected a non-negative integer μη έγκυρο αναγνωριστικό αντικειμένου {value}: αναμένεται μη αρνητικός ακέραιος
|
||||
@msg `{key}` entry {index} is {value}, not a non-negative integer object id το στοιχείο {index} του `{key}` είναι {value}, όχι αναγνωριστικό αντικειμένου (μη αρνητικός ακέραιος)
|
||||
@msg no such object id {value} δεν υπάρχει αντικείμενο με αναγνωριστικό {value}
|
||||
@msg id {value} is a layer, not an object το αναγνωριστικό {value} είναι επίπεδο, όχι αντικείμενο
|
||||
@msg id {value} is a layer, not an object; move layers with layer.move το αναγνωριστικό {value} είναι επίπεδο, όχι αντικείμενο· μετακινήστε τα επίπεδα με το layer.move
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 168 and column 5.
|
@@ -4798,6 +4798,7 @@ axis Both Ambos
|
||||
@msg object ids are non-negative integers, not {v} los identificadores de objeto son enteros no negativos, no {v}
|
||||
@msg {key} must be an array of non-negative integer object ids {key} debe ser una lista de identificadores de objeto que sean enteros no negativos
|
||||
@msg invalid object id {value}: expected a non-negative integer identificador de objeto no válido {value}: se esperaba un entero no negativo
|
||||
@msg `{key}` entry {index} is {value}, not a non-negative integer object id la entrada {index} de `{key}` es {value}, no un identificador de objeto (un entero no negativo)
|
||||
@msg no such object id {value} no existe el objeto con el identificador {value}
|
||||
@msg id {value} is a layer, not an object el identificador {value} es una capa, no un objeto
|
||||
@msg id {value} is a layer, not an object; move layers with layer.move el identificador {value} es una capa, no un objeto; mueva las capas con layer.move
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4983,6 +4983,7 @@ imageTrace Outline Ääriviiva
|
||||
@msg object ids are non-negative integers, not {v} objektitunnukset ovat ei-negatiivisia kokonaislukuja, ei {v}
|
||||
@msg {key} must be an array of non-negative integer object ids {key} on oltava taulukko objektitunnuksia, jotka ovat ei-negatiivisia kokonaislukuja
|
||||
@msg invalid object id {value}: expected a non-negative integer virheellinen objektitunnus {value}: odotettiin ei-negatiivista kokonaislukua
|
||||
@msg `{key}` entry {index} is {value}, not a non-negative integer object id `{key}`-luettelon kohde {index} on {value}, ei objektitunnus (ei-negatiivinen kokonaisluku)
|
||||
@msg no such object id {value} objektia tunnuksella {value} ei ole
|
||||
@msg id {value} is a layer, not an object tunnus {value} on taso, ei objekti
|
||||
@msg id {value} is a layer, not an object; move layers with layer.move tunnus {value} on taso, ei objekti; siirrä tasoja komennolla layer.move
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4806,6 +4806,7 @@ axis Both Les deux
|
||||
@msg object ids are non-negative integers, not {v} les identifiants d’objet sont des entiers positifs ou nuls, pas {v}
|
||||
@msg {key} must be an array of non-negative integer object ids {key} doit être un tableau d’identifiants d’objet, entiers positifs ou nuls
|
||||
@msg invalid object id {value}: expected a non-negative integer identifiant d’objet {value} non valide (un entier positif ou nul est attendu)
|
||||
@msg `{key}` entry {index} is {value}, not a non-negative integer object id l’entrée {index} de `{key}` vaut {value}, pas un identifiant d’objet (entier positif ou nul)
|
||||
@msg no such object id {value} aucun objet ne porte l’identifiant {value}
|
||||
@msg id {value} is a layer, not an object l’identifiant {value} désigne un calque, pas un objet
|
||||
@msg id {value} is a layer, not an object; move layers with layer.move l’identifiant {value} désigne un calque, pas un objet ; déplacez les calques avec layer.move
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6379,6 +6379,7 @@
|
||||
@msg invalid image data for `{0}` `{0}` के लिए अमान्य इमेज डेटा
|
||||
@msg invalid native panel set अमान्य मूल पैनल सेट
|
||||
@msg invalid object id {value}: expected a non-negative integer अमान्य ऑब्जेक्ट id {value}: एक गैर-नकारात्मक पूर्णांक की उम्मीद
|
||||
@msg `{key}` entry {index} is {value}, not a non-negative integer object id `{key}` की प्रविष्टि {index} {value} है, गैर-नकारात्मक पूर्णांक ऑब्जेक्ट id नहीं
|
||||
@msg Invalid panel layout; restored the default dock अमान्य पैनल लेआउट; डिफ़ॉल्ट डॉक पुनर्स्थापित किया गया
|
||||
@msg invalid parameter name {k} अमान्य पैरामीटर नाम {k}
|
||||
@msg invalid parameters for `{cmd}`: {msg} `{cmd}` के लिए अमान्य पैरामीटर: {msg}
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -5119,6 +5119,7 @@ imageTrace Outline Körvonal
|
||||
@msg object ids are non-negative integers, not {v} az objektumazonosító nemnegatív egész szám, ez nem az: {v}
|
||||
@msg {key} must be an array of non-negative integer object ids {key}: objektumazonosítók tömbje kell, mindegyik nemnegatív egész szám
|
||||
@msg invalid object id {value}: expected a non-negative integer érvénytelen objektumazonosító: {value} (nemnegatív egész szám kell)
|
||||
@msg `{key}` entry {index} is {value}, not a non-negative integer object id `{key}` {index} indexű eleme {value}, nem objektumazonosító (nemnegatív egész szám kell)
|
||||
@msg no such object id {value} nincs ilyen objektumazonosító: {value}
|
||||
@msg id {value} is a layer, not an object a(z) {value} azonosító egy réteg, nem objektum
|
||||
@msg id {value} is a layer, not an object; move layers with layer.move a(z) {value} azonosító egy réteg, nem objektum; a rétegeket a layer.move paranccsal helyezheti át
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4704,6 +4704,7 @@ imageTrace Outline Kontur
|
||||
@msg object ids are non-negative integers, not {v} id objek adalah bilangan bulat non-negatif, bukan {v}
|
||||
@msg {key} must be an array of non-negative integer object ids {key} harus berupa larik id objek yang berupa bilangan bulat non-negatif
|
||||
@msg invalid object id {value}: expected a non-negative integer id objek {value} tidak valid: harus berupa bilangan bulat non-negatif
|
||||
@msg `{key}` entry {index} is {value}, not a non-negative integer object id entri {index} dari `{key}` adalah {value}, bukan id objek (bilangan bulat non-negatif)
|
||||
@msg no such object id {value} tidak ada objek dengan id {value}
|
||||
@msg automation filesystem access is not granted: read authority is absent: {path} akses sistem berkas otomatisasi tidak diberikan: otoritas baca tidak ada: {path}
|
||||
@msg automation filesystem access is not granted: write authority is absent: {path} akses sistem berkas otomatisasi tidak diberikan: otoritas tulis tidak ada: {path}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2270 and column 5.
|
@@ -4840,6 +4840,7 @@ axis Both Entrambi
|
||||
@msg object ids are non-negative integers, not {v} gli ID oggetto sono interi non negativi, non {v}
|
||||
@msg {key} must be an array of non-negative integer object ids {key} deve essere un array di ID oggetto interi non negativi
|
||||
@msg invalid object id {value}: expected a non-negative integer ID oggetto {value} non valido: è richiesto un intero non negativo
|
||||
@msg `{key}` entry {index} is {value}, not a non-negative integer object id la voce {index} di `{key}` è {value}, non un ID oggetto (intero non negativo)
|
||||
@msg no such object id {value} nessun oggetto con ID {value}
|
||||
@msg id {value} is a layer, not an object l’ID {value} è un livello, non un oggetto
|
||||
@msg id {value} is a layer, not an object; move layers with layer.move l’ID {value} è un livello, non un oggetto; sposta i livelli con layer.move
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4881,6 +4881,7 @@ swatch Bright Violet ビビッドバイオレット
|
||||
@msg object ids are non-negative integers, not {v} オブジェクト ID は 0 以上の整数です ({v} は使えません)
|
||||
@msg {key} must be an array of non-negative integer object ids {key} にはオブジェクト ID (0 以上の整数) の配列を指定してください
|
||||
@msg invalid object id {value}: expected a non-negative integer オブジェクト ID {value} は正しくありません: 0 以上の整数を指定してください
|
||||
@msg `{key}` entry {index} is {value}, not a non-negative integer object id `{key}` の要素 {index} は {value} で、オブジェクト ID (0 以上の整数) ではありません
|
||||
@msg no such object id {value} ID が {value} のオブジェクトは見つかりません
|
||||
@msg id {value} is a layer, not an object ID {value} はオブジェクトではなくレイヤーです
|
||||
@msg id {value} is a layer, not an object; move layers with layer.move ID {value} はオブジェクトではなくレイヤーです。レイヤーは layer.move で移動してください
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4500,6 +4500,7 @@ font-technology Variable ცვლადი
|
||||
@msg object ids are non-negative integers, not {v} ობიექტების id არაუარყოფითი მთელი რიცხვებია და არა {v}
|
||||
@msg {key} must be an array of non-negative integer object ids {key} უნდა იყოს ობიექტების id-ების მასივი, სადაც ყოველი id არაუარყოფითი მთელი რიცხვია
|
||||
@msg invalid object id {value}: expected a non-negative integer ობიექტის არასწორი id {value}: მოსალოდნელია არაუარყოფითი მთელი რიცხვი
|
||||
@msg `{key}` entry {index} is {value}, not a non-negative integer object id `{key}`-ის ელემენტი {index} არის {value} და არა ობიექტის id (არაუარყოფითი მთელი რიცხვი)
|
||||
@msg no such object id {value} ობიექტი id-ით {value} არ არსებობს
|
||||
@msg id {value} is a layer, not an object id {value} შრეა და არა ობიექტი
|
||||
@msg id {value} is a layer, not an object; move layers with layer.move id {value} შრეა და არა ობიექტი; შრეების გადასატანად გამოიყენეთ layer.move
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -4808,6 +4808,7 @@ theme Appearance 외관
|
||||
@msg object ids are non-negative integers, not {v} 개체 id는 음이 아닌 정수여야 하며 {v}은(는) 안 됩니다
|
||||
@msg {key} must be an array of non-negative integer object ids {key}은(는) 음이 아닌 정수인 개체 id의 배열이어야 합니다
|
||||
@msg invalid object id {value}: expected a non-negative integer 개체 id {value}이(가) 올바르지 않습니다: 음이 아닌 정수여야 합니다
|
||||
@msg `{key}` entry {index} is {value}, not a non-negative integer object id `{key}`의 항목 {index}은(는) {value}이며, 음이 아닌 정수인 개체 id가 아닙니다
|
||||
@msg no such object id {value} id가 {value}인 개체가 없습니다
|
||||
@msg id {value} is a layer, not an object id {value}은(는) 개체가 아닌 레이어입니다
|
||||
@msg id {value} is a layer, not an object; move layers with layer.move id {value}은(는) 개체가 아닌 레이어입니다. 레이어는 layer.move로 이동하세요
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4716,6 +4716,7 @@ imageTrace Outline Contour
|
||||
@msg object ids are non-negative integers, not {v} object-id's zijn niet-negatieve gehele getallen, niet {v}
|
||||
@msg {key} must be an array of non-negative integer object ids {key} moet een array van object-id's zijn die niet-negatieve gehele getallen zijn
|
||||
@msg invalid object id {value}: expected a non-negative integer ongeldige object-id {value}: een niet-negatief geheel getal verwacht
|
||||
@msg `{key}` entry {index} is {value}, not a non-negative integer object id item {index} van `{key}` is {value}, geen object-id (een niet-negatief geheel getal)
|
||||
@msg no such object id {value} er is geen object met id {value}
|
||||
@msg automation filesystem access is not granted: read authority is absent: {path} toegang tot het bestandssysteem voor automatisering is niet verleend: de leesbevoegdheid ontbreekt: {path}
|
||||
@msg automation filesystem access is not granted: write authority is absent: {path} toegang tot het bestandssysteem voor automatisering is niet verleend: de schrijfbevoegdheid ontbreekt: {path}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4906,6 +4906,7 @@ imageTrace Outline Kontur
|
||||
@msg object ids are non-negative integers, not {v} identyfikatory obiektów to nieujemne liczby całkowite, nie {v}
|
||||
@msg {key} must be an array of non-negative integer object ids {key} musi być tablicą identyfikatorów obiektów, które są nieujemnymi liczbami całkowitymi
|
||||
@msg invalid object id {value}: expected a non-negative integer nieprawidłowy identyfikator obiektu {value}: oczekiwano nieujemnej liczby całkowitej
|
||||
@msg `{key}` entry {index} is {value}, not a non-negative integer object id element {index} w `{key}` to {value}, a nie identyfikator obiektu (nieujemna liczba całkowita)
|
||||
@msg no such object id {value} brak obiektu o identyfikatorze {value}
|
||||
@msg id {value} is a layer, not an object identyfikator {value} oznacza warstwę, a nie obiekt
|
||||
@msg id {value} is a layer, not an object; move layers with layer.move identyfikator {value} oznacza warstwę, a nie obiekt; warstwy przenosi się za pomocą layer.move
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4344,6 +4344,7 @@ axis Both Ambos
|
||||
@msg object ids are non-negative integers, not {v} os ids de objeto são inteiros não negativos, não {v}
|
||||
@msg {key} must be an array of non-negative integer object ids {key} deve ser uma matriz de ids de objeto que sejam inteiros não negativos
|
||||
@msg invalid object id {value}: expected a non-negative integer id de objeto inválido {value}: era esperado um inteiro não negativo
|
||||
@msg `{key}` entry {index} is {value}, not a non-negative integer object id a entrada {index} de `{key}` é {value}, não um id de objeto (inteiro não negativo)
|
||||
@msg no such object id {value} não existe objeto com o id {value}
|
||||
@msg id {value} is a layer, not an object o id {value} é uma camada, não um objeto
|
||||
@msg id {value} is a layer, not an object; move layers with layer.move o id {value} é uma camada, não um objeto; mova camadas com layer.move
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -4837,6 +4837,7 @@ axis Both Оба
|
||||
@msg object ids are non-negative integers, not {v} идентификаторы объектов — неотрицательные целые числа, а не {v}
|
||||
@msg {key} must be an array of non-negative integer object ids {key} должно быть массивом идентификаторов объектов, которые являются неотрицательными целыми числами
|
||||
@msg invalid object id {value}: expected a non-negative integer недопустимый идентификатор объекта {value}: ожидается неотрицательное целое число
|
||||
@msg `{key}` entry {index} is {value}, not a non-negative integer object id элемент {index} в `{key}` равен {value}, а не идентификатору объекта (неотрицательному целому числу)
|
||||
@msg no such object id {value} объект с идентификатором {value} не существует
|
||||
@msg id {value} is a layer, not an object идентификатор {value} относится к слою, а не к объекту
|
||||
@msg id {value} is a layer, not an object; move layers with layer.move идентификатор {value} относится к слою, а не к объекту; перемещайте слои с помощью layer.move
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4715,6 +4715,7 @@ axis Both Båda
|
||||
@msg object ids are non-negative integers, not {v} objekt-id:n är icke-negativa heltal, inte {v}
|
||||
@msg {key} must be an array of non-negative integer object ids {key} måste vara en array med objekt-id:n som är icke-negativa heltal
|
||||
@msg invalid object id {value}: expected a non-negative integer ogiltigt objekt-id {value}: ett icke-negativt heltal förväntades
|
||||
@msg `{key}` entry {index} is {value}, not a non-negative integer object id post {index} i `{key}` är {value}, inte ett objekt-id (ett icke-negativt heltal)
|
||||
@msg no such object id {value} det finns inget objekt med id {value}
|
||||
@msg id {value} is a layer, not an object id {value} är ett lager, inte ett objekt
|
||||
@msg id {value} is a layer, not an object; move layers with layer.move id {value} är ett lager, inte ett objekt; flytta lager med layer.move
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -2062,9 +2062,9 @@ fn dataset_import_cell_errors_are_translated() {
|
||||
|
||||
/// The errors of the parameter checks that many commands share (a number given as text, a missing
|
||||
/// number or index, a fraction for a whole number, `ids` that aren't a list of object ids, an
|
||||
/// object id that is negative or names no object, a date that isn't Unix seconds) fill their
|
||||
/// templates, and each language shows the value in the message, such as a quoted `"60 mm"`, as
|
||||
/// given: in its row's translation, or in English where it has none.
|
||||
/// `ids` entry or an `id` that is negative, an object id that names no object, a date that isn't
|
||||
/// Unix seconds) fill their templates, and each language shows the value in the message, such as a
|
||||
/// quoted `"60 mm"`, as given: in its row's translation, or in English where it has none.
|
||||
#[test]
|
||||
fn shared_param_check_errors_show_their_values_as_given() {
|
||||
use serde_json::json;
|
||||
@@ -2075,7 +2075,7 @@ fn shared_param_check_errors_show_their_values_as_given() {
|
||||
s.execute("select.all", &json!({})).unwrap();
|
||||
// A command, its params, the English template of its error and the template's values.
|
||||
type Case = (&'static str, serde_json::Value, &'static str, &'static [(&'static str, &'static str)]);
|
||||
let cases: [Case; 10] = [
|
||||
let cases: [Case; 11] = [
|
||||
("object.move", json!({"dx": "60 mm"}), "`{key}` must be a number, not {v}", &[("key", "dx"), ("v", r#""60 mm""#)]),
|
||||
("object.rotate", json!({"angle": true}), "`{key}` must be a number, not {v}", &[("key", "angle"), ("v", "true")]),
|
||||
("object.setBounds", json!({"width": [1, 2]}), "`{key}` must be a number, not {v}", &[("key", "width"), ("v", "[1,2]")]),
|
||||
@@ -2083,7 +2083,13 @@ fn shared_param_check_errors_show_their_values_as_given() {
|
||||
("appearance.setItem", json!({}), "missing integer `{key}`", &[("key", "index")]),
|
||||
("object.transformEach", json!({"reference": 2.5}), "`{key}` must be a whole number", &[("key", "reference")]),
|
||||
("edit.clear", json!({"ids": "all"}), "{key} must be an array of non-negative integer object ids", &[("key", "ids")]),
|
||||
("edit.clear", json!({"ids": [-1]}), "invalid object id {value}: expected a non-negative integer", &[("value", "-1")]),
|
||||
(
|
||||
"edit.clear",
|
||||
json!({"ids": [-1]}),
|
||||
"`{key}` entry {index} is {value}, not a non-negative integer object id",
|
||||
&[("key", "ids"), ("index", "0"), ("value", "-1")],
|
||||
),
|
||||
("paint.invert", json!({"id": -1}), "invalid object id {value}: expected a non-negative integer", &[("value", "-1")]),
|
||||
("edit.clear", json!({"ids": [99999]}), "no such object id {value}", &[("value", "99999")]),
|
||||
("file.new", json!({"created": "yesterday"}), "{key} must be Unix seconds (an integer) or null", &[("key", "created")]),
|
||||
];
|
||||
|
||||
@@ -5532,6 +5532,7 @@ theme Appearance Görünüm
|
||||
@msg missing integer `{key}` `{key}` tam sayısı eksik
|
||||
@msg {key} must be an array of non-negative integer object ids {key}, negatif olmayan tam sayı nesne kimlikleri dizisi olmalıdır
|
||||
@msg invalid object id {value}: expected a non-negative integer geçersiz nesne kimliği {value}: negatif olmayan bir tam sayı bekleniyordu
|
||||
@msg `{key}` entry {index} is {value}, not a non-negative integer object id `{key}` içindeki {index} dizinli öğe {value}, negatif olmayan tam sayı bir nesne kimliği değil
|
||||
@msg no such object id {value} böyle bir nesne kimliği yok: {value}
|
||||
@msg id {value} is a layer, not an object {value} kimliği bir katmana ait, bir nesneye değil
|
||||
@msg id {value} is a layer, not an object; move layers with layer.move {value} kimliği bir katmana ait, bir nesneye değil; katmanları layer.move ile taşıyın
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -4824,6 +4824,7 @@ axis Both Обидві
|
||||
@msg object ids are non-negative integers, not {v} ідентифікатори об’єктів — невід’ємні цілі числа, а не {v}
|
||||
@msg {key} must be an array of non-negative integer object ids {key} має бути масивом ідентифікаторів об’єктів, які є невід’ємними цілими числами
|
||||
@msg invalid object id {value}: expected a non-negative integer неприпустимий ідентифікатор об’єкта {value}: очікується невід’ємне ціле число
|
||||
@msg `{key}` entry {index} is {value}, not a non-negative integer object id елемент {index} у `{key}` дорівнює {value}, а не ідентифікатору об’єкта (невід’ємному цілому числу)
|
||||
@msg no such object id {value} об’єкт з ідентифікатором {value} не існує
|
||||
@msg id {value} is a layer, not an object ідентифікатор {value} належить шару, а не об’єкту
|
||||
@msg id {value} is a layer, not an object; move layers with layer.move ідентифікатор {value} належить шару, а не об’єкту; переміщуйте шари за допомогою layer.move
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 182 and column 5.
|
@@ -4773,6 +4773,7 @@ theme Appearance Giao diện
|
||||
@msg object ids are non-negative integers, not {v} id đối tượng là số nguyên không âm, không phải {v}
|
||||
@msg {key} must be an array of non-negative integer object ids {key} phải là một mảng id đối tượng là số nguyên không âm
|
||||
@msg invalid object id {value}: expected a non-negative integer id đối tượng {value} không hợp lệ: cần một số nguyên không âm
|
||||
@msg `{key}` entry {index} is {value}, not a non-negative integer object id mục {index} của `{key}` là {value}, không phải id đối tượng (số nguyên không âm)
|
||||
@msg no such object id {value} không có đối tượng nào có id {value}
|
||||
@msg id {value} is a layer, not an object id {value} là một lớp, không phải đối tượng
|
||||
@msg id {value} is a layer, not an object; move layers with layer.move id {value} là một lớp, không phải đối tượng; hãy di chuyển lớp bằng layer.move
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 204 and column 5.
|
@@ -4590,6 +4590,7 @@ imageTrace Outline 轮廓
|
||||
@msg object ids are non-negative integers, not {v} 对象 id 是非负整数,而不是 {v}
|
||||
@msg {key} must be an array of non-negative integer object ids {key} 必须是由非负整数对象 id 组成的数组
|
||||
@msg invalid object id {value}: expected a non-negative integer 无效的对象 id {value}:应为非负整数
|
||||
@msg `{key}` entry {index} is {value}, not a non-negative integer object id `{key}` 中索引 {index} 处的项为 {value},不是非负整数对象 id
|
||||
@msg no such object id {value} 没有 id 为 {value} 的对象
|
||||
@msg id {value} is a layer, not an object id {value} 是图层,不是对象
|
||||
@msg id {value} is a layer, not an object; move layers with layer.move id {value} 是图层,不是对象;请使用 layer.move 移动图层
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5411,6 +5411,7 @@ lighting Light 光源
|
||||
@msg {key} must be Unix seconds (an integer) or null {key} 必須是以秒為單位的 Unix 時間 (整數) 或 null
|
||||
@msg {key} must be an array of non-negative integer object ids {key} 必須是由非負整數物件 id 組成的陣列
|
||||
@msg invalid object id {value}: expected a non-negative integer 無效的物件 id {value}:應為非負整數
|
||||
@msg `{key}` entry {index} is {value}, not a non-negative integer object id `{key}` 中索引 {index} 處的項目為 {value},不是非負整數物件 id
|
||||
@msg no such object id {value} 沒有 id 為 {value} 的物件
|
||||
@msg id {value} is a layer, not an object id {value} 是圖層,不是物件
|
||||
@msg id {value} is a layer, not an object; move layers with layer.move id {value} 是圖層,不是物件;請使用 layer.move 移動圖層
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
+6
-2
@@ -403,7 +403,10 @@ counterparts, `image.info`, `links.info`, `links.goTo`, `links.unembed`, `shaper
|
||||
`transparency.editOpacityMask`, `transparency.viewOpacityMask`, `symbol.edit`, the `object.blend.spine.*` commands
|
||||
and `path.curvature`) refuse an `id` that is not a non-negative integer (`"abc"`, `"5"`, `true`) with an error and
|
||||
change nothing; an omitted or null `id` keeps the default. A command that leaves out an object selected inside a
|
||||
selected group also leaves it out when `ids` list it together with a group that holds it.
|
||||
selected group also leaves it out when `ids` list it together with a group that holds it. Every command that takes
|
||||
`ids` (or `slices`, for the slice commands) refuses a list with an entry that is not a non-negative integer, or a value
|
||||
that is not a list, and changes nothing; it does not act on the other entries. The error for an entry names the key,
|
||||
the entry's index (from 0) and its value: "`ids` entry 1 is true, not a non-negative integer object id".
|
||||
|
||||
Paint values (`fill`, `stroke`) accept `"#rrggbb"`, `"none"`, `[r,g,b]` (0..1), `{"c","m","y","k"}`, `{"gray"}`,
|
||||
or a full `paint.setFill` params object (`{"gradient": …}`, `{"swatch": "name"}`).
|
||||
@@ -4937,7 +4940,8 @@ The Slice tool (`slice`, Shift+K) and the Slice Selection tool (`sliceSelection`
|
||||
`object.slice.create {x, y, width, height}` (the Slice tool's drag; it snaps to ruler guides), `object.slice.select
|
||||
{slices?, toggle?}`, `object.slice.move {slices?, dx, dy}` (an object slice moves its object),
|
||||
`object.slice.setRect {id, x, y, width, height}` (user slices) and `object.slice.delete {slices?}`. Slice ids are user
|
||||
slice ids and the ids of objects with an object slice.
|
||||
slice ids and the ids of objects with an object slice. `object.slice.select` with `slices` left out or null deselects
|
||||
the slices.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"object.slice.create","params":{"x":0,"y":0,"width":200,"height":80}}}
|
||||
|
||||
@@ -729,6 +729,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- Native macOS menu bar, vector tool cursors, a ⌘K command palette, middle-button panning with any tool. Native menus explicitly end tracking on command selection, outside window presses and Escape; menu updates wait until tracking ends, and a dismissing press never starts a canvas gesture (M14.8).
|
||||
- The errors of the blend spine commands (Move Spine Point, Reshape Spine Segment, Add Spine Point and Delete Spine Point) show in the status bar in the interface language: each has a row in every catalog that translates all status and error messages and in Bulgarian (`blend_spine_param_errors_are_translated`).
|
||||
- The errors of the parameter checks that many commands share show in the status bar in the interface language: a value that is not a number where a number goes (`object.move` with `"dx": "60 mm"`, and `object.rotate`, `object.setBounds` and the other commands that call `check_numbers`), a missing number or index, `ids` that are not a list of object ids, an object id that is not a non-negative integer or names no object, and a date that is not Unix seconds. Each has a row in every catalog that translates all status and error messages, and in Bulgarian, Danish, Indonesian and Dutch (`shared_param_check_errors_are_translated`).
|
||||
- Commands that read their `ids` (or `slices`) list themselves refuse a list with an entry that is not a non-negative integer, and a value that is not a list, and change nothing; they no longer drop the entry and act on the rest (Copy Appearance From, the Edit Colors commands and Overprint Black, Find and Replace, `layer.highlight`, `layer.move`, `library.add`, the Links panel commands, `object.transform`, Make Pattern, the Repeat commands, the slice commands, Join, Average and Join, `plugin.run`, `symbol.edit`, Make Opacity Mask; `tests::ids_without_selection::an_ids_entry_that_is_not_an_id_is_refused_and_changes_nothing`). The error for an entry of an object id list names the key, the entry and its value ("`ids` entry 1 is true, not a non-negative integer object id"), with a row in every catalog that has the other object id errors.
|
||||
- The errors of Enter Isolation Mode (`object.isolate`) for a type object, for hidden or locked art and for a call with no `id` while nothing is selected ("missing id", which other commands that take an `id` also give), and the error of the Layers panel's target circle (`layer.target`) for an object outside the isolated group, show in the status bar in the interface language: each has a row in every catalog that translates all status and error messages, and in Bulgarian, Danish, Indonesian and Dutch.
|
||||
- An EPS or PostScript file that stops on a PostScript error gives the error's name as PostScript spells it (`typecheck`, `rangecheck` and the other names the reader raises, except `undefined`, whose message names the unknown name) followed by what it means, in the warning after opening and in the error when the file can't be opened. The status bar and the open-error dialog show the meaning in the interface language: each meaning has a row in every catalog (`postscript_error_meanings_are_translated`).
|
||||
- The macOS menu bar is built from the in-window menus (one model, `native_menu`) and laid out as a Mac app's: the VectorCraft menu holds About, Settings ▸ (each Preferences page, General… ⌘K), Language ▸, Appearance ▸ (UI brightness), Services, Hide VectorCraft ⌃⌘H (⌘H stays Hide Edges), Hide Others, Show All and Quit; Window gains Minimize ⌃⌘M, Zoom and Bring All to Front, and Help the system's search field. Its key equivalents go back to egui as key presses, so user shortcuts, focused text fields and the Type tool's input method behave as in the window, and nothing fires twice; enabled, check marks and labels update in place. `--in-window-menus` or `VECTORCRAFT_IN_WINDOW_MENUS=1` keeps the menus in the window; `ui.inspect` reports `nativeMenuBar`.
|
||||
|
||||
Reference in New Issue
Block a user