diff --git a/crates/doc/src/pattern.rs b/crates/doc/src/pattern.rs index d151462a..cc89787e 100644 --- a/crates/doc/src/pattern.rs +++ b/crates/doc/src/pattern.rs @@ -182,6 +182,10 @@ pub struct PatternDef { pub dim_copies: f32, #[serde(default = "yes")] pub show_tile_edge: bool, + /// Show Swatch Bounds: pattern editing mode outlines the part of the tiling the swatch repeats + /// (the tile, or the period of a brick or hex tiling), dashed. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub show_swatch_bounds: bool, } impl PatternDef { @@ -200,6 +204,7 @@ impl PatternDef { copies: 5, dim_copies: 70.0, show_tile_edge: true, + show_swatch_bounds: false, } } pub fn width(&self) -> f64 { @@ -226,6 +231,11 @@ impl PatternDef { pub fn period(&self) -> (f64, f64) { tile_period(self.tile_type, self.width(), self.height()) } + /// The swatch bounds (Show Swatch Bounds): the period of the tiling from the tile's origin. + pub fn swatch_bounds(&self) -> Rect { + let (w, h) = self.period(); + Rect::from_origin_size(self.tile.origin(), (w, h)) + } /// Maps the art's coordinates into pattern space for instance offset `o`. pub fn instance_xf(&self, o: Vec2) -> Affine { Affine::translate(o - self.tile.origin().to_vec2()) diff --git a/crates/engine/src/cmd/patterncmds.rs b/crates/engine/src/cmd/patterncmds.rs index 7da22bf9..17fd8f27 100644 --- a/crates/engine/src/cmd/patterncmds.rs +++ b/crates/engine/src/cmd/patterncmds.rs @@ -65,7 +65,7 @@ pub fn specs() -> Vec { "Pattern Options", ["Window", "Pattern Options"], None, - "{name? (default: the pattern being edited), newName?, tileType?, brickOffset?, width?, height?, hSpacing?, vSpacing?, sizeTileToArt?: bool, overlap?: {h: left|right, v: top|bottom}, copies?: 3|5|7|9, dimCopies?: %, showTileEdge?: bool}", + "{name? (default: the pattern being edited), newName?, tileType?, brickOffset?, width?, height?, hSpacing?, vSpacing?, sizeTileToArt?: bool, overlap?: {h: left|right, v: top|bottom}, copies?: 3|5|7|9, dimCopies?: %, showTileEdge?: bool, showSwatchBounds?: bool (outline the part of the tiling the swatch repeats, dashed)}", has_doc, pattern_options ), @@ -457,6 +457,9 @@ fn pattern_options(s: &mut Session, p: &Value) -> Result { if let Some(v) = p.get("showTileEdge").and_then(Value::as_bool) { def.show_tile_edge = v; } + if let Some(v) = p.get("showSwatchBounds").and_then(Value::as_bool) { + def.show_swatch_bounds = v; + } if def.size_tile_to_art { let saved = std::mem::take(&mut def.art); def.art = live_art.clone().unwrap_or_else(|| saved.clone()); @@ -574,6 +577,7 @@ fn pattern_list(s: &mut Session, _: &Value) -> Result { "copies": p.copies, "dimCopies": p.dim_copies, "showTileEdge": p.show_tile_edge, + "showSwatchBounds": p.show_swatch_bounds, "artCount": p.art.len(), }) }) diff --git a/crates/engine/src/cmd/prefscmds.rs b/crates/engine/src/cmd/prefscmds.rs index 7059aa32..d9d49afe 100644 --- a/crates/engine/src/cmd/prefscmds.rs +++ b/crates/engine/src/cmd/prefscmds.rs @@ -292,6 +292,8 @@ pub const PREF_SPECS: &[PrefSpec] = &[ p!("touchGestures", "Devices", "", "Enable Touch Gestures", bool), // Graphic Styles panel menu p!("overrideCharColor", "Type", "Graphic Styles", "Override Character Color", bool), + // Object → Pattern → Tile Edge Color + p!("patternTileEdgeColor", "Guides & Grid", "Pattern Editing", "Tile Edge Color", color), ]; pub fn spec(key: &str) -> Option<&'static PrefSpec> { @@ -377,6 +379,7 @@ impl Session { pub fn apply_prefs(&mut self, p: Prefs) { let grid_changed = p.gridline_every != self.prefs.gridline_every || p.grid_subdivisions != self.prefs.grid_subdivisions; let history_changed = p.history_states != self.prefs.history_states; + let tile_edge_changed = p.pattern_tile_edge_color != self.prefs.pattern_tile_edge_color; self.prefs = p; vectorcraft_render::set_default_threads(u16::try_from(self.prefs.render_threads).ok()); for st in &mut self.docs { @@ -385,6 +388,10 @@ impl Session { let over = st.history.undo.len().saturating_sub(st.history.limit); st.history.undo.drain(..over); } + // Documents in pattern editing mode redraw their tile edge. + if tile_edge_changed && st.doc.pattern_edit.is_some() { + st.revision += 1; + } if grid_changed { let d = std::sync::Arc::make_mut(&mut st.doc); d.grid.spacing = self.prefs.gridline_every; diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 4ccdb00a..cda8edf8 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -310,6 +310,10 @@ pub struct Prefs { /// Override Character Color: a graphic style applied to type replaces its characters' fill /// and stroke with the style's fills and strokes. pub override_char_color: bool, + // Pattern editing + /// Object → Pattern → Tile Edge Color (`#rrggbb`): the tile edge and swatch bounds in pattern + /// editing mode. + pub pattern_tile_edge_color: String, } impl Default for Prefs { @@ -423,6 +427,10 @@ impl Default for Prefs { touch_workspace: true, touch_gestures: true, override_char_color: true, + pattern_tile_edge_color: { + let [r, g, b] = vectorcraft_doc::LAYER_COLORS[0].1; + Color::rgb8(r, g, b).to_hex() + }, } } } @@ -828,4 +836,6 @@ mod tests_swatchlib; #[cfg(test)] mod tests_textedit; #[cfg(test)] +mod tests_tileedge; +#[cfg(test)] mod tests_xform; diff --git a/crates/engine/src/tests_tileedge.rs b/crates/engine/src/tests_tileedge.rs new file mode 100644 index 00000000..c6865052 --- /dev/null +++ b/crates/engine/src/tests_tileedge.rs @@ -0,0 +1,63 @@ +//! Pattern editing: the Tile Edge Color preference and Pattern Options' Show Swatch Bounds. + +use serde_json::{Value, json}; +use vectorcraft_doc::pattern::PatternDef; + +use super::*; + +fn run(s: &mut Session, id: &str, p: Value) -> Value { + s.execute(id, &p).unwrap_or_else(|e| panic!("{id} {p}: {e}")) +} + +/// A session editing pattern "Dots" made from a 10×10 rectangle. +fn editing() -> Session { + let mut s = Session::new(); + run(&mut s, "file.new", json!({"width": 200, "height": 200})); + run(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10})); + run(&mut s, "object.pattern.make", json!({"name": "Dots", "width": 20, "height": 20})); + s +} + +fn def(s: &Session) -> PatternDef { + s.doc().unwrap().doc.pattern("Dots").unwrap().clone() +} + +#[test] +fn show_swatch_bounds_is_a_pattern_option_that_saves() { + let mut s = editing(); + assert!(!def(&s).show_swatch_bounds); + run(&mut s, "pattern.options", json!({"showSwatchBounds": true})); + assert!(def(&s).show_swatch_bounds); + assert_eq!(run(&mut s, "pattern.list", json!({}))["patterns"][0]["showSwatchBounds"], true); + // One undo step. + run(&mut s, "edit.undo", json!({})); + assert!(!def(&s).show_swatch_bounds); + run(&mut s, "edit.redo", json!({})); + // Saved with the document; files from before the option load with it off. + let bytes = vectorcraft_format::save(&s.doc().unwrap().doc, false); + let back = vectorcraft_format::load(&bytes).unwrap(); + assert_eq!(back.patterns, s.doc().unwrap().doc.patterns); + assert!(back.pattern("Dots").unwrap().show_swatch_bounds); + let mut old = serde_json::to_value(def(&s)).unwrap(); + old.as_object_mut().unwrap().remove("showSwatchBounds"); + assert!(!serde_json::from_value::(old).unwrap().show_swatch_bounds); +} + +#[test] +fn tile_edge_color_is_a_preference_that_redraws_pattern_editing() { + let mut s = editing(); + let default = run(&mut s, "prefs.get", json!({"key": "patternTileEdgeColor"})); + let [r, g, b] = vectorcraft_doc::LAYER_COLORS[0].1; + assert_eq!(default, vectorcraft_color::Color::rgb8(r, g, b).to_hex()); + let rev = s.doc().unwrap().revision; + run(&mut s, "prefs.set", json!({"key": "patternTileEdgeColor", "value": "FF0000"})); + assert_eq!(s.prefs.pattern_tile_edge_color, "#ff0000"); + assert!(s.doc().unwrap().revision > rev, "the canvas redraws the tile edge"); + assert!(s.execute("prefs.set", &json!({"key": "patternTileEdgeColor", "value": "red"})).is_err()); + // Saved with the preferences; older preferences get the default. + let back: Prefs = serde_json::from_value(s.prefs.to_json()).unwrap(); + assert_eq!(back.pattern_tile_edge_color, "#ff0000"); + let mut old = s.prefs.to_json(); + old.as_object_mut().unwrap().remove("patternTileEdgeColor"); + assert_eq!(json!(serde_json::from_value::(old).unwrap().pattern_tile_edge_color), default); +} diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index f95b0334..07a06156 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -79,6 +79,9 @@ pub struct RenderOptions { pub trim: bool, /// Leave template layers out (exports and thumbnails: templates are guides, not artwork). pub skip_templates: bool, + /// Pattern editing mode's tile edge and swatch bounds colour (Object → Pattern → Tile Edge + /// Color), RGB. + pub tile_edge: [u8; 3], } impl Default for RenderOptions { @@ -93,6 +96,7 @@ impl Default for RenderOptions { overprint_preview: false, trim: false, skip_templates: false, + tile_edge: vectorcraft_doc::LAYER_COLORS[0].1, } } } @@ -1170,3 +1174,5 @@ mod tests_isolation; mod tests_knockout; #[cfg(test)] mod tests_objectfx; +#[cfg(test)] +mod tests_tileedge; diff --git a/crates/render/src/pattern.rs b/crates/render/src/pattern.rs index 8ca8fc09..23a729e7 100644 --- a/crates/render/src/pattern.rs +++ b/crates/render/src/pattern.rs @@ -194,9 +194,17 @@ impl Renderer { }); } self.draw_arc(ctx, f, layer); + let [r, g, b] = f.opts.tile_edge; if def.show_tile_edge { - let bp = def.tile.to_path(0.1); - self.hairline(ctx, f, &bp, [0x2d, 0x8c, 0xeb, 255]); + self.hairline(ctx, f, &def.tile.to_path(0.1), [r, g, b, 255]); + } + if def.show_swatch_bounds { + let mut bounds = def.swatch_bounds().to_path(0.1); + bounds.apply_affine(f.view); + ctx.set_transform(Affine::IDENTITY); + ctx.set_stroke(vello_cpu::kurbo::Stroke::new(1.0).with_dashes(0.0, [4.0, 3.0])); + ctx.set_paint(peniko::Color::from_rgba8(r, g, b, 255)); + ctx.stroke_path(&bounds); } true } diff --git a/crates/render/src/tests_tileedge.rs b/crates/render/src/tests_tileedge.rs new file mode 100644 index 00000000..3657f0b3 --- /dev/null +++ b/crates/render/src/tests_tileedge.rs @@ -0,0 +1,69 @@ +//! Pattern editing mode: the tile edge in the Tile Edge Color, and Show Swatch Bounds. + +use super::*; +use vectorcraft_color::{Color, Paint}; +use vectorcraft_doc::pattern::{PatternDef, PatternEdit, TileType}; +use vectorcraft_doc::{Appearance, Node}; +use vectorcraft_geom::shapes; + +/// A document editing a 20×20 pattern tile of type `tt` (a red 10×10 square in its corner, no +/// preview copies), its tile edge and swatch bounds shown as asked. +fn editing(tt: TileType, edge: bool, bounds: bool) -> Document { + let mut d = Document::new(200.0, 200.0); + let id = d.alloc_id(); + let red = Node::path( + id, + shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), + Appearance::basic(Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0), + ); + let mut def = PatternDef::new("Checks", vec![Arc::new(red.clone())]); + def.tile = Rect::new(0.0, 0.0, 20.0, 20.0); + (def.tile_type, def.copies, def.show_tile_edge, def.show_swatch_bounds) = (tt, 1, edge, bounds); + d.patterns.push(def); + let layer = d.add_layer(Some("Pattern Editing Mode")); + d.insert(Some(layer), 0, red).unwrap(); + d.pattern_edit = Some(PatternEdit { pattern: "Checks".into(), layer, original: None }); + d +} + +/// `d` at 1:1 with the tile's corner at the centre of pixel (50, 50), on white. +fn render(d: &Document, tile_edge: Option<[u8; 3]>) -> Rendered { + let opts = RenderOptions { background: Some([255; 4]), tile_edge: tile_edge.unwrap_or(RenderOptions::default().tile_edge), ..Default::default() }; + Renderer::new().render(d, 120, 120, Affine::translate((50.5, 50.5)), &opts) +} + +fn is_white(p: [u8; 4]) -> bool { + p[0] > 245 && p[1] > 245 && p[2] > 245 +} + +#[test] +fn the_tile_edge_takes_the_tile_edge_color() { + let green = render(&editing(TileType::Grid, true, false), Some([0, 200, 0])); + // The left edge below the red square, and the bottom edge. + for (x, y) in [(50, 65), (60, 70), (70, 55)] { + let p = green.pixel(x, y); + assert!(p[1] > 150 && p[0] < 100 && p[2] < 100, "({x},{y}) {p:?}"); + } + assert!(is_white(green.pixel(65, 65)), "inside the tile"); + // The default is the first layer colour (light blue). + let [r, g, b] = RenderOptions::default().tile_edge; + let p = render(&editing(TileType::Grid, true, false), None).pixel(50, 65); + assert!(p[0].abs_diff(r) < 30 && p[1].abs_diff(g) < 30 && p[2].abs_diff(b) < 30, "{p:?}"); + // Hidden: nothing there. + assert!(is_white(render(&editing(TileType::Grid, false, false), Some([0, 200, 0])).pixel(50, 65))); +} + +#[test] +fn swatch_bounds_outline_the_period_dashed() { + // Brick by row repeats every two rows: the swatch is 20×40 from the tile's corner. + let tt = TileType::BrickByRow { offset: 0.5 }; + let on = render(&editing(tt, false, true), Some([0, 200, 0])); + let green = |p: [u8; 4]| p[1] > 150 && p[0] < 100 && p[2] < 100; + let bottom: Vec = (51..70).map(|x| green(on.pixel(x, 90))).collect(); + assert!(bottom.iter().any(|g| *g) && bottom.iter().any(|g| !*g), "dashed: {bottom:?}"); + assert!((51..90).any(|y| green(on.pixel(70, y))), "right side"); + assert!(!(51..70).any(|x| green(on.pixel(x, 70))), "the tile edge itself stays hidden"); + // Off: no outline. + let off = render(&editing(tt, false, false), Some([0, 200, 0])); + assert!((51..70).all(|x| is_white(off.pixel(x, 90)))); +} diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 859d0c29..779f821a 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -213,6 +213,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { proof: vectorcraft_render::proof::active_proof(), overprint_preview: vectorcraft_render::proof::overprint_preview_on(), trim: app.ui.view.trim_view, + tile_edge: vectorcraft_color::Color::from_hex(&app.session.prefs.pattern_tile_edge_color).map_or(opts.tile_edge, |c| { + let [r, g, b, _] = c.to_rgba8(1.0); + [r, g, b] + }), ..opts }; // Light documents render synchronously (no lag vs overlays); heavy ones go to the worker. diff --git a/crates/ui-egui/src/dialogs/mod.rs b/crates/ui-egui/src/dialogs/mod.rs index 6a41958b..c53552a6 100644 --- a/crates/ui-egui/src/dialogs/mod.rs +++ b/crates/ui-egui/src/dialogs/mod.rs @@ -30,6 +30,7 @@ mod save_changes; pub mod save_swatch_library; mod shapes; pub mod swatch_options; +pub mod tile_edge_color; mod tools; mod transform; @@ -152,6 +153,7 @@ registry! { ColorBalance: [color_balance::KIND] => color_balance::SPEC, Saturate: [saturate::KIND] => saturate::SPEC, SaveSwatchLibrary: [save_swatch_library::KIND] => save_swatch_library::SPEC, + TileEdgeColor: [tile_edge_color::KIND] => tile_edge_color::SPEC, } /// The spec for a `Dialog::kind` ([`DialogSpec::FORM`] when unregistered). diff --git a/crates/ui-egui/src/dialogs/tile_edge_color.rs b/crates/ui-egui/src/dialogs/tile_edge_color.rs new file mode 100644 index 00000000..3efcdfa2 --- /dev/null +++ b/crates/ui-egui/src/dialogs/tile_edge_color.rs @@ -0,0 +1,93 @@ +//! Object → Pattern → Tile Edge Color: the colour pattern editing mode draws the tile edge and the +//! swatch bounds in, a preset (the layer colours) or a custom colour. OK sets the preference +//! `patternTileEdgeColor` (`prefs.set`). +//! +//! Fields: `color` (`#rrggbb`, or a preset's name such as "Light Blue"). + +use serde_json::{Value, json}; +use vectorcraft_color::Color; +use vectorcraft_doc::LAYER_COLORS; + +use super::swatch_options::{grid, label}; +use super::{DialogSpec, run_and_close}; +use crate::state::Dialog; +use crate::{VectorcraftApp, widgets}; + +/// The dialog kind of Tile Edge Color. +pub const KIND: &str = "tileEdgeColor"; + +pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| "Tile Edge Color".into(), body, confirm, min_width: 300.0, ..DialogSpec::FORM }; + +/// Open Tile Edge Color on the current preference. +pub fn open(app: &mut VectorcraftApp) -> Result { + app.ui.dialog = Some(Dialog::new(KIND, json!({ "color": app.session.prefs.pattern_tile_edge_color }))); + Ok(Value::Null) +} + +/// `color` (a preset's name or a hex colour) as a colour. +fn resolve(color: &str) -> Option { + match LAYER_COLORS.iter().find(|(name, _)| name.eq_ignore_ascii_case(color.trim())) { + Some((_, [r, g, b])) => Some(Color::rgb8(*r, *g, *b)), + None => Color::from_hex(color), + } +} + +fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { + let color = resolve(&d.str("color")).unwrap_or(Color::BLACK); + let [r, g, b, _] = color.to_rgba8(1.0); + let preset = LAYER_COLORS.iter().position(|(_, c)| *c == [r, g, b]); + let names: Vec<&str> = LAYER_COLORS.iter().map(|(n, _)| *n).chain(["Custom"]).collect(); + grid(ui, |ui| { + label(ui, "Color:"); + ui.horizontal(|ui| { + if let Some((_, [r, g, b])) = + widgets::dropdown(ui, "tile-edge", names[preset.unwrap_or(LAYER_COLORS.len())], &names, 140.0).and_then(|i| LAYER_COLORS.get(i)) + { + d.fields.insert("color".into(), json!(Color::rgb8(*r, *g, *b).to_hex())); + } + // Any other colour (the preset list shows Custom then). + let mut rgb = [r, g, b]; + if ui.color_edit_button_srgb(&mut rgb).changed() { + d.fields.insert("color".into(), json!(Color::rgb8(rgb[0], rgb[1], rgb[2]).to_hex())); + } + }); + ui.end_row(); + }); + false +} + +fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { + let color = d.str("color"); + let hex = resolve(&color).ok_or_else(|| format!("`{color}` is not a colour (#rrggbb or a preset name)"))?.to_hex(); + run_and_close(app, "prefs.set", json!({ "key": "patternTileEdgeColor", "value": hex })) +} + +#[cfg(test)] +mod tests { + use super::*; + use vectorcraft_engine::Session; + + #[test] + fn presets_or_custom_colours_set_the_preference() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("ui.tileEdgeColor", json!({})).unwrap(); + assert_eq!(app.ui.dialog.as_ref().unwrap().str("color"), app.session.prefs.pattern_tile_edge_color); + // Drawn headlessly in the shared frame: the preset's name shows. + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let mut out = ctx.run_ui(Default::default(), |ui| super::super::show(&mut app, ui.ctx())); + out.textures_delta.clear(); + let set = |app: &mut VectorcraftApp, v: &str| { + app.ui.dialog.as_mut().unwrap().fields.insert("color".into(), json!(v)); + super::super::confirm(app) + }; + set(&mut app, "Red").unwrap(); + assert_eq!(app.session.prefs.pattern_tile_edge_color, "#ff4f4f"); + assert!(app.ui.dialog.is_none()); + app.run("ui.tileEdgeColor", json!({})).unwrap(); + set(&mut app, "#00FF00").unwrap(); + assert_eq!(app.session.prefs.pattern_tile_edge_color, "#00ff00"); + app.run("ui.tileEdgeColor", json!({})).unwrap(); + assert!(set(&mut app, "nope").is_err()); + } +} diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 9e3708c2..6802cfe3 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -253,6 +253,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "", "{names: [two or more styles]} open Graphic Style Options (dialog `graphicStyleOptions`, field `name`) to name the style OK merges from them (graphicStyle.merge)", ), + ( + "ui.tileEdgeColor", + "Tile Edge Color…", + "", + "{} open Tile Edge Color (dialog `tileEdgeColor`, field `color`: #rrggbb or a preset name such as \"Light Blue\"); OK sets the preference patternTileEdgeColor (prefs.set), the colour of the tile edge and swatch bounds in pattern editing mode", + ), ]; /// Handle a UI command. `None` = not a UI command (the engine handles it). @@ -600,6 +606,7 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option crate::dialogs::tile_edge_color::open(app), _ => return None, }; Some(r) @@ -996,7 +1003,10 @@ pub fn menu_tree() -> Vec<(&'static str, Vec)> { ], ), sub("Shape", vec![c("Convert to Shape", "object.shape.convertToShape"), c("Expand Shape", "object.expandShape")]), - sub("Pattern", vec![c("Make", "object.pattern.make"), c("Edit Pattern", "object.pattern.edit"), todo("Tile Edge Color…")]), + sub( + "Pattern", + vec![c("Make", "object.pattern.make"), c("Edit Pattern", "object.pattern.edit"), c("Tile Edge Color…", "ui.tileEdgeColor")], + ), sub( "Repeat", vec![ diff --git a/crates/ui-egui/src/panels/pattern_options.rs b/crates/ui-egui/src/panels/pattern_options.rs index 2abb93da..5df83a37 100644 --- a/crates/ui-egui/src/panels/pattern_options.rs +++ b/crates/ui-egui/src/panels/pattern_options.rs @@ -1,6 +1,6 @@ //! Pattern Options panel: active in pattern editing mode (Object → Pattern → Make / Edit //! Pattern). Name, Tile Type, Brick Offset, Width/Height, Size Tile to Art with H/V spacing, -//! Overlap, Copies, Dim Copies, Show Tile Edge; Save a Copy / Done / Cancel. Outside editing +//! Overlap, Copies, Dim Copies, Show Tile Edge, Show Swatch Bounds; Save a Copy / Done / Cancel. Outside editing //! mode it lists the document's patterns with an Edit button. use egui::Ui; @@ -140,6 +140,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { if widgets::check(ui, "Show Tile Edge", def.show_tile_edge, true) { set(app, &name, json!({"showTileEdge": !def.show_tile_edge})); } + if widgets::check(ui, "Show Swatch Bounds", def.show_swatch_bounds, true) { + set(app, &name, json!({"showSwatchBounds": !def.show_swatch_bounds})); + } widgets::divider(ui); ui.horizontal(|ui| { if widgets::flat_button(ui, "Save a Copy", 90.0).clicked() @@ -214,3 +217,38 @@ pub fn repeat_fields(app: &VectorcraftApp) -> Option { RepeatKind::Mirror { angle, .. } => json!({"angle": angle}), }) } + +#[cfg(test)] +mod tests { + use super::*; + use vectorcraft_engine::Session; + + /// One headless frame of the panel with `events`: where each text was drawn. + fn frame(app: &mut VectorcraftApp, ctx: &egui::Context, events: Vec) -> Vec<(String, egui::Rect)> { + let mut out = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| show(app, ui)); + out.textures_delta.clear(); + let mut v = vec![]; + for c in &out.shapes { + if let egui::Shape::Text(t) = &c.shape { + v.push((t.galley.text().to_string(), t.visual_bounding_rect())); + } + } + v + } + + #[test] + fn show_swatch_bounds_toggles_the_pattern_option() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 200, "height": 200})).unwrap(); + app.run("shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap(); + app.run("object.pattern.make", json!({"name": "Dots"})).unwrap(); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let texts = frame(&mut app, &ctx, vec![]); + let at = texts.iter().find(|(t, _)| t == "Show Swatch Bounds").expect("the checkbox").1.center(); + let click = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }; + frame(&mut app, &ctx, vec![egui::Event::PointerMoved(at), click(true)]); + frame(&mut app, &ctx, vec![click(false)]); + assert!(app.session.active().unwrap().doc.pattern("Dots").unwrap().show_swatch_bounds); + } +} diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 50c11c3c..5c616691 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -90,3 +90,7 @@ Add to Swatches. opening a `.vcswatches` or `.gpl` file with `app.open` does the same. `ui.saveSwatchLibrary {names?}` opens the `saveSwatchLibrary` dialog (fields `name`, `format`: `vcswatches`/`gpl`/`css`, `user`: save to the user library folder, `selectedOnly` with `names`); `ui.dialog.confirm` runs `swatch.library.save` (to a file it asks for a path). + +Tile Edge Color: Object → Pattern → Tile Edge Color… (`ui.tileEdgeColor`) opens the `tileEdgeColor` dialog (field +`color`: `#rrggbb` or a preset name such as "Light Blue"); `ui.dialog.confirm` sets the preference +`patternTileEdgeColor` (`prefs.set`), the colour pattern editing mode draws the tile edge and the swatch bounds in. diff --git a/docs/mcp.md b/docs/mcp.md index f2aebe12..09cdeaf9 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -371,4 +371,14 @@ that split into many regions. The result reports `{ids, vector, rasterized, opti ```json {"name":"run_command","arguments":{"command":"object.flattenTransparency","params":{"ids":[12,15],"preset":"high"}}} {"name":"run_command","arguments":{"command":"object.flattenTransparency","params":{"balance":0,"lineArtPpi":150}}} + +## Pattern editing display + +In pattern editing mode the tile edge (`pattern.options {showTileEdge}`) and the swatch bounds (`pattern.options +{showSwatchBounds}`: the part of the tiling the swatch repeats, dashed) are drawn in the preference +`patternTileEdgeColor`. + +```json +{"name":"run_command","arguments":{"command":"pattern.options","params":{"showSwatchBounds":true}}} +{"name":"run_command","arguments":{"command":"prefs.set","params":{"key":"patternTileEdgeColor","value":"#ff4f4f"}}} ```