mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
M3.72: Tile Edge Color dialog and Show Swatch Bounds in pattern editing
Object > Pattern > Tile Edge Color... picks the colour of the tile edge
(a layer-colour preset or a custom colour), stored in the preference
patternTileEdgeColor and passed to the renderer. Pattern Options gains
Show Swatch Bounds: a dashed outline of the part of the tiling the swatch
repeats (the period of brick and hex tilings).
Commands: prefs.set {patternTileEdgeColor}, pattern.options
{showSwatchBounds}, ui.tileEdgeColor. Model: PatternDef.show_swatch_bounds.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
d8b942c43d
commit
b36fd67bf4
15 files changed
+343
-5
No files matched your search
@@ -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())
|
||||
|
||||
@@ -65,7 +65,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"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<Value> {
|
||||
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<Value> {
|
||||
"copies": p.copies,
|
||||
"dimCopies": p.dim_copies,
|
||||
"showTileEdge": p.show_tile_edge,
|
||||
"showSwatchBounds": p.show_swatch_bounds,
|
||||
"artCount": p.art.len(),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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::<PatternDef>(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::<Prefs>(old).unwrap().pattern_tile_edge_color), default);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<bool> = (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))));
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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<Value, String> {
|
||||
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<Color> {
|
||||
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<Value, String> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -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<R
|
||||
let names = p.get("names").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_str).map(str::to_string).collect());
|
||||
crate::dialogs::graphic_style_options::open_merge(app, names.unwrap_or_default())
|
||||
}
|
||||
"ui.tileEdgeColor" => crate::dialogs::tile_edge_color::open(app),
|
||||
_ => return None,
|
||||
};
|
||||
Some(r)
|
||||
@@ -996,7 +1003,10 @@ pub fn menu_tree() -> Vec<(&'static str, Vec<Item>)> {
|
||||
],
|
||||
),
|
||||
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![
|
||||
|
||||
@@ -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<Value> {
|
||||
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<egui::Event>) -> 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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
+10
@@ -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"}}}
|
||||
```
|
||||
Reference in new issue
Block a user