250 lines
11 KiB
Rust
250 lines
11 KiB
Rust
//! 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, Show Swatch Bounds; Save a Copy / Done / Cancel. Outside editing
|
||
//! mode it lists the document's patterns with an Edit button.
|
||
|
||
use egui::Ui;
|
||
use serde_json::{Value, json};
|
||
use vectorcraft_doc::pattern::{PatternDef, RepeatKind, TileType};
|
||
use vectorcraft_doc::{NodeKind, Unit};
|
||
|
||
use super::{pstate, set_pstate};
|
||
use crate::VectorcraftApp;
|
||
use crate::widgets::{self, menu_item};
|
||
|
||
const TILE_LABELS: [&str; 5] = ["Grid", "Brick by Row", "Brick by Column", "Hex by Column", "Hex by Row"];
|
||
const BRICK: [(&str, f64); 4] = [("1/2", 0.5), ("1/3", 1.0 / 3.0), ("1/4", 0.25), ("1/5", 0.2)];
|
||
const COPIES: [(&str, u32); 4] = [("3 x 3", 3), ("5 x 5", 5), ("7 x 7", 7), ("9 x 9", 9)];
|
||
|
||
fn editing(app: &VectorcraftApp) -> Option<(PatternDef, Unit)> {
|
||
let st = app.session.active()?;
|
||
let pe = st.doc.pattern_edit.as_ref()?;
|
||
Some((st.doc.pattern(&pe.pattern)?.clone(), app.session.general_unit()))
|
||
}
|
||
|
||
fn row(ui: &mut Ui, label: &str, add: impl FnOnce(&mut Ui)) {
|
||
widgets::label_row(ui, label, 96.0, add);
|
||
}
|
||
|
||
fn set(app: &mut VectorcraftApp, name: &str, mut p: Value) {
|
||
p["name"] = json!(name);
|
||
if let Err(e) = app.run("pattern.options", p) {
|
||
app.status(e);
|
||
}
|
||
}
|
||
|
||
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||
if app.session.active().is_none() {
|
||
super::empty_state(ui, "swatch-book", tl!("No document"), tl!("Open a document to edit its patterns."));
|
||
return;
|
||
}
|
||
let Some((def, unit)) = editing(app) else {
|
||
idle(app, ui);
|
||
return;
|
||
};
|
||
let name = def.name.clone();
|
||
// Name (commit on Enter / focus loss).
|
||
row(ui, tl!("Name:"), |ui| {
|
||
let id = ui.id().with("po-name");
|
||
let mut buf: String = ui.data_mut(|d| d.get_temp::<String>(id)).unwrap_or_else(|| name.clone());
|
||
if !ui.memory(|m| m.has_focus(id)) {
|
||
buf = name.clone();
|
||
}
|
||
let r = ui.add(egui::TextEdit::singleline(&mut buf).id(id).desired_width(150.0));
|
||
ui.data_mut(|d| d.insert_temp(id, buf.clone()));
|
||
if r.lost_focus() && !buf.trim().is_empty() && buf != name {
|
||
set(app, &name, json!({"newName": buf.trim()}));
|
||
}
|
||
});
|
||
let tt_index = TileType::IDS.iter().position(|i| *i == def.tile_type.id()).unwrap_or(0);
|
||
row(ui, tl!("Tile Type:"), |ui| {
|
||
if let Some(i) = widgets::dropdown(ui, "po-tile", TILE_LABELS[tt_index], &TILE_LABELS, 150.0) {
|
||
set(app, &name, json!({"tileType": TileType::IDS[i]}));
|
||
}
|
||
});
|
||
let offset = match def.tile_type {
|
||
TileType::BrickByRow { offset } | TileType::BrickByColumn { offset } => Some(offset),
|
||
_ => None,
|
||
};
|
||
row(ui, tl!("Brick Offset:"), |ui| {
|
||
ui.add_enabled_ui(offset.is_some(), |ui| {
|
||
let cur = BRICK.iter().find(|(_, v)| offset.is_some_and(|o| (o - v).abs() < 1e-6)).map(|(l, _)| *l).unwrap_or("1/2");
|
||
let labels: Vec<&str> = BRICK.iter().map(|(l, _)| *l).collect();
|
||
if let Some(i) = widgets::dropdown(ui, "po-brick", cur, &labels, 70.0) {
|
||
set(app, &name, json!({"brickOffset": BRICK[i].1}));
|
||
}
|
||
});
|
||
});
|
||
let sized = def.size_tile_to_art;
|
||
row(ui, tl!("Width:"), |ui| {
|
||
ui.add_enabled_ui(!sized, |ui| {
|
||
if let Some(v) = widgets::num_field(ui, "po-w", Some(def.tile.width()), unit, 90.0) {
|
||
set(app, &name, json!({"width": v}));
|
||
}
|
||
});
|
||
});
|
||
row(ui, tl!("Height:"), |ui| {
|
||
ui.add_enabled_ui(!sized, |ui| {
|
||
if let Some(v) = widgets::num_field(ui, "po-h", Some(def.tile.height()), unit, 90.0) {
|
||
set(app, &name, json!({"height": v}));
|
||
}
|
||
});
|
||
});
|
||
if widgets::check(ui, tl!("Size Tile to Art"), sized, true) {
|
||
set(app, &name, json!({"sizeTileToArt": !sized}));
|
||
}
|
||
row(ui, tl!("H Spacing:"), |ui| {
|
||
ui.add_enabled_ui(sized, |ui| {
|
||
if let Some(v) = widgets::num_field(ui, "po-hs", Some(def.h_spacing), unit, 90.0) {
|
||
set(app, &name, json!({"hSpacing": v}));
|
||
}
|
||
});
|
||
});
|
||
row(ui, tl!("V Spacing:"), |ui| {
|
||
ui.add_enabled_ui(sized, |ui| {
|
||
if let Some(v) = widgets::num_field(ui, "po-vs", Some(def.v_spacing), unit, 90.0) {
|
||
set(app, &name, json!({"vSpacing": v}));
|
||
}
|
||
});
|
||
});
|
||
row(ui, tl!("Overlap:"), |ui| {
|
||
let h = if def.overlap.right_in_front { "Right in Front" } else { "Left in Front" };
|
||
if let Some(i) = widgets::dropdown(ui, "po-oh", h, &["Left in Front", "Right in Front"], 110.0) {
|
||
set(app, &name, json!({"overlap": {"h": if i == 1 { "right" } else { "left" }}}));
|
||
}
|
||
});
|
||
row(ui, "", |ui| {
|
||
let v = if def.overlap.bottom_in_front { "Bottom in Front" } else { "Top in Front" };
|
||
if let Some(i) = widgets::dropdown(ui, "po-ov", v, &["Top in Front", "Bottom in Front"], 110.0) {
|
||
set(app, &name, json!({"overlap": {"v": if i == 1 { "bottom" } else { "top" }}}));
|
||
}
|
||
});
|
||
widgets::divider(ui);
|
||
row(ui, tl!("Copies:"), |ui| {
|
||
let cur = COPIES.iter().find(|(_, n)| *n == def.copies).map(|(l, _)| *l).unwrap_or("5 x 5");
|
||
let labels: Vec<&str> = COPIES.iter().map(|(l, _)| *l).collect();
|
||
if let Some(i) = widgets::dropdown(ui, "po-copies", cur, &labels, 80.0) {
|
||
set(app, &name, json!({"copies": COPIES[i].1}));
|
||
}
|
||
});
|
||
row(ui, tl!("Dim Copies to:"), |ui| {
|
||
if let Some(v) = widgets::plain_field(ui, "po-dim", def.dim_copies as f64, "%", 0, 60.0) {
|
||
set(app, &name, json!({"dimCopies": v}));
|
||
}
|
||
});
|
||
if widgets::check(ui, tl!("Show Tile Edge"), def.show_tile_edge, true) {
|
||
set(app, &name, json!({"showTileEdge": !def.show_tile_edge}));
|
||
}
|
||
if widgets::check(ui, tl!("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, tl!("Save a Copy"), 90.0).clicked()
|
||
&& let Err(e) = app.run("object.pattern.saveCopy", json!({}))
|
||
{
|
||
app.status(e);
|
||
}
|
||
if widgets::flat_button(ui, tl!("Done"), 60.0).clicked() {
|
||
app.run("object.pattern.done", json!({})).ok();
|
||
}
|
||
if widgets::flat_button(ui, tl!("Cancel"), 60.0).clicked() {
|
||
app.run("object.pattern.cancel", json!({})).ok();
|
||
}
|
||
});
|
||
}
|
||
|
||
/// Not editing: the document's patterns, one to pick and edit.
|
||
fn idle(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||
let names: Vec<String> = app.session.active().map(|st| st.doc.patterns.iter().map(|p| p.name.clone()).collect()).unwrap_or_default();
|
||
widgets::dim_label(ui, tl!("Pattern options are available in pattern editing mode."));
|
||
ui.add_space(4.0);
|
||
if names.is_empty() {
|
||
widgets::dim_label(ui, tl!("Select art and choose Object › Pattern › Make."));
|
||
return;
|
||
}
|
||
let mut chosen: String = pstate(ui.ctx(), "po-chosen");
|
||
if !names.contains(&chosen) {
|
||
chosen = names[0].clone();
|
||
}
|
||
ui.horizontal(|ui| {
|
||
let labels: Vec<&str> = names.iter().map(String::as_str).collect();
|
||
if let Some(i) = widgets::dropdown_names(ui, "po-pick", &chosen, &labels, 150.0) {
|
||
set_pstate(ui.ctx(), "po-chosen", names[i].clone());
|
||
}
|
||
if widgets::flat_button(ui, tl!("Edit Pattern"), 90.0).clicked()
|
||
&& let Err(e) = app.run("object.pattern.edit", json!({"name": chosen}))
|
||
{
|
||
app.status(e);
|
||
}
|
||
});
|
||
}
|
||
|
||
pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||
let editing = editing(app).is_some();
|
||
if menu_item(ui, tl!("Save a Copy"), editing, false) {
|
||
app.run("object.pattern.saveCopy", json!({})).ok();
|
||
}
|
||
if menu_item(ui, tl!("Done"), editing, false) {
|
||
app.run("object.pattern.done", json!({})).ok();
|
||
}
|
||
if menu_item(ui, tl!("Cancel"), editing, false) {
|
||
app.run("object.pattern.cancel", json!({})).ok();
|
||
}
|
||
}
|
||
|
||
/// Current values of the selected repeat as Repeat Options dialog fields.
|
||
pub fn repeat_fields(app: &VectorcraftApp) -> Option<Value> {
|
||
let st = app.session.active()?;
|
||
let spec = st.selection.objects.iter().find_map(|id| {
|
||
st.doc.ancestry(*id)?.into_iter().rev().find_map(|a| match &st.doc.node(a)?.kind {
|
||
NodeKind::Repeat(r) => Some(r.clone()),
|
||
_ => None,
|
||
})
|
||
})?;
|
||
Some(match spec.kind {
|
||
RepeatKind::Radial { instances, radius, reverse_overlap, start_angle, end_angle, .. } => {
|
||
json!({"instances": instances, "radius": radius, "startAngle": start_angle, "endAngle": end_angle, "reverseOverlap": reverse_overlap})
|
||
}
|
||
RepeatKind::Grid { h_spacing, v_spacing, rows, cols, grid_type, flip_rows, flip_cols } => {
|
||
json!({"hSpacing": h_spacing, "vSpacing": v_spacing, "rows": rows, "cols": cols, "gridType": grid_type.id(), "flipRows": flip_rows, "flipCols": flip_cols})
|
||
}
|
||
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);
|
||
}
|
||
}
|