diff --git a/crates/ui-egui/src/dialogs/form.rs b/crates/ui-egui/src/dialogs/form.rs index 0cda6ac0..0458d923 100644 --- a/crates/ui-egui/src/dialogs/form.rs +++ b/crates/ui-egui/src/dialogs/form.rs @@ -49,7 +49,7 @@ pub(super) fn length(ui: &mut egui::Ui, d: &mut Dialog, key: &str, unit: Unit, w fn lengths(kind: &str) -> &'static [&'static str] { match kind { "move" => &["dx", "dy"], - "rectangle" | "ellipse" | "artboardOptions" => &["width", "height"], + "rectangle" | "ellipse" | "rectangularGrid" | "artboardOptions" => &["width", "height"], "roundedRectangle" => &["width", "height", "radius"], "polygon" => &["radius"], "star" => &["radius1", "radius2"], @@ -67,6 +67,7 @@ fn order(kind: &str) -> &'static [&'static str] { match kind { "offsetPath" => &["offset", "joins", "miterLimit"], "splitIntoGrid" => &["rows", "columns", "gutter"], + "rectangularGrid" => &["width", "height", "rows", "columns"], "artboardOptions" => &["name", "width", "height"], _ => &[], } @@ -178,7 +179,12 @@ pub(super) fn grid(ui: &mut egui::Ui, d: &mut Dialog, unit: Unit) { if lengths.contains(&k.as_str()) { length_field(ui, d, &k, &humanized(&k), unit); } else if options.is_empty() { - field(ui, d, &k, &humanized(&k)); + let label = match (d.kind.as_str(), k.as_str()) { + ("rectangularGrid", "rows") => tl!("Horizontal Dividers:").into(), + ("rectangularGrid", "columns") => tl!("Vertical Dividers:").into(), + _ => humanized(&k), + }; + field(ui, d, &k, &label); } else { row_label(ui, &humanized(&k)); dropdown(ui, d, &k, FIELD_W, &options, true); diff --git a/crates/ui-egui/src/dialogs/mod.rs b/crates/ui-egui/src/dialogs/mod.rs index a02abf87..d1bb3296 100644 --- a/crates/ui-egui/src/dialogs/mod.rs +++ b/crates/ui-egui/src/dialogs/mod.rs @@ -205,7 +205,7 @@ macro_rules! registry { registry! { NewDocument: [new_document::KIND] => new_document::SPEC, - Shape: ["rectangle", "roundedRectangle", "ellipse", "polygon", "star", "lineSegment"] => shapes::SPEC, + Shape: ["rectangle", "roundedRectangle", "ellipse", "polygon", "star", "lineSegment", "rectangularGrid"] => shapes::SPEC, Transform: ["move", "rotate", "scale", "reflect", "shear"] => transform::SPEC, PathOp: ["average", "offsetPath", "simplify", "splitIntoGrid"] => path_ops::SPEC, DocumentSetup: ["documentSetup"] => document_setup::SPEC, @@ -513,3 +513,6 @@ mod tests_perspective; #[cfg(test)] mod tests_modal; + +#[cfg(test)] +mod tests_rectangular_grid; diff --git a/crates/ui-egui/src/dialogs/shapes.rs b/crates/ui-egui/src/dialogs/shapes.rs index aa57f977..56680c12 100644 --- a/crates/ui-egui/src/dialogs/shapes.rs +++ b/crates/ui-egui/src/dialogs/shapes.rs @@ -16,6 +16,7 @@ fn title(kind: &str) -> &'static str { "ellipse" => tl!("Ellipse"), "polygon" => tl!("Polygon"), "star" => tl!("Star"), + "rectangularGrid" => tl!("Rectangular Grid"), _ => tl!("Line Segment Tool Options"), } } @@ -29,6 +30,11 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { "shape.star", json!({"cx": x, "cy": y, "radius1": d.f64("radius1", 50.0), "radius2": d.f64("radius2", 25.0), "points": d.f64("points", 5.0) as u64}), ), + "rectangularGrid" => ( + "shape.rectangularGrid", + json!({"x": x, "y": y, "width": d.f64("width", 100.0), "height": d.f64("height", 100.0), + "rows": (d.f64("rows", 5.0) as u64).min(999), "columns": (d.f64("columns", 5.0) as u64).min(999)}), + ), "lineSegment" => { let (l, a) = (d.f64("length", 100.0), d.f64("angle", 0.0).to_radians()); ("shape.line", json!({"x1": x, "y1": y, "x2": x + l * a.cos(), "y2": y - l * a.sin()})) @@ -39,8 +45,13 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { ), }; // On the active plane while the perspective grid shows, sized in plane units from the click. - match vectorcraft_engine::perspective_click(&app.session, id, ¶ms, vectorcraft_geom::Point::new(x, y)) { + let options = (d.kind == "rectangularGrid").then(|| json!({"rows": params["rows"], "columns": params["columns"]})); + let result = match vectorcraft_engine::perspective_click(&app.session, id, ¶ms, vectorcraft_geom::Point::new(x, y)) { Some((id, params)) => run_and_close(app, &id, params), None => run_and_close(app, id, params), + }?; + if let Some(options) = options { + app.run("tool.setOption", json!({"tool": "rectangularGrid", "values": options}))?; } + Ok(result) } diff --git a/crates/ui-egui/src/dialogs/tests.rs b/crates/ui-egui/src/dialogs/tests.rs index 9b0df938..7885236c 100644 --- a/crates/ui-egui/src/dialogs/tests.rs +++ b/crates/ui-egui/src/dialogs/tests.rs @@ -40,6 +40,7 @@ fn kinds_map_to_their_dialogs() { ("newDocument", DialogKind::NewDocument), ("roundedRectangle", DialogKind::Shape), ("lineSegment", DialogKind::Shape), + ("rectangularGrid", DialogKind::Shape), ("shear", DialogKind::Transform), ("splitIntoGrid", DialogKind::PathOp), ("artboardOptions", DialogKind::ArtboardOptions), @@ -78,9 +79,20 @@ fn every_dialog_draws() { app.ui.dialog = None; let _ = app.session.cancel_interaction(); } - for kind in - ["rectangle", "roundedRectangle", "ellipse", "polygon", "star", "lineSegment", "rotate", "reflect", "scale", "shear", "artboardOptions"] - { + for kind in [ + "rectangle", + "roundedRectangle", + "ellipse", + "polygon", + "star", + "lineSegment", + "rectangularGrid", + "rotate", + "reflect", + "scale", + "shear", + "artboardOptions", + ] { open_tool_dialog(&mut app, kind, json!({"x": 5, "y": 5, "index": 0})); assert!(app.ui.dialog.as_ref().is_some_and(|d| d.kind == kind), "{kind}"); frame(&mut app, Default::default()); diff --git a/crates/ui-egui/src/dialogs/tests_rectangular_grid.rs b/crates/ui-egui/src/dialogs/tests_rectangular_grid.rs new file mode 100644 index 00000000..ae6258d9 --- /dev/null +++ b/crates/ui-egui/src/dialogs/tests_rectangular_grid.rs @@ -0,0 +1,313 @@ +//! Rectangular Grid click-to-size flow, from canvas dispatch through the registered dialog. +//! All geometry here is original, synthetic test data. + +use std::sync::Arc; + +use serde_json::{Value, json}; +use vectorcraft_doc::{Document, NodeId, NodeKind}; +use vectorcraft_engine::{Session, ViewInfo}; +use vectorcraft_geom::{PathData, Point, Rect}; +use vectorcraft_tools::distort::perspective::{PerspectiveGrid, Plane}; +use vectorcraft_tools::{PointerEvent, PointerKind}; + +use crate::{VectorcraftApp, theme}; + +const KIND: &str = "rectangularGrid"; + +fn app() -> VectorcraftApp { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 800, "height": 600})).unwrap(); + app.select_tool(KIND); + // Pointer-up persists tool settings even for a click. Seed them so opening/cancelling can + // be compared with the complete pre-click preferences, including an unchanged store. + saved_counts(&mut app, 5, 5); + app +} + +fn saved_counts(app: &mut VectorcraftApp, rows: u32, columns: u32) { + let values = json!({"rows": rows, "columns": columns}); + app.session.set_tool_options(Some(KIND), values.as_object().unwrap()); +} + +fn view() -> ViewInfo { + ViewInfo { smart_guides: false, snap_to_point: false, snap_to_grid: false, snap_to_pixel: false, ..Default::default() } +} + +fn pointer(app: &mut VectorcraftApp, kind: PointerKind, p: Point) { + crate::canvas::dispatch(app, &PointerEvent::new(kind, p.x, p.y), view()); +} + +fn click(app: &mut VectorcraftApp, at: Point) { + app.select_tool(KIND); + pointer(app, PointerKind::Down, at); + assert!(app.ui.dialog.is_none(), "the dialog opens on release"); + pointer(app, PointerKind::Up, at); + assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(KIND)); + assert!(!app.session.tool_busy()); + assert!(!app.session.in_interaction()); +} + +fn field(app: &VectorcraftApp, key: &str) -> Value { + app.ui.dialog.as_ref().unwrap().fields.get(key).cloned().unwrap_or_default() +} + +fn set(app: &mut VectorcraftApp, key: &str, value: Value) { + app.ui.dialog.as_mut().unwrap().fields.insert(key.into(), value); +} + +fn values(app: &mut VectorcraftApp, width: Value, height: Value, rows: Value, columns: Value) { + for (key, value) in [("width", width), ("height", height), ("rows", rows), ("columns", columns)] { + set(app, key, value); + } +} + +fn confirm(app: &mut VectorcraftApp) -> NodeId { + let result = super::confirm(app).unwrap(); + assert!(app.ui.dialog.is_none()); + NodeId(result["id"].as_u64().expect("confirmation returns the created group")) +} + +fn paths(app: &VectorcraftApp, id: NodeId) -> Vec { + let doc = &app.session.doc().unwrap().doc; + let group = doc.node(id).unwrap(); + assert!(matches!(group.kind, NodeKind::Group { .. })); + group.children().unwrap().iter().map(|n| n.path_data().expect("every grid child is an editable path").clone()).collect() +} + +fn assert_path(path: &PathData, expected: &[Point], closed: bool) { + assert_eq!(path.subpaths.len(), 1); + let subpath = &path.subpaths[0]; + assert_eq!(subpath.closed, closed); + assert_eq!(subpath.anchors.len(), expected.len()); + for (anchor, expected) in subpath.anchors.iter().zip(expected) { + assert!(anchor.p.distance(*expected) < 1e-7, "{:?} != {expected:?}", anchor.p); + assert!(anchor.h_in.distance(anchor.p) < 1e-7 && anchor.h_out.distance(anchor.p) < 1e-7, "grid edges stay straight"); + } +} + +/// Independent coordinate oracle: counts mean interior dividers, plus one closed outer frame. +fn assert_grid(app: &VectorcraftApp, id: NodeId, bounds: Rect, rows: u32, columns: u32) { + let paths = paths(app, id); + assert_eq!(paths.len(), (rows + columns + 1) as usize); + let mut remaining = paths.iter(); + for i in 1..=rows { + let y = bounds.y0 + bounds.height() * f64::from(i) / f64::from(rows + 1); + assert_path(remaining.next().unwrap(), &[Point::new(bounds.x0, y), Point::new(bounds.x1, y)], false); + } + for i in 1..=columns { + let x = bounds.x0 + bounds.width() * f64::from(i) / f64::from(columns + 1); + assert_path(remaining.next().unwrap(), &[Point::new(x, bounds.y0), Point::new(x, bounds.y1)], false); + } + assert_path( + remaining.next().unwrap(), + &[Point::new(bounds.x0, bounds.y0), Point::new(bounds.x1, bounds.y0), Point::new(bounds.x1, bounds.y1), Point::new(bounds.x0, bounds.y1)], + true, + ); + assert!(remaining.next().is_none()); +} + +struct Snapshot { + doc: Arc, + selected: Vec, + prefs: Value, + options: Value, + undo: usize, + redo: usize, + journal: Vec<(String, Value)>, + dirty: bool, +} + +impl Snapshot { + fn take(app: &VectorcraftApp) -> Self { + let state = app.session.doc().unwrap(); + Self { + doc: state.doc.clone(), + selected: state.selection.objects.clone(), + prefs: app.session.prefs.to_json(), + options: app.session.tool_options_of(KIND), + undo: state.history.undo.len(), + redo: state.history.redo.len(), + journal: app.session.journal.clone(), + dirty: state.is_dirty(), + } + } + + fn assert_unchanged(&self, app: &VectorcraftApp) { + let state = app.session.doc().unwrap(); + assert_eq!(state.doc, self.doc, "the document, including allocated ids, stays unchanged"); + assert_eq!(state.selection.objects, self.selected); + assert_eq!(app.session.prefs.to_json(), self.prefs); + assert_eq!(app.session.tool_options_of(KIND), self.options); + assert_eq!((state.history.undo.len(), state.history.redo.len()), (self.undo, self.redo)); + assert_eq!(app.session.journal, self.journal); + assert_eq!(state.is_dirty(), self.dirty); + assert!(!app.session.in_interaction()); + } +} + +fn key_frame(app: &mut VectorcraftApp, key: egui::Key) { + let ctx = egui::Context::default(); + theme::install_fonts(&ctx); + theme::apply(&ctx, Default::default()); + let event = egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }; + let input = egui::RawInput { events: vec![event], ..Default::default() }; + let mut out = ctx.run_ui(input, |ui| { + crate::shortcuts::handle(app, ui.ctx()); + super::show(app, ui.ctx()); + }); + out.textures_delta.clear(); +} + +#[test] +fn rectangular_grid_canvas_click_opens_registered_dialog_with_saved_counts_and_point_defaults() { + let mut app = app(); + saved_counts(&mut app, 7, 3); + app.run("document.setUnits", json!({"units": "mm"})).unwrap(); + let before = Snapshot::take(&app); + click(&mut app, Point::new(137.25, 86.5)); + before.assert_unchanged(&app); + assert!(super::DialogKind::of(KIND).is_some(), "must not fall through to the no-op form spec"); + assert_eq!((field(&app, "x"), field(&app, "y")), (json!(137.25), json!(86.5))); + assert_eq!((field(&app, "width"), field(&app, "height")), (json!("100 pt"), json!("100 pt"))); + assert_eq!((field(&app, "rows"), field(&app, "columns")), (json!(7), json!(3))); + let text = crate::tests_labels::painted_text(&mut app, |app, ui| super::show(app, ui.ctx())); + let labels = ["Width:", "Height:", "Horizontal Dividers:", "Vertical Dividers:"]; + let positions: Vec<_> = labels.iter().map(|label| text.find(label).expect("a labelled field")).collect(); + assert!(positions.windows(2).all(|pair| pair[0] < pair[1]), "{text}"); + let id = confirm(&mut app); + assert_grid(&app, id, Rect::new(137.25, 86.5, 237.25, 186.5), 7, 3); +} + +#[test] +fn rectangular_grid_creates_exact_dividers_as_one_undo_redo_step() { + let mut app = app(); + app.run("shape.rectangle", json!({"x": 12, "y": 18, "width": 9, "height": 11})).unwrap(); + let before = Snapshot::take(&app); + click(&mut app, Point::new(125.0, 75.0)); + values(&mut app, json!(120), json!(60), json!(2), json!(3)); + let id = confirm(&mut app); + assert_grid(&app, id, Rect::new(125.0, 75.0, 245.0, 135.0), 2, 3); + assert_eq!(app.session.doc().unwrap().selection.objects, vec![id]); + assert_eq!(app.session.doc().unwrap().history.undo.len(), before.undo + 1); + assert_eq!(app.session.journal.last().unwrap().0, "shape.rectangularGrid"); + assert_eq!(app.session.journal.len(), before.journal.len() + 1); + let created = app.session.doc().unwrap().doc.clone(); + app.run("edit.undo", json!({})).unwrap(); + assert_eq!(app.session.doc().unwrap().doc, before.doc); + assert_eq!(app.session.doc().unwrap().selection.objects, before.selected); + assert_eq!(app.session.doc().unwrap().history.undo.len(), before.undo); + assert_eq!(app.session.tool_options_of(KIND), json!({"rows": 2, "columns": 3}), "undo does not undo the saved tool settings"); + app.run("edit.redo", json!({})).unwrap(); + assert_eq!(app.session.doc().unwrap().doc, created); + assert_grid(&app, id, Rect::new(125.0, 75.0, 245.0, 135.0), 2, 3); + assert_eq!(app.session.doc().unwrap().history.undo.len(), before.undo + 1); +} + +#[test] +fn rectangular_grid_uses_shared_length_parsing_and_numeric_points() { + for (unit, width, height, expected_width, expected_height) in [ + ("mm", json!("25.4 mm"), json!("12.7 mm"), 72.0, 36.0), + ("mm", json!(72), json!(36), 72.0, 36.0), + ("inches", json!("2 cm"), json!("18 pt"), 144.0 / 2.54, 18.0), + ("mm", json!("1 in + 18 pt"), json!("2p6"), 90.0, 30.0), + ] { + let mut app = app(); + app.run("document.setUnits", json!({"units": unit})).unwrap(); + click(&mut app, Point::new(-13.5, 29.25)); + values(&mut app, width, height, json!("2"), json!("3")); + let id = confirm(&mut app); + assert_grid(&app, id, Rect::new(-13.5, 29.25, -13.5 + expected_width, 29.25 + expected_height), 2, 3); + } +} + +#[test] +fn rectangular_grid_cancel_and_escape_discard_edits_and_reopen_from_saved_values() { + for escape in [false, true] { + let mut app = app(); + saved_counts(&mut app, 2, 8); + // A pending redo must survive opening, editing and cancelling the size dialog. + app.run("shape.rectangle", json!({"x": 5, "y": 6, "width": 7, "height": 8})).unwrap(); + app.run("edit.undo", json!({})).unwrap(); + let before = Snapshot::take(&app); + click(&mut app, Point::new(111.0, 147.0)); + values(&mut app, json!("18 mm"), json!("nonsense"), json!(9), json!(1)); + if escape { + key_frame(&mut app, egui::Key::Escape); + } else { + super::cancel(&mut app); + } + assert!(app.ui.dialog.is_none()); + before.assert_unchanged(&app); + click(&mut app, Point::new(209.0, 173.0)); + assert_eq!((field(&app, "x"), field(&app, "y")), (json!(209.0), json!(173.0))); + assert_eq!((field(&app, "width"), field(&app, "height")), (json!("100 pt"), json!("100 pt"))); + assert_eq!((field(&app, "rows"), field(&app, "columns")), (json!(2), json!(8))); + super::cancel(&mut app); + before.assert_unchanged(&app); + } +} + +#[test] +fn rectangular_grid_enter_confirms_through_the_shared_modal_frame() { + let mut app = app(); + click(&mut app, Point::new(121.0, 143.0)); + values(&mut app, json!("84 pt"), json!("56 pt"), json!(2), json!(3)); + key_frame(&mut app, egui::Key::Enter); + assert!(app.ui.dialog.is_none()); + let selected = &app.session.doc().unwrap().selection.objects; + assert_eq!(selected.len(), 1); + assert_grid(&app, selected[0], Rect::new(121.0, 143.0, 205.0, 199.0), 2, 3); +} + +#[test] +fn rectangular_grid_perspective_click_projects_each_anchor_and_is_one_undo_step() { + let mut app = app(); + app.run("perspective.grid.preset", json!({"kind": 2})).unwrap(); + app.run("perspective.grid.set", json!({"snap": false})).unwrap(); + app.run("perspective.plane.set", json!({"plane": "right"})).unwrap(); + let grid = PerspectiveGrid::effective(&app.session.doc().unwrap().doc); + let at = grid.to_page(Plane::Right, Point::new(65.0, 85.0)).unwrap(); + let map = grid.size_homography(Plane::Right, at, false).unwrap(); + let before = Snapshot::take(&app); + click(&mut app, at); + values(&mut app, json!(96), json!(54), json!(1), json!(2)); + let id = confirm(&mut app); + let expected = [ + vec![Point::new(at.x, at.y + 27.0), Point::new(at.x + 96.0, at.y + 27.0)], + vec![Point::new(at.x + 32.0, at.y), Point::new(at.x + 32.0, at.y + 54.0)], + vec![Point::new(at.x + 64.0, at.y), Point::new(at.x + 64.0, at.y + 54.0)], + vec![at, Point::new(at.x + 96.0, at.y), Point::new(at.x + 96.0, at.y + 54.0), Point::new(at.x, at.y + 54.0)], + ]; + let actual = paths(&app, id); + assert_eq!(actual.len(), expected.len()); + for (index, (path, points)) in actual.iter().zip(expected).enumerate() { + let projected: Vec = points.into_iter().map(|p| map.apply(p).unwrap()).collect(); + assert_path(path, &projected, index == 3); + } + let state = app.session.doc().unwrap(); + assert_eq!(PerspectiveGrid::effective(&state.doc).attachment_of(&state.doc, id), Some((Plane::Right, 0.0))); + assert_eq!(state.history.undo.len(), before.undo + 1); + let (command, params) = app.session.journal.last().unwrap(); + assert_eq!(command, "perspective.draw"); + assert_eq!(params["command"], json!("shape.rectangularGrid")); + assert_eq!(app.session.journal.len(), before.journal.len() + 1); + let created = state.doc.clone(); + app.run("edit.undo", json!({})).unwrap(); + assert_eq!(app.session.doc().unwrap().doc, before.doc); + app.run("edit.redo", json!({})).unwrap(); + assert_eq!(app.session.doc().unwrap().doc, created); +} + +#[test] +fn rectangular_grid_failed_creation_does_not_save_counts() { + let mut app = app(); + saved_counts(&mut app, 2, 3); + let layer = app.session.doc().unwrap().doc.layers[0].id; + app.run("layer.setProps", json!({"ids": [layer.0], "locked": true})).unwrap(); + let before = Snapshot::take(&app); + click(&mut app, Point::new(30.0, 40.0)); + values(&mut app, json!(120), json!(60), json!(7), json!(8)); + assert!(super::confirm(&mut app).is_err()); + assert_eq!(app.session.tool_options_of(KIND), before.options); + assert_eq!(app.session.doc().unwrap().history.undo.len(), before.undo); +} diff --git a/crates/ui-egui/src/dialogs/tools.rs b/crates/ui-egui/src/dialogs/tools.rs index d62aef85..b78cdb8f 100644 --- a/crates/ui-egui/src/dialogs/tools.rs +++ b/crates/ui-egui/src/dialogs/tools.rs @@ -16,6 +16,10 @@ pub fn open_tool_dialog(app: &mut VectorcraftApp, kind: &str, p: Value) { "polygon" => Dialog::new(kind, json!({"x": x, "y": y, "radius": "50 pt", "sides": 6})), "star" => Dialog::new(kind, json!({"x": x, "y": y, "radius1": "50 pt", "radius2": "25 pt", "points": 5})), "lineSegment" => Dialog::new(kind, json!({"x": x, "y": y, "length": "100 pt", "angle": 0})), + "rectangularGrid" => { + let options = app.session.tool_options_of(kind); + Dialog::new(kind, json!({"x": x, "y": y, "width": "100 pt", "height": "100 pt", "rows": options["rows"], "columns": options["columns"]})) + } // Graph tool click: the graph's size. "graph" => Dialog::new( "command", diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index b488bbfa..99ddd848 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -2626,6 +2626,8 @@ theme Appearance Motiv Polar Grid Polární mřížka Polygon Mnohoúhelník Rectangular Grid Obdélníková mřížka + Horizontal Dividers: Vodorovné dělicí čáry: + Vertical Dividers: Svislé dělicí čáry: Rounded Rectangle Zaoblený obdélník Spiral Spirála Export CSS Exportovat CSS diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index fe93142d..292d3a3c 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -2097,6 +2097,9 @@ Rectangle: Sunshine Rechteck: Sonnenschein Rectangle… Rechteck… Rectangular Grid Rechteckiges Raster +# Rectangular Grid click dialog. + Horizontal Dividers: Horizontale Trennlinien: + Vertical Dividers: Vertikale Trennlinien: Rectangular Grid Tool Rechteckiges-Raster-Werkzeug Red Rot Redefine Character Style Zeichenformat neu definieren diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 7167403d..0872d912 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -2148,6 +2148,9 @@ Rectangle: Sunshine Rectángulo: Sol Rectangle… Rectángulo… Rectangular Grid Cuadrícula rectangular +# Rectangular Grid click dialog. + Horizontal Dividers: Líneas divisorias horizontales: + Vertical Dividers: Líneas divisorias verticales: Rectangular Grid Tool Herramienta Cuadrícula rectangular Red Rojo Redefine Character Style Redefinir estilo de carácter diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 127a4e9c..91a10396 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -2145,6 +2145,9 @@ Rectangle: Sunshine Rectangle : soleil Rectangle… Rectangle… Rectangular Grid Grille rectangulaire +# Rectangular Grid click dialog. + Horizontal Dividers: Séparations horizontales : + Vertical Dividers: Séparations verticales : Rectangular Grid Tool Outil Grille rectangulaire Red Rouge Redefine Character Style Redéfinir le style de caractère diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 4ed4c7c3..0fa97231 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -2148,6 +2148,9 @@ Rectangle: Sunshine Rettangolo: Sole Rectangle… Rettangolo… Rectangular Grid Griglia rettangolare +# Rectangular Grid click dialog. + Horizontal Dividers: Divisori orizzontali: + Vertical Dividers: Divisori verticali: Rectangular Grid Tool Strumento Griglia rettangolare Red Rosso Redefine Character Style Ridefinisci stile carattere diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 144db545..360c07a1 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -2151,6 +2151,9 @@ Rectangle Properties: 長方形のプロパティ: Rectangle Tool 長方形ツール Rectangular Grid 長方形グリッド +# Rectangular Grid click dialog. + Horizontal Dividers: 水平方向の分割線: + Vertical Dividers: 垂直方向の分割線: Rectangular Grid Tool 長方形グリッドツール Red レッド Redefine Character Style 文字スタイルを再定義 diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index b473c5fb..3cf25440 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -2144,6 +2144,9 @@ Rectangle: Sunshine Прямоугольник: Солнце Rectangle… Прямоугольник… Rectangular Grid Прямоугольная сетка +# Rectangular Grid click dialog. + Horizontal Dividers: Горизонтальные разделители: + Vertical Dividers: Вертикальные разделители: Rectangular Grid Tool Инструмент «Прямоугольная сетка» Red Красный Redefine Character Style Переопределить стиль символа diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index b4281f29..c7dbdc7a 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -2140,6 +2140,9 @@ Rectangle: Sunshine Прямокутник: Сонце Rectangle… Прямокутник… Rectangular Grid Прямокутна сітка +# Rectangular Grid click dialog. + Horizontal Dividers: Горизонтальні розділювачі: + Vertical Dividers: Вертикальні розділювачі: Rectangular Grid Tool Інструмент «Прямокутна сітка» Red Червоний Redefine Character Style Перевизначити стиль символу diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 62fe742c..e28d35b2 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -2072,6 +2072,9 @@ Rectangle: Sunshine 矩形:阳光 Rectangle… 矩形… Rectangular Grid 矩形网格 +# Rectangular Grid click dialog. + Horizontal Dividers: 水平分隔线: + Vertical Dividers: 垂直分隔线: Rectangular Grid Tool 矩形网格工具 Red 红 Redefine Character Style 重新定义字符样式 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index bb296f3d..84bf158b 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2126,6 +2126,9 @@ Rectangle: Sunshine 矩形:陽光 Rectangle… 矩形… Rectangular Grid 矩形格線 +# Rectangular Grid click dialog. + Horizontal Dividers: 水平分隔線: + Vertical Dividers: 垂直分隔線: Rectangular Grid Tool 矩形格線工具 Red 紅色 Redefine Character Style 重新定義字元樣式