diff --git a/crates/engine/src/cmd/effectcmd.rs b/crates/engine/src/cmd/effectcmd.rs index 4fc2f6e71..01066e15e 100644 --- a/crates/engine/src/cmd/effectcmd.rs +++ b/crates/engine/src/cmd/effectcmd.rs @@ -141,6 +141,9 @@ fn check_revolve_count(n: &Node, cmd: &str) -> Result<()> { pub(crate) const ONE_3D: &str = "An object takes one 3D effect. Edit the existing effect's options, or remove it first."; +/// `effect.apply`: adds effect `effect` (or `id`) with `params`, its defaults filled in, to the +/// targets' active item as one undo step. A 3D effect must fit each target, which takes only one; +/// a Pathfinder effect on loose objects groups them first. → `{ids, index, item}`. pub(crate) fn apply(s: &mut Session, p: &Value) -> Result { const C: &str = "effect.apply"; let id = str_param(p, "effect").or_else(|| str_param(p, "id")).ok_or_else(|| bad(C, "missing `effect` id"))?; @@ -329,6 +332,9 @@ fn lighting_preset(s: &mut Session, p: &Value) -> Result { set_params(s, &options) } +/// `effect.setParams`: merges `params` into the effect at `index` of the targets' active item, and +/// sets its Warp style (`warpStyle`) or visibility (`visible`), as one undo step. They are checked +/// as `effect.apply` checks them, and a visible 3D effect must still fit. → `{ids}`. fn set_params(s: &mut Session, p: &Value) -> Result { const C: &str = "effect.setParams"; let index = index_param(p, "index", C)?; diff --git a/crates/engine/src/cmd/plugin.rs b/crates/engine/src/cmd/plugin.rs index 2a5d93b25..2855fcb08 100644 --- a/crates/engine/src/cmd/plugin.rs +++ b/crates/engine/src/cmd/plugin.rs @@ -59,6 +59,10 @@ fn plugin_by_id(cmd: &str, p: &Value) -> Result> { registry::get(id).ok_or_else(|| err(vectorcraft_plugins::Error::NotFound(id.into()))) } +/// `plugin.run`: runs filter `id` on `ids` (else the selected paths and compound paths) with its +/// parameters and `_context`, and applies its output as one undo step ([`objects::apply_output`]; +/// effect records as [`admit_effects`] admits them). → `{plugin, ids}`: what it made or changed, +/// which is then selected. fn run(s: &mut Session, p: &Value) -> Result { const C: &str = "plugin.run"; let plugin = plugin_by_id(C, p)?; diff --git a/crates/plugins/src/objects.rs b/crates/plugins/src/objects.rs index 43283b180..c1d581569 100644 --- a/crates/plugins/src/objects.rs +++ b/crates/plugins/src/objects.rs @@ -76,7 +76,7 @@ impl OutStroke { /// host's to say. pub type AdmitEffects<'a> = &'a dyn Fn(&[Effect], &Node) -> std::result::Result, String>; -/// The names plug-ins use for line joins and caps, as SVG's. +/// The name plug-ins use for line join `j`, as SVG's `stroke-linejoin`. fn join_name(j: LineJoin) -> &'static str { match j { LineJoin::Miter => "miter", @@ -85,6 +85,7 @@ fn join_name(j: LineJoin) -> &'static str { } } +/// The name plug-ins use for line cap `c`, as SVG's `stroke-linecap`. fn cap_name(c: LineCap) -> &'static str { match c { LineCap::Butt => "butt", @@ -265,6 +266,9 @@ fn opt<'a>(o: &'a serde_json::Map, key: &str) -> Option<&'a Value o.get(key).filter(|v| !v.is_null()) } +/// Object `v` of a plug-in's output (`at` names it in errors), its anchors added to `anchors` and +/// held to [`MAX_ANCHORS`]: each key it has checked, numbers clamped into range, as [`decode`] reads +/// them. fn decode_object(v: &Value, at: &str, anchors: &mut usize) -> Result { let o = v.as_object().ok_or_else(|| out_err(at, "an object must be a JSON object"))?; let mut out = OutObject::default(); diff --git a/crates/plugins/src/objects/tests.rs b/crates/plugins/src/objects/tests.rs index fca4cb545..1b21fee2c 100644 --- a/crates/plugins/src/objects/tests.rs +++ b/crates/plugins/src/objects/tests.rs @@ -46,6 +46,8 @@ fn targets_are_selected_paths_in_paint_order() { assert_eq!(filter_targets(&d, &[layer]), [ids[0], ids[2]]); } +/// Objects as plug-ins are given them (type, name, fill rule, fills, strokes, bounds); handed back +/// unchanged, they change nothing, swatch links included. #[test] fn objects_round_trip_through_the_json_model() { let (mut d, ids, _) = doc(); @@ -90,6 +92,8 @@ fn an_unchanged_path_stays_a_live_shape() { assert!(matches!(&d.node(a).unwrap().kind, NodeKind::Path { live: None, .. }), "moved: a plain path"); } +/// An output updates the input objects it lists, deletes the ones it leaves out, and adds new +/// objects where it lists them. #[test] fn outputs_update_add_and_delete_in_place() { let (mut d, ids, layer) = doc(); @@ -109,6 +113,8 @@ fn outputs_update_add_and_delete_in_place() { assert!(n1.appearance.stroke().is_none()); } +/// New objects take the place of the input objects they replace, or go on top of the given +/// container when there were none. #[test] fn new_objects_take_the_place_of_replaced_inputs_or_go_on_top() { let (mut d, ids, layer) = doc(); @@ -121,6 +127,8 @@ fn new_objects_take_the_place_of_replaced_inputs_or_go_on_top() { assert_eq!(names(&d, layer).last().map(String::as_str), Some("X")); } +/// An output turns a path into a compound path and back and sets its fill rule, strokes (weights +/// clamped), opacity and name. #[test] fn geometry_type_and_strokes_change() { let (mut d, ids, _) = doc(); @@ -143,6 +151,8 @@ fn geometry_type_and_strokes_change() { assert!(n.appearance.stroke().is_none()); } +/// A paint handed back as it was keeps its swatch link and any other is unlinked; a pattern paint +/// must name a pattern the document has. #[test] fn changed_paints_lose_swatch_links_and_patterns_must_exist() { let (mut d, ids, _) = doc(); @@ -160,6 +170,7 @@ fn changed_paints_lose_swatch_links_and_patterns_must_exist() { assert!(apply_output(&mut d, &[a], decode_json(pattern).unwrap(), None, &keep).is_ok()); } +/// Output the host can't read as objects is refused, whatever is wrong with it. #[test] fn bad_outputs_are_refused() { let (d, ids, _) = doc(); @@ -234,6 +245,7 @@ fn stroke_styles_and_effect_records_round_trip() { assert_eq!((e.to_string().contains("objects[0].effects: not an effect"), &d), (true, &before), "{e}"); } +/// Stroke styles and effect records in a form the host doesn't read are refused. #[test] fn bad_stroke_styles_and_effect_records_are_refused() { let a = doc().1[0].0;