diff --git a/ROADMAP.md b/ROADMAP.md index e7000874f..4115c31c2 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ # VectorCraft Roadmap -- Contour tool (beyond Illustrator): drag a selected shape's edge outward or inward for a live, editable offset copy, with a floating bar for distance and Sharp, Round or Square corners; Bake turns it into paths. Scores and estimates unchanged. +- Contour add-on (beyond Illustrator): drag a selected shape's edge outward or inward for a live, editable offset copy, with a floating bar for distance and Sharp, Round or Square corners; Bake turns it into paths. Scores and estimates unchanged. - Paint: new gradients use the document's color model for default stops, including Gradient tool creation. Scores and estimates unchanged. @@ -13,7 +13,7 @@ **Stage: alpha** · next: beta, ~13 points (ready for real work ~62% → ~75%, and reliable `.ai` exchange) and ~170–270 h away -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-05 · **Change:** minor (Contour tool, beyond Illustrator; scores and estimates unchanged; new gradients follow document color mode; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-05 · **Change:** minor (Contour add-on, beyond Illustrator; scores and estimates unchanged; new gradients follow document color mode; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adobe Illustrator workflow. It runs on macOS, Windows, Linux, FreeBSD and the web (WASM), and agents can drive all of it over MCP, the CLI and a JSON diff --git a/crates/engine/src/cmd.rs b/crates/engine/src/cmd.rs index e59331691..b1b9efd60 100644 --- a/crates/engine/src/cmd.rs +++ b/crates/engine/src/cmd.rs @@ -12,7 +12,6 @@ mod colorcmds; pub mod colormgmt; pub mod colortheme; mod compoundshape; -pub(crate) mod contour; pub(crate) mod create; mod css; mod cut; @@ -263,7 +262,6 @@ pub fn command_specs() -> &'static [CommandSpec] { v.extend(xform::specs()); v.extend(effectcmd::specs()); v.extend(pathops::specs()); - v.extend(contour::specs()); v.extend(compoundshape::specs()); v.extend(typecmd::specs()); v.extend(menucmds::specs()); diff --git a/crates/engine/src/cmd/addons.rs b/crates/engine/src/cmd/addons.rs index ac9597cbd..15ba1dad4 100644 --- a/crates/engine/src/cmd/addons.rs +++ b/crates/engine/src/cmd/addons.rs @@ -4,6 +4,7 @@ mod arrange; mod cleanup; mod color; +mod contour; mod document; mod pathtools; mod pen; @@ -17,6 +18,7 @@ pub fn specs() -> Vec { let mut v = arrange::specs(); v.extend(cleanup::specs()); v.extend(color::specs()); + v.extend(contour::specs()); v.extend(document::specs()); v.extend(pathtools::specs()); v.extend(pen::specs()); diff --git a/crates/engine/src/cmd/contour.rs b/crates/engine/src/cmd/addons/contour.rs similarity index 89% rename from crates/engine/src/cmd/contour.rs rename to crates/engine/src/cmd/addons/contour.rs index 5bbf5652b..46df381ed 100644 --- a/crates/engine/src/cmd/contour.rs +++ b/crates/engine/src/cmd/addons/contour.rs @@ -1,18 +1,21 @@ -//! `object.contour`: the Contour tool's command. A Contour is a copy of a selected shape that +//! Contour (`addon.contour.*`), a built-in add-on: the Contour tool's command. A Contour is a copy of a selected shape that //! carries a live Offset Path effect, so it grows or shrinks the shape without baking anything: its //! fill, stroke and effect stay editable, and Expand Appearance bakes it into plain paths. The //! original stays where it is. Running the command on an object that already carries an Offset //! Path effect (a Contour made earlier) changes that effect instead of nesting another one. +#[cfg(test)] +mod tests; + use serde_json::{Value, json}; use vectorcraft_doc::NodeKind; use vectorcraft_render::effects; -use super::edit::{copy_above, selected_roots}; -use super::pathops::{len_param, num_param}; -use super::{CommandSpec, EngineError, NodeId, Result, Session, bad, cmd, has_selection, str_param}; +use super::super::edit::{copy_above, selected_roots}; +use super::super::pathops::{len_param, num_param}; +use super::super::{CommandSpec, EngineError, NodeId, Result, Session, bad, cmd, has_selection, str_param}; -const C: &str = "object.contour"; +const C: &str = "addon.contour.set"; /// The live effect that does the offsetting (the one Effect › Path › Offset Path applies). const EFFECT: &str = "path.offsetPath"; /// The largest offset, in points; Offset Path takes the same. @@ -20,7 +23,7 @@ const MAX_OFFSET: f64 = 1e5; pub fn specs() -> Vec { vec![cmd!( - "object.contour", + "addon.contour.set", "Contour", [], None, diff --git a/crates/engine/src/tests/contour.rs b/crates/engine/src/cmd/addons/contour/tests.rs similarity index 92% rename from crates/engine/src/tests/contour.rs rename to crates/engine/src/cmd/addons/contour/tests.rs index 164370c75..900d110d5 100644 --- a/crates/engine/src/tests/contour.rs +++ b/crates/engine/src/cmd/addons/contour/tests.rs @@ -1,4 +1,4 @@ -//! Tests for `object.contour`: a live Offset Path copy of a shape that stays editable until baked. +//! Tests for `addon.contour.set`: a live Offset Path copy of a shape that stays editable until baked. use serde_json::{Value, json}; use vectorcraft_doc::NodeKind; @@ -46,7 +46,7 @@ fn contour_is_a_live_copy_above_the_original() { let mut s = session(); let a = rect(&mut s, 100.0, 100.0, 50.0, 40.0); let before = bounds(&s, a); - let r = s.execute("object.contour", &json!({"offset": 10})).unwrap(); + let r = s.execute("addon.contour.set", &json!({"offset": 10})).unwrap(); let (made, updated) = (ids_of(&r, "created"), ids_of(&r, "updated")); assert_eq!((made.len(), updated.len()), (1, 0)); // The original is untouched, the copy is above it and selected, and nothing is baked: the @@ -70,7 +70,7 @@ fn baking_a_contour_gives_the_grown_and_the_shrunk_shape() { let a = rect(&mut s, 100.0, 100.0, 50.0, 40.0); for (offset, want) in [(10.0, Rect::new(90.0, 90.0, 160.0, 150.0)), (-5.0, Rect::new(105.0, 105.0, 145.0, 135.0))] { select(&mut s, &[a]); - s.execute("object.contour", &json!({"offset": offset})).unwrap(); + s.execute("addon.contour.set", &json!({"offset": offset})).unwrap(); let baked = ids_of(&s.execute("effect.expandAppearance", &json!({})).unwrap(), "ids"); let b = bounds(&s, baked[0]); // The default stroke widens the baked art by its weight; compare the fill's edges. @@ -92,10 +92,10 @@ fn baked_fill_bounds(s: &Session, id: NodeId) -> Rect { fn a_second_drag_changes_the_contour_instead_of_nesting_one() { let mut s = session(); let a = rect(&mut s, 100.0, 100.0, 50.0, 40.0); - let made = ids_of(&s.execute("object.contour", &json!({"offset": 10})).unwrap(), "created"); + let made = ids_of(&s.execute("addon.contour.set", &json!({"offset": 10})).unwrap(), "created"); let count = node_count(&s); // The Contour is selected: the next call edits it. - let r = s.execute("object.contour", &json!({"offset": -4, "corners": "round"})).unwrap(); + let r = s.execute("addon.contour.set", &json!({"offset": -4, "corners": "round"})).unwrap(); assert_eq!(ids_of(&r, "updated"), made); assert!(ids_of(&r, "created").is_empty()); assert_eq!(node_count(&s), count, "no new object"); @@ -110,7 +110,7 @@ fn corner_styles_choose_the_join() { for (corners, joins) in [("sharp", "miter"), ("round", "round"), ("square", "bevel"), ("Round", "round")] { let mut s = session(); rect(&mut s, 0.0, 0.0, 30.0, 30.0); - let made = ids_of(&s.execute("object.contour", &json!({"offset": 3, "corners": corners})).unwrap(), "created"); + let made = ids_of(&s.execute("addon.contour.set", &json!({"offset": 3, "corners": corners})).unwrap(), "created"); assert_eq!(effects_of(&s, made[0])[0].1["joins"], json!(joins), "{corners}"); } } @@ -119,7 +119,7 @@ fn corner_styles_choose_the_join() { fn contour_keeps_the_color_and_stroke_editable_on_the_copy() { let mut s = session(); let a = rect(&mut s, 0.0, 0.0, 30.0, 30.0); - let made = ids_of(&s.execute("object.contour", &json!({"offset": 6})).unwrap(), "created"); + let made = ids_of(&s.execute("addon.contour.set", &json!({"offset": 6})).unwrap(), "created"); let original = node(&s, a).appearance.items.clone(); assert_eq!(node(&s, made[0]).appearance.items, original, "the copy starts with the shape's own paint"); // Recolouring the Contour (it is the selection) changes its fill only: the live effect stays @@ -135,7 +135,7 @@ fn contour_is_one_undo_step() { let mut s = session(); let a = rect(&mut s, 0.0, 0.0, 30.0, 30.0); let count = node_count(&s); - s.execute("object.contour", &json!({"offset": 6})).unwrap(); + s.execute("addon.contour.set", &json!({"offset": 6})).unwrap(); assert_eq!(node_count(&s), count + 1); s.execute("edit.undo", &json!({})).unwrap(); assert_eq!(node_count(&s), count); @@ -148,13 +148,13 @@ fn contour_refuses_what_it_cannot_offset() { rect(&mut s, 0.0, 0.0, 30.0, 30.0); let count = node_count(&s); for params in [json!({}), json!({"offset": "abc"}), json!({"offset": 1e300}), json!({"offset": 5, "corners": "pointy"})] { - assert!(s.execute("object.contour", ¶ms).is_err(), "{params}"); + assert!(s.execute("addon.contour.set", ¶ms).is_err(), "{params}"); } // Nothing was added by the refused calls. assert_eq!(node_count(&s), count); // Type is not a path to offset. s.execute("text.create", &json!({"x": 10, "y": 10, "text": "Hi"})).unwrap(); - let err = s.execute("object.contour", &json!({"offset": 5})).unwrap_err().to_string(); + let err = s.execute("addon.contour.set", &json!({"offset": 5})).unwrap_err().to_string(); assert!(err.contains("select paths or shapes"), "{err}"); } @@ -169,9 +169,9 @@ fn border_with_a_gap_from_two_contours_and_minus_front() { // The gap first (+8), then the border (+20) from the same shape: both land directly above it, // the later one lower, so the gap ends up in front of the border. select(&mut s, &[a]); - let gap = ids_of(&s.execute("object.contour", &json!({"offset": 8})).unwrap(), "created")[0]; + let gap = ids_of(&s.execute("addon.contour.set", &json!({"offset": 8})).unwrap(), "created")[0]; select(&mut s, &[a]); - let border = ids_of(&s.execute("object.contour", &json!({"offset": 20})).unwrap(), "created")[0]; + let border = ids_of(&s.execute("addon.contour.set", &json!({"offset": 20})).unwrap(), "created")[0]; let d = &s.doc().unwrap().doc; assert!(d.index_path(gap).unwrap() > d.index_path(border).unwrap(), "the gap is in front of the border"); select(&mut s, &[border, gap]); @@ -239,7 +239,7 @@ mod tool { assert!(!s.in_interaction()); assert_eq!(undo_depth(&s), before + 1); assert_eq!(node_count(&s), nodes + 1); - assert_eq!(s.journal.last().map(|j| j.0.as_str()), Some("object.contour")); + assert_eq!(s.journal.last().map(|j| j.0.as_str()), Some("addon.contour.set")); let made = s.doc().unwrap().selection.objects.clone(); assert_eq!(made.len(), 1); assert_ne!(made[0], a); @@ -304,7 +304,7 @@ mod tool { #[test] fn dragging_a_contour_again_keeps_its_corners_and_miter_limit() { let (mut s, _) = ready(); - s.execute("object.contour", &json!({"offset": 5, "corners": "square", "miterLimit": 7})).unwrap(); + s.execute("addon.contour.set", &json!({"offset": 5, "corners": "square", "miterLimit": 7})).unwrap(); let contour = s.doc().unwrap().selection.objects[0]; s.set_tool_option("corners", &json!("sharp")); at(&mut s, PointerKind::Down, 150.0, 120.0, Mods::default()); diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index e78d828dc..9ec3ccaf9 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -36,7 +36,6 @@ mod compoundpaint; mod constrain_new; mod constrain_tools; mod containers; -mod contour; mod css; mod cut; mod dashalign; diff --git a/crates/tools/src/addons.rs b/crates/tools/src/addons.rs index 42643538e..69bb4858a 100644 --- a/crates/tools/src/addons.rs +++ b/crates/tools/src/addons.rs @@ -3,6 +3,7 @@ //! submenu selects them. Tool gestures emit engine commands. pub mod anchor_eraser; +pub mod contour; pub mod corner_tool; pub mod enclosing_lasso; pub mod extend_path; @@ -28,14 +29,17 @@ pub const PATH_TOOLS: &[ToolInfo] = &[ /// Smart Pen's tool, a flyout group of its own. pub const SMART_PEN: &[ToolInfo] = &[ToolInfo { id: smart_pen::ID, label: "Smart Pen Tool", shortcut: None, icon: "spline-pointer" }]; +pub const CONTOUR: &[ToolInfo] = &[ToolInfo { id: contour::ID, label: "Contour Tool", shortcut: None, icon: "tool-contour" }]; + pub const ENCLOSING_LASSO: &[ToolInfo] = &[ToolInfo { id: enclosing_lasso::ID, label: "Enclosing Lasso Tool", shortcut: None, icon: "tool-lasso" }]; /// The add-ons' tool groups, after the toolbar's own ([`crate::TOOL_GROUPS`]), each with the /// heading the All Tools drawer lists it under (its add-on's name). -const HEADED: &[(&[ToolInfo], &str)] = &[(PATH_TOOLS, "Path Precision Tools"), (SMART_PEN, "Smart Pen"), (ENCLOSING_LASSO, "Enclosing Lasso")]; +const HEADED: &[(&[ToolInfo], &str)] = + &[(PATH_TOOLS, "Path Precision Tools"), (SMART_PEN, "Smart Pen"), (ENCLOSING_LASSO, "Enclosing Lasso"), (CONTOUR, "Contour")]; /// The add-ons' tool groups, after the toolbar's own ([`crate::TOOL_GROUPS`]). -pub const GROUPS: &[&[ToolInfo]] = &[PATH_TOOLS, SMART_PEN, ENCLOSING_LASSO]; +pub const GROUPS: &[&[ToolInfo]] = &[PATH_TOOLS, SMART_PEN, ENCLOSING_LASSO, CONTOUR]; /// The heading the All Tools drawer lists tool `id` under: its add-on's name. pub fn heading(id: &str) -> Option<&'static str> { @@ -54,6 +58,7 @@ pub fn create(id: &str) -> Option> { slide_anchor::ID => Box::new(slide_anchor::SlideAnchorTool::default()), smart_pen::ID => Box::new(smart_pen::SmartPenTool::default()), enclosing_lasso::ID => Box::new(enclosing_lasso::EnclosingLassoTool::default()), + contour::ID => Box::new(contour::ContourTool::default()), _ => return None, }) } @@ -74,6 +79,7 @@ mod tests { assert!(PATH_TOOLS.iter().all(|t| heading(t.id) == Some("Path Precision Tools"))); assert_eq!(heading(smart_pen::ID), Some("Smart Pen")); assert_eq!(heading(enclosing_lasso::ID), Some("Enclosing Lasso")); + assert_eq!(heading(contour::ID), Some("Contour")); assert_eq!(GROUPS.len(), HEADED.len()); assert!(crate::catalog::all_tools().any(|t| t.id == extend_path::ID)); assert!(create("pen").is_none() && heading("pen").is_none()); diff --git a/crates/tools/src/contour.rs b/crates/tools/src/addons/contour.rs similarity index 94% rename from crates/tools/src/contour.rs rename to crates/tools/src/addons/contour.rs index cf450752c..e77bb29ee 100644 --- a/crates/tools/src/contour.rs +++ b/crates/tools/src/addons/contour.rs @@ -1,5 +1,5 @@ //! The Contour tool: drag away from a selected shape's edge to grow a live copy of it, or toward the -//! inside to shrink it (`object.contour`). The copy is the original's outline offset by the distance +//! inside to shrink it (`addon.contour.set`). The copy is the original's outline offset by the distance //! the pointer has moved from the edge, previewed while the button is down and kept as one undo step //! on release. Nothing is baked: the copy carries a live Offset Path effect, so it keeps being //! editable (a second drag on a selected Contour changes its distance instead of nesting another), @@ -16,9 +16,12 @@ use vectorcraft_geom::{BezPath, FillRule, Point, hit}; use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey}; -/// The live effect a Contour is made of (the one `object.contour` applies). +/// The tool's id. +pub const ID: &str = "contour"; + +/// The live effect a Contour is made of (the one `addon.contour.set` applies). const EFFECT: &str = "path.offsetPath"; -/// The largest distance, in points, `object.contour` accepts. +/// The largest distance, in points, `addon.contour.set` accepts. const MAX_OFFSET: f64 = 1e5; /// The guide showing the shape a Contour was made from. const GUIDE: [u8; 3] = [0x1e, 0x88, 0xe5]; @@ -51,7 +54,7 @@ fn contour_offset(n: &Node) -> Option { Some(e.params.get("offset").and_then(Value::as_f64).unwrap_or(0.0)) } -/// The corner style and miter limit of a Contour's Offset Path effect, as `object.contour` takes +/// The corner style and miter limit of a Contour's Offset Path effect, as `addon.contour.set` takes /// them. fn contour_style(n: &Node) -> Option { let e = n.appearance.effects.iter().rev().find(|e| e.id == EFFECT)?; @@ -154,7 +157,7 @@ fn round_distance(v: f64, mods: Mods) -> f64 { impl Tool for ContourTool { fn id(&self) -> &'static str { - "contour" + ID } fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec { @@ -182,7 +185,7 @@ impl Tool for ContourTool { drag.last = Some(offset); let mut params = drag.style.clone(); params["offset"] = json!(offset); - vec![Action::Preview("object.contour".into(), params)] + vec![Action::Preview("addon.contour.set".into(), params)] } PointerKind::Up => match self.drag.take() { Some(d) if d.last.is_some() => vec![Action::Commit], @@ -249,8 +252,3 @@ impl Tool for ContourTool { self.drag.is_some() } } - -/// The Contour tool, for [`crate::create`]. -pub fn create(id: &str) -> Option> { - (id == "contour").then(|| Box::new(ContourTool::default()) as Box) -} diff --git a/crates/tools/src/catalog.rs b/crates/tools/src/catalog.rs index 49fe557ea..9bbd7cbdf 100644 --- a/crates/tools/src/catalog.rs +++ b/crates/tools/src/catalog.rs @@ -94,7 +94,6 @@ pub const TOOL_GROUPS: &[&[ToolInfo]] = &[ t("livePaintBucket", "Live Paint Bucket", Some("K"), "tool-bucket"), t("livePaintSelection", "Live Paint Selection Tool", Some("Shift+L"), "tool-live-select"), ], - &[t("contour", "Contour Tool", None, "tool-contour")], &[ t("perspectiveGrid", "Perspective Grid Tool", Some("Shift+P"), "tool-perspective"), t("perspectiveSelection", "Perspective Selection Tool", Some("Shift+V"), "tool-perspective-select"), diff --git a/crates/tools/src/lib.rs b/crates/tools/src/lib.rs index d14e43c67..ee00fdbe9 100644 --- a/crates/tools/src/lib.rs +++ b/crates/tools/src/lib.rs @@ -11,7 +11,6 @@ pub mod addons; pub mod bbox; pub mod builder; pub mod catalog; -pub mod contour; pub mod corners; pub mod cropimage; pub mod cut; @@ -297,7 +296,6 @@ pub fn create(id: &str) -> Box { .or_else(|| printtiling::create(other)) .or_else(|| cropimage::create(other)) .or_else(|| cut::create(other)) - .or_else(|| contour::create(other)) .or_else(|| intertwine::create(other)) .or_else(|| objects_on_path::create(other)) .or_else(|| touchtype::create(other)) diff --git a/crates/ui-egui/src/addons.rs b/crates/ui-egui/src/addons.rs index d9c86240a..f0238aee2 100644 --- a/crates/ui-egui/src/addons.rs +++ b/crates/ui-egui/src/addons.rs @@ -3,6 +3,7 @@ pub mod arrange; pub mod cleanup; pub mod color; +pub mod contour; pub mod document; pub mod enclosing_lasso; pub mod pathtools; @@ -18,6 +19,7 @@ pub fn plugin_menu() -> Vec { Item::Sub("Arrange Tools", arrange::menu()), Item::Sub("Artwork Cleanup", cleanup::menu()), Item::Sub("Color Effects", color::menu()), + Item::Sub("Contour", contour::menu()), Item::Sub("Document Tools", document::menu()), Item::Sub("Enclosing Lasso", enclosing_lasso::menu()), Item::Sub("Path Precision Tools", pathtools::menu()), diff --git a/crates/ui-egui/src/addons/contour.rs b/crates/ui-egui/src/addons/contour.rs new file mode 100644 index 000000000..084a8b98c --- /dev/null +++ b/crates/ui-egui/src/addons/contour.rs @@ -0,0 +1,12 @@ +//! The Contour add-on in the interface: its submenu (Object › Plug-ins › Contour), which selects the +//! Contour tool. The Contour bar (`canvas/contour_bar.rs`) shows over the selection while the tool is +//! active. Everything acts through `addon.contour.set` and Expand Appearance. + +use serde_json::json; +use vectorcraft_tools::addons::contour; + +use crate::menus::Item; + +pub fn menu() -> Vec { + vec![Item::Cmd("Contour Tool", "tool.select", json!({"tool": contour::ID}))] +} diff --git a/crates/ui-egui/src/canvas/contour_bar.rs b/crates/ui-egui/src/canvas/contour_bar.rs index 443fdc2aa..4b560eeba 100644 --- a/crates/ui-egui/src/canvas/contour_bar.rs +++ b/crates/ui-egui/src/canvas/contour_bar.rs @@ -1,6 +1,6 @@ //! The Contour bar: a floating bar above the selection while the Contour tool is active. It has the //! Contour's distance in a number box, its corner style (Sharp, Round or Square) and Bake, which -//! turns the live Contour into plain paths. It acts through `object.contour` and +//! turns the live Contour into plain paths. It acts through `addon.contour.set` and //! `effect.expandAppearance`, so everything it does is a command with its own undo step. use serde_json::Value; @@ -98,7 +98,7 @@ pub(super) fn contour_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) { if resp.drag_stopped() || resp.lost_focus() { ui.data_mut(|d| d.remove_temp::(key)); if (v - shown).abs() > 1e-9 { - run = Some(("object.contour", json!({"offset": v, "corners": corners}))); + run = Some(("addon.contour.set", json!({"offset": v, "corners": corners}))); } } for (label, id) in [(tl!("Sharp"), "sharp"), (tl!("Round"), "round"), (tl!("Square"), "square")] { @@ -119,7 +119,7 @@ pub(super) fn contour_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) { if result.is_ok() && let Some(s) = &stand { - result = app.run("object.contour", json!({"offset": s.offset, "corners": id})); + result = app.run("addon.contour.set", json!({"offset": s.offset, "corners": id})); } if let Err(e) = result { app.ui.status = e; diff --git a/crates/ui-egui/src/canvas/tests.rs b/crates/ui-egui/src/canvas/tests.rs index da52ed353..3f6f9f0ff 100644 --- a/crates/ui-egui/src/canvas/tests.rs +++ b/crates/ui-egui/src/canvas/tests.rs @@ -9053,7 +9053,7 @@ fn the_contour_bar_sets_corners_and_bakes() { assert!(offsets(&app).is_empty()); // A Contour made now has round corners; Square changes the selected Contour in place. - app.session.execute("object.contour", &json!({"offset": 8, "corners": "round"})).unwrap(); + app.session.execute("addon.contour.set", &json!({"offset": 8, "corners": "round"})).unwrap(); assert_eq!(offsets(&app), ["round"]); click_contour_bar(&mut app, &ctx, "Square"); assert_eq!(offsets(&app), ["bevel"], "the selected Contour changed, none was added"); diff --git a/crates/ui-egui/src/edit_toolbar.rs b/crates/ui-egui/src/edit_toolbar.rs index dcec1ffc5..eb79cf357 100644 --- a/crates/ui-egui/src/edit_toolbar.rs +++ b/crates/ui-egui/src/edit_toolbar.rs @@ -410,7 +410,7 @@ fn heading(id: &str) -> &'static str { _ if id.starts_with("perspective") => "Perspective", "artboard" => "Artboards", "slice" | "sliceSelection" => "Slices", - "objectsOnPath" | "contour" => "Modify", + "objectsOnPath" => "Modify", _ => "More", } } diff --git a/crates/ui-egui/src/edit_toolbar/tests.rs b/crates/ui-egui/src/edit_toolbar/tests.rs index 85309507f..c474cea58 100644 --- a/crates/ui-egui/src/edit_toolbar/tests.rs +++ b/crates/ui-egui/src/edit_toolbar/tests.rs @@ -529,10 +529,22 @@ fn drop_targets_follow_the_buttons_edges() { fn addon_tools_are_in_the_drawer_not_the_toolbars() { let mut app = app(); let enclosing = vectorcraft_tools::addons::enclosing_lasso::ID; + let contour = vectorcraft_tools::addons::contour::ID; let addon: Vec<&str> = vectorcraft_tools::addons::GROUPS.iter().flat_map(|g| g.iter().map(|t| t.id)).collect(); assert_eq!( addon, - ["anchorEraserBrush", "extendPath", "measureInspect", "parametricShape", "cornerTool", "slideAnchor", "sharedEdge", "smartPen", enclosing] + [ + "anchorEraserBrush", + "extendPath", + "measureInspect", + "parametricShape", + "cornerTool", + "slideAnchor", + "sharedEdge", + "smartPen", + enclosing, + contour + ] ); for advanced in [false, true] { if advanced { @@ -546,7 +558,8 @@ fn addon_tools_are_in_the_drawer_not_the_toolbars() { assert_eq!(listed("extendPath"), Some(("Path Precision Tools", addon[..7].to_vec()))); assert_eq!(listed("smartPen"), Some(("Smart Pen", vec!["smartPen"]))); assert_eq!(listed(enclosing), Some(("Enclosing Lasso", vec![enclosing]))); - for tool in ["extendPath", "smartPen", enclosing] { + assert_eq!(listed(contour), Some(("Contour", vec![contour]))); + for tool in ["extendPath", "smartPen", enclosing, contour] { app.run("toolbar.addTool", json!({"tool": tool})).unwrap(); assert!(slot_of(&app, tool).is_some(), "{tool}"); } diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index ee25d5057..96735ce20 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -6201,6 +6201,13 @@ breakText Glyphs Глифи Fully Outside Изцяло извън No artboards match. Няма съвпадащи работни области. Chosen: {n} of {total} Избрани: {n} от {total} + +# Contour add-on + Contour Очертание + Contour Tool Инструмент „Очертание“ + Sharp Заостряне + Bake Разшири + Turn the Contour into plain paths Превръща очертанието в обикновени пътища Set Corners… Задаване на ъгли… All Anchors Всички опорни точки Odd-Numbered Anchors Нечетни опорни точки diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 1f4ea2422..ad78ee84e 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -6236,6 +6236,13 @@ shape Open Otvoren Fully Outside Potpuno vani No artboards match. Nijedna radna ploča ne odgovara. Chosen: {n} of {total} Odabrano: {n} od {total} + +# Contour add-on + Contour Kontura + Contour Tool Alat kontura + Sharp Oštro + Bake Proširi + Turn the Contour into plain paths Pretvori konturu u obične putanje Set Corners… Postavi uglove… All Anchors Sva sidra Odd-Numbered Anchors Neparna sidra diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 70c893f32..8ad31d6d0 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -6337,6 +6337,13 @@ breakText Glyphs Glifs Fully Outside Totalment fora No artboards match. Cap tauler no coincideix. Chosen: {n} of {total} Triats: {n} de {total} + +# Contour add-on + Contour Contorn + Contour Tool Eina de contorn + Sharp Punxegut + Bake Expandeix + Turn the Contour into plain paths Converteix el contorn en traçats simples Set Corners… Defineix les cantonades… All Anchors Tots els ancoratges Odd-Numbered Anchors Ancoratges senars diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index cac60a648..d801c39fd 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -6351,6 +6351,13 @@ breakText Glyphs Glyfy Fully Outside Zcela vně No artboards match. Žádná pracovní plocha neodpovídá. Chosen: {n} of {total} Zvoleno: {n} z {total} + +# Contour add-on + Contour Obrys + Contour Tool Nástroj Obrys + Sharp Ostrý + Bake Rozložit + Turn the Contour into plain paths Převést obrys na běžné cesty Set Corners… Nastavit rohy… All Anchors Všechny kotevní body Odd-Numbered Anchors Liché kotevní body diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 623e70038..e410c8e4a 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -6217,6 +6217,13 @@ breakText Glyphs Glyffer Fully Outside Helt udenfor No artboards match. Ingen tegneflader passer. Chosen: {n} of {total} Valgt: {n} af {total} + +# Contour add-on + Contour Kontur + Contour Tool Kontur-værktøj + Sharp Spids + Bake Udvid + Turn the Contour into plain paths Gør konturen til almindelige stier Set Corners… Angiv hjørner… All Anchors Alle ankre Odd-Numbered Anchors Ulige ankre diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index fea15fbe2..e11cf1bac 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -5937,6 +5937,13 @@ breakText Glyphs Glyphen Fully Outside Vollständig außen No artboards match. Keine Zeichenfläche passt. Chosen: {n} of {total} Gewählt: {n} von {total} + +# Contour add-on + Contour Umriss + Contour Tool Umriss-Werkzeug + Sharp Spitz + Bake Umwandeln + Turn the Contour into plain paths Wandelt den Umriss in einfache Pfade um Set Corners… Ecken festlegen… All Anchors Alle Ankerpunkte Odd-Numbered Anchors Ungerade Ankerpunkte diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 72684a9c6..9f3d0c18e 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -6303,6 +6303,13 @@ breakText Glyphs Γλύφοι Fully Outside Πλήρως εκτός No artboards match. Κανένας πίνακας σχεδίασης δεν ταιριάζει. Chosen: {n} of {total} Επιλεγμένοι: {n} από {total} + +# Contour add-on + Contour Παράλληλο περίγραμμα + Contour Tool Εργαλείο παράλληλου περιγράμματος + Sharp Οξεία + Bake Επέκταση + Turn the Contour into plain paths Μετατρέπει το παράλληλο περίγραμμα σε απλές διαδρομές Set Corners… Ορισμός γωνιών… All Anchors Όλα τα σημεία αγκύρωσης Odd-Numbered Anchors Μονά σημεία αγκύρωσης diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index caf2b7b05..b02abb8f1 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -6293,6 +6293,13 @@ breakText Glyphs Glifos Fully Outside Totalmente fuera No artboards match. Ninguna mesa de trabajo coincide. Chosen: {n} of {total} Elegidas: {n} de {total} + +# Contour add-on + Contour Contorno + Contour Tool Herramienta Contorno + Sharp Puntiaguda + Bake Expandir + Turn the Contour into plain paths Convierte el contorno en trazados simples Set Corners… Definir esquinas… All Anchors Todas las anclas Odd-Numbered Anchors Anclas impares diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 73a7465a8..eebd34d89 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -6386,6 +6386,13 @@ breakText Glyphs Merkit Fully Outside Kokonaan ulkona No artboards match. Mikään piirtoalusta ei vastaa hakua. Chosen: {n} of {total} Valittu: {n}/{total} + +# Contour add-on + Contour Ääriviiva + Contour Tool Ääriviivatyökalu + Sharp Terävä + Bake Laajenna + Turn the Contour into plain paths Muuntaa ääriviivan tavallisiksi poluiksi Set Corners… Aseta kulmat… All Anchors Kaikki ankkurit Odd-Numbered Anchors Parittomat ankkurit diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 8aa5fa1e9..a5fbb1ab0 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -6346,6 +6346,13 @@ breakText Glyphs Glyphes Fully Outside Entièrement à l’extérieur No artboards match. Aucun plan de travail ne correspond. Chosen: {n} of {total} Choisis : {n} sur {total} + +# Contour add-on + Contour Pourtour + Contour Tool Outil Pourtour + Sharp Pointu + Bake Décomposer + Turn the Contour into plain paths Transforme le pourtour en tracés simples Set Corners… Définir les angles… All Anchors Tous les points d’ancrage Odd-Numbered Anchors Points d’ancrage impairs diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 2137addf1..3959b3fee 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -6335,6 +6335,13 @@ breakText Glyphs Glifák Fully Outside Teljesen kívül No artboards match. Nincs egyező rajztábla. Chosen: {n} of {total} Kiválasztva: {n} / {total} + +# Contour add-on + Contour Kontúr + Contour Tool Kontúr eszköz + Sharp Hegyes + Bake Kibontás + Turn the Contour into plain paths A kontúrt egyszerű útvonalakká alakítja Set Corners… Sarkok beállítása… All Anchors Összes horgonypont Odd-Numbered Anchors Páratlan horgonypontok diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index c0c6489bf..fd72bc7e6 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -6217,6 +6217,13 @@ breakText Glyphs Glif Fully Outside Sepenuhnya di luar No artboards match. Tidak ada artboard yang cocok. Chosen: {n} of {total} Dipilih: {n} dari {total} + +# Contour add-on + Contour Kontur + Contour Tool Alat kontur + Sharp Tajam + Bake Perluas + Turn the Contour into plain paths Ubah kontur menjadi jalur biasa Set Corners… Atur sudut… All Anchors Semua jangkar Odd-Numbered Anchors Jangkar bernomor ganjil diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index d3add5036..4f2035969 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -5971,6 +5971,13 @@ breakText Glyphs Glifi Fully Outside Completamente esterno No artboards match. Nessuna tavola da disegno corrisponde. Chosen: {n} of {total} Scelte: {n} di {total} + +# Contour add-on + Contour Contorno + Contour Tool Strumento Contorno + Sharp Appuntito + Bake Espandi + Turn the Contour into plain paths Trasforma il contorno in tracciati semplici Set Corners… Imposta angoli… All Anchors Tutti gli ancoraggi Odd-Numbered Anchors Ancoraggi dispari diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 3533338d7..9f089ecf4 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -6036,6 +6036,13 @@ breakText Glyphs 字形 Fully Outside 完全に外側 No artboards match. 一致するアートボードはありません。 Chosen: {n} of {total} 選択中: {n} / {total} + +# Contour add-on + Contour 輪郭 + Contour Tool 輪郭ツール + Sharp 鋭角 + Bake 分割 + Turn the Contour into plain paths 輪郭を通常のパスに変換します Set Corners… コーナーを設定… All Anchors すべてのアンカー Odd-Numbered Anchors 奇数番目のアンカー diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index cc96a111c..78da64abc 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -6362,6 +6362,13 @@ font-script Japanese იაპონური Fully Outside მთლიანად გარეთ No artboards match. შესაბამისი სამუშაო დაფა არ არის. Chosen: {n} of {total} არჩეულია: {n} / {total} + +# Contour add-on + Contour მოხაზულობა + Contour Tool მოხაზულობის ხელსაწყო + Sharp მახვილი + Bake გაშლა + Turn the Contour into plain paths მოხაზულობას ჩვეულებრივ ტრაექტორიებად აქცევს Set Corners… კუთხეების დაყენება… All Anchors ყველა საყრდენი წერტილი Odd-Numbered Anchors კენტი საყრდენი წერტილები diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 1ac6fdc75..3e99b509b 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -5932,6 +5932,13 @@ breakText Glyphs 글리프 Fully Outside 완전히 바깥쪽 No artboards match. 일치하는 아트보드가 없습니다. Chosen: {n} of {total} 선택됨: {n} / {total} + +# Contour add-on + Contour 윤곽 + Contour Tool 윤곽 도구 + Sharp 뾰족하게 + Bake 확장 + Turn the Contour into plain paths 윤곽을 일반 패스로 바꿉니다 Set Corners… 모퉁이 설정… All Anchors 모든 앵커 Odd-Numbered Anchors 홀수 번째 앵커 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 0a5943f82..7f7818c54 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -6261,6 +6261,13 @@ breakText Glyphs Glyphs Fully Outside Volledig buiten No artboards match. Geen tekengebied komt overeen. Chosen: {n} of {total} Gekozen: {n} van {total} + +# Contour add-on + Contour Omtrek + Contour Tool Omtrek-gereedschap + Sharp Scherp + Bake Uitbreiden + Turn the Contour into plain paths Zet de omtrek om in gewone paden Set Corners… Hoeken instellen… All Anchors Alle ankerpunten Odd-Numbered Anchors Oneven ankerpunten diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index da7f069e6..56127f052 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -6206,6 +6206,13 @@ breakText Glyphs Glify Fully Outside Całkowicie na zewnątrz No artboards match. Żaden obszar kompozycji nie pasuje. Chosen: {n} of {total} Wybrano: {n} z {total} + +# Contour add-on + Contour Kontur + Contour Tool Narzędzie Kontur + Sharp Ostry + Bake Rozszerz + Turn the Contour into plain paths Zamienia kontur w zwykłe ścieżki Set Corners… Ustaw narożniki… All Anchors Wszystkie punkty Odd-Numbered Anchors Punkty nieparzyste diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 4e05fc1ee..7f68c5c7f 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -6424,6 +6424,13 @@ swatch Yellow Green Verde-amarelado Fully Outside Totalmente fora No artboards match. Nenhuma mesa de trabalho corresponde. Chosen: {n} of {total} Escolhidas: {n} de {total} + +# Contour add-on + Contour Contorno + Contour Tool Ferramenta Contorno + Sharp Pontiagudo + Bake Expandir + Turn the Contour into plain paths Transforma o contorno em caminhos simples Set Corners… Definir Cantos… All Anchors Todas as Âncoras Odd-Numbered Anchors Âncoras Ímpares diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 60dcc9e32..10426b2ef 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -6440,6 +6440,13 @@ shape Open Открытый Fully Outside Полностью снаружи No artboards match. Нет подходящих монтажных областей. Chosen: {n} of {total} Выбрано: {n} из {total} + +# Contour add-on + Contour Эквидистанта + Contour Tool Инструмент «Эквидистанта» + Sharp Острый + Bake Разобрать + Turn the Contour into plain paths Преобразует эквидистанту в обычные контуры Set Corners… Задать углы… All Anchors Все опорные точки Odd-Numbered Anchors Нечётные опорные точки diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 7a26b4d02..1d2d9b03e 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -6407,6 +6407,13 @@ digits Hindi Hindi Fully Outside Helt utanför No artboards match. Ingen rityta matchar. Chosen: {n} of {total} Valda: {n} av {total} + +# Contour add-on + Contour Kantlinje + Contour Tool Kantlinjeverktyg + Sharp Spetsig + Bake Expandera + Turn the Contour into plain paths Gör om kantlinjen till vanliga banor Set Corners… Ange hörn… All Anchors Alla ankare Odd-Numbered Anchors Udda ankare diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index f50913c3e..133538fe9 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -6308,6 +6308,13 @@ breakText Glyphs Glifler Fully Outside Tamamen dışarıda No artboards match. Eşleşen çalışma yüzeyi yok. Chosen: {n} of {total} Seçilen: {n}/{total} + +# Contour add-on + Contour Dış Hat + Contour Tool Dış Hat Aracı + Sharp Keskin + Bake Genişlet + Turn the Contour into plain paths Dış hattı sade yollara dönüştürür Set Corners… Köşeleri Ayarla… All Anchors Tüm Çapalar Odd-Numbered Anchors Tek Numaralı Çapalar diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 35b806a1f..95724a3d7 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -5951,6 +5951,13 @@ breakText Glyphs Гліфи Fully Outside Повністю зовні No artboards match. Немає відповідних монтажних областей. Chosen: {n} of {total} Вибрано: {n} з {total} + +# Contour add-on + Contour Еквідистанта + Contour Tool Інструмент «Еквідистанта» + Sharp Гострий + Bake Розібрати + Turn the Contour into plain paths Перетворює еквідистанту на звичайні контури Set Corners… Задати кути… All Anchors Усі опорні точки Odd-Numbered Anchors Непарні опорні точки diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index bae63ccf8..93113bc39 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -5952,6 +5952,13 @@ breakText Glyphs Glyph Fully Outside Hoàn toàn bên ngoài No artboards match. Không có bảng vẽ nào khớp. Chosen: {n} of {total} Đã chọn: {n} trên {total} + +# Contour add-on + Contour Đường viền + Contour Tool Công cụ Đường viền + Sharp Nhọn + Bake Mở rộng + Turn the Contour into plain paths Chuyển đường viền thành các đường dẫn thường Set Corners… Đặt góc… All Anchors Tất cả điểm neo Odd-Numbered Anchors Điểm neo số lẻ diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index c5004c367..20ec64796 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -6192,6 +6192,13 @@ breakText Glyphs 字形 Fully Outside 完全在外 No artboards match. 没有匹配的画板。 Chosen: {n} of {total} 已选:{n} / {total} + +# Contour add-on + Contour 轮廓 + Contour Tool 轮廓工具 + Sharp 尖角 + Bake 扩展 + Turn the Contour into plain paths 将轮廓转为普通路径 Set Corners… 设置转角… All Anchors 所有锚点 Odd-Numbered Anchors 奇数锚点 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index e42ab765b..7dbb11507 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -5989,6 +5989,13 @@ breakText Glyphs 字符 Fully Outside 完全在外 No artboards match. 沒有符合的工作區域。 Chosen: {n} of {total} 已選:{n} / {total} + +# Contour add-on + Contour 輪廓 + Contour Tool 輪廓工具 + Sharp 尖角 + Bake 展開 + Turn the Contour into plain paths 將輪廓轉為一般路徑 Set Corners… 設定轉角… All Anchors 所有錨點 Odd-Numbered Anchors 奇數錨點 diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index fcb9a1cc9..94054a0d4 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -59,7 +59,6 @@ pub const BASIC: &[(&str, &[&[&str]])] = &[ &["rotate", "reflect", "scale", "shear", "reshape", "freeTransform", "puppetWarp"], &["shapeBuilder", "livePaintBucket", "livePaintSelection", "blend"], &["dimension"], - &["contour"], ], ), ("Type", &[&["areaType", "typeOnPath", "verticalAreaType", "verticalTypeOnPath"], &["type", "verticalType", "touchType"]]), diff --git a/docs/gaps.md b/docs/gaps.md index 92ee96cb9..68d08df0b 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Gaps -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-05 · **Change:** minor (Contour tool, beyond Illustrator; scores and estimates unchanged; a slow first frame is waited for instead of restarting without the graphics adapter, and the menu bar no longer waits for the installed fonts' scan; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-05 · **Change:** minor (Contour add-on, beyond Illustrator; scores and estimates unchanged; a slow first frame is waited for instead of restarting without the graphics adapter, and the menu bar no longer waits for the installed fonts' scan; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) ## Feature submission evidence @@ -28,6 +28,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| +| 2026-11-04 | minor | Contour add-on (beyond Illustrator): drag away from a selected shape's edge to grow a live copy of it, or toward the inside to shrink it; the copy carries a live Offset Path effect, so it stays editable until Bake (Expand Appearance), a second drag changes the same Contour instead of nesting one, and a floating bar sets the distance and the Sharp, Round or Square corners. `addon.contour.set` is the command behind it. It is a built-in add-on: Object › Plug-ins › Contour, and its own heading in All Tools, with no default shortcut or toolbar slot. A VectorCraft design built on Offset Path; no other application was inspected. Scores and estimates unchanged. | | 2026-11-04 | minor | Contour tool (beyond Illustrator): drag away from a selected shape's edge to grow a live copy of it, or toward the inside to shrink it; the copy carries a live Offset Path effect, so it stays editable until Bake (Expand Appearance), a second drag changes the same Contour instead of nesting one, and a floating bar sets the distance and the Sharp, Round or Square corners. `object.contour` is the command behind it. A VectorCraft design built on Offset Path; no other application was inspected. Scores and estimates unchanged. | | 2026-11-05 | minor | G12: Hyphenation › Default Language sets the language of new type unless Document Setup chose one, stored as a tag; each run hyphenates in its own language, English only so far, so 2 Preferences options stay unused (#394) | | 2026-11-03 | minor | New gradients use CMYK default stops in CMYK documents, including Gradient tool creation. Scores and estimates unchanged. | diff --git a/docs/mcp.md b/docs/mcp.md index 797ac023b..8fd7a394a 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -6780,6 +6780,27 @@ The panel sets the next segment and runs Remove Points Keep Shape (`addon.pathto {"name":"run_command","arguments":{"command":"addon.pen.reshape","params":{"id":12,"segment":0,"t":0.3,"dx":0,"dy":-25}}} ``` +## Contour add-on + +Contour is a built-in add-on: it grows or shrinks a copy of a path or shape by a distance, live. Its command is +`addon.contour.set`; in the desktop app it lives in Object › Plug-ins › Contour and in the Contour Tool (`contour`), which +the toolbars don't show until it is added from the All Tools drawer (or chosen with `tool.select`); it has no shortcut. A +floating bar over the selection (the distance, Sharp / Round / Square and Bake) acts through the same commands. + +`addon.contour.set {offset, corners?, miterLimit?}` puts a copy of each selected path, shape or compound path directly above +it with a live Offset Path effect of `offset` points (negative insets), and selects the copies. `corners` is `"sharp"` +(default), `"round"` or `"square"` (corners cut off flat), `miterLimit` 1 to 500 (4; sharp corners). A selected object that +already has that effect (a Contour made earlier) has its effect changed instead of getting another, so the same call +adjusts a Contour. The original is kept, and nothing is baked: `effect.expandAppearance` turns a Contour into plain paths. +Type, images and groups are left alone; with nothing to offset it says "select paths or shapes". One undo step. Result: +`{ids, created, updated}`. + +The tool (`contour`) drags a distance from the selected shape's edge, outward to grow and inward to shrink: the press +changes nothing until the pointer moves, a Contour that already stands at a distance continues from it, Shift rounds to +whole points and Escape cancels. A drag keeps a Contour's own corners and miter limit; a new Contour takes the tool's +options `corners` and `miterLimit` (`tool.setOption`). A border with a gap is two Contours of one shape (the gap first, so +it ends up in front), baked with `effect.expandAppearance`, then `object.pathfinder.minusFront`. + ## Color Effects add-on Color Effects is a built-in add-on: its effects are colour adjustments in Effect › Color Effects, its command is diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 76c4c4ab2..4b90d9967 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with Adobe Illustrator -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-05 · **Change:** minor (Contour tool, beyond Illustrator; scores and estimates unchanged; Select by Artboards add-on selects by overlap, inside or outside several artboards; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-05 · **Change:** minor (Contour add-on, beyond Illustrator; scores and estimates unchanged; Optical kerning spaces pairs by the font's own stem spacing instead of a fixed tight gap; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) ## Feature submission evidence @@ -482,7 +482,7 @@ since 2026-10-01. | **Agent automation** | beyond Illustrator | Every command, gesture and dialog is drivable over MCP, the CLI and the control channel; synthetic-input counts, pending events and per-frame control requests are bounded (#1709); what an agent reads and writes on disk can be confined to chosen folders (`--automation-read-root` / `--automation-write-root`, as in PhotoCraft, #832) | | **Scrubby numeric fields** | beyond Illustrator | As in Photoshop, dragging a numeric field's label (`W:`, `Opacity:`, the Character panel's icons) left or right steps its value live; one undo step per drag, Escape cancels (#400) | | **Align Stroke on live type** | beyond Illustrator | The Stroke panel's Align Stroke works on type's character stroke: inside or outside the glyphs without Create Outlines, the type staying editable (as InDesign does; users have asked Illustrator for it since 2010, e.g. [redd.it/1dwhos6](https://redd.it/1dwhos6)). Canvas, PDF and EPS clip it to its side of the glyphs; SVG keeps the text live and draws the stroke on the glyph outlines (#2294) | -| **Contour tool** | beyond Illustrator | Drag away from a selected shape's edge to grow a live copy of it, or toward the inside to shrink it (Shift: whole points); the copy carries a live Offset Path effect so it stays editable, with its own fill and stroke, until Bake (Expand Appearance). A second drag on a selected Contour changes it instead of nesting another; Escape cancels. A floating bar above the selection sets the distance and the corners (Sharp, Round, Square); `object.contour` makes or changes one over MCP and the control channel. Two Contours baked and combined with Minus Front give a border with a gap. Paths, shapes and compound paths only; type isn't offset. Tests: `crates/engine/src/tests/contour.rs`, `crates/ui-egui/src/canvas/tests.rs` | +| **Contour add-on** | beyond Illustrator | Drag away from a selected shape's edge to grow a live copy of it, or toward the inside to shrink it (Shift: whole points); the copy carries a live Offset Path effect so it stays editable, with its own fill and stroke, until Bake (Expand Appearance). A second drag on a selected Contour changes it instead of nesting another; Escape cancels. A floating bar above the selection sets the distance and the corners (Sharp, Round, Square); `addon.contour.set` makes or changes one over MCP and the control channel. The tool is in Object › Plug-ins › Contour and under its own heading in All Tools, with no default shortcut or toolbar slot. Two Contours baked and combined with Minus Front give a border with a gap. Paths, shapes and compound paths only; type isn't offset. Tests: `crates/engine/src/cmd/addons/contour/tests.rs`, `crates/ui-egui/src/canvas/tests.rs` | ## Evidence: what ships today, by area @@ -1650,6 +1650,7 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | Date | Change | Summary | |---|---|---| +| 2026-11-04 | minor | Contour add-on (beyond Illustrator): drag away from a selected shape's edge to grow a live copy of it, or toward the inside to shrink it; the copy carries a live Offset Path effect, so it stays editable until Bake (Expand Appearance), a second drag changes the same Contour instead of nesting one, and a floating bar sets the distance and the Sharp, Round or Square corners. `addon.contour.set` is the command behind it. It is a built-in add-on: Object › Plug-ins › Contour, and its own heading in All Tools, with no default shortcut or toolbar slot. A VectorCraft design built on Offset Path; no other application was inspected. Scores and estimates unchanged. | | 2026-11-04 | minor | Contour tool (beyond Illustrator): drag away from a selected shape's edge to grow a live copy of it, or toward the inside to shrink it; the copy carries a live Offset Path effect, so it stays editable until Bake (Expand Appearance), a second drag changes the same Contour instead of nesting one, and a floating bar sets the distance and the Sharp, Round or Square corners. `object.contour` is the command behind it. A VectorCraft design built on Offset Path; no other application was inspected. Scores and estimates unchanged. | | 2026-11-05 | minor | View & navigation: document tabs are reordered by dragging them along the tab strip (`document.move`, #5663); evidence added; scores unchanged. | | 2026-10-30 | minor | Effects: Color Halftone's Max Radius is 4–127 px of the Document Raster Effects resolution, shown in px whatever the units; older documents open converted, and files saved for older app versions hold points (#3260); scores unchanged. | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 282eea9b6..ac9cf181b 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-05 · **Change:** minor (Contour tool, beyond Illustrator; scores and estimates unchanged; Select by Artboards add-on selects by overlap, inside or outside several artboards; Cmd+Space keeps the active drawing cursor during its drag; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-05 · **Change:** minor (Contour add-on, beyond Illustrator; scores and estimates unchanged; Select by Artboards add-on selects by overlap, inside or outside several artboards; Cmd+Space keeps the active drawing cursor during its drag; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged) ## Feature submission evidence @@ -437,6 +437,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development | Date | Change | Summary | |---|---|---| +| 2026-11-04 | minor | Contour add-on (beyond Illustrator): drag away from a selected shape's edge to grow a live copy of it, or toward the inside to shrink it; the copy carries a live Offset Path effect, so it stays editable until Bake (Expand Appearance), a second drag changes the same Contour instead of nesting one, and a floating bar sets the distance and the Sharp, Round or Square corners. `addon.contour.set` is the command behind it. It is a built-in add-on: Object › Plug-ins › Contour, and its own heading in All Tools, with no default shortcut or toolbar slot. A VectorCraft design built on Offset Path; no other application was inspected. Scores and estimates unchanged. | | 2026-11-04 | minor | Contour tool (beyond Illustrator): drag away from a selected shape's edge to grow a live copy of it, or toward the inside to shrink it; the copy carries a live Offset Path effect, so it stays editable until Bake (Expand Appearance), a second drag changes the same Contour instead of nesting one, and a floating bar sets the distance and the Sharp, Round or Square corners. `object.contour` is the command behind it. A VectorCraft design built on Offset Path; no other application was inspected. Scores and estimates unchanged. | | 2026-11-05 | minor | Document tabs are reordered by dragging them along the tab strip (`document.move`, #5663). | | 2026-11-04 | minor | Path Precision Tools add-on (a VectorCraft addition): Set Corners… rounds only the corners in an angle range (#5545); scores and estimates unchanged |