Merge pull request #1169 from FredApps/fix/rectangular-grid-click-dialog-validated-20261011

Fix Rectangular Grid click options dialog
This commit is contained in:
BFlat 2026-10-12 01:31:04 +07:00 committed by GitHub
commit 2ac3ba2b86
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 385 additions and 7 deletions

View File

@ -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);

View File

@ -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;

View File

@ -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<Value, String> {
"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<Value, String> {
),
};
// On the active plane while the perspective grid shows, sized in plane units from the click.
match vectorcraft_engine::perspective_click(&app.session, id, &params, 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, &params, 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)
}

View File

@ -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());

View File

@ -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<PathData> {
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<Document>,
selected: Vec<NodeId>,
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<Point> = 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);
}

View File

@ -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",

View File

@ -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

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -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

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -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

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -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

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -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

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -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 文字スタイルを再定義

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -2144,6 +2144,9 @@
Rectangle: Sunshine Прямоугольник: Солнце
Rectangle… Прямоугольник…
Rectangular Grid Прямоугольная сетка
# Rectangular Grid click dialog.
Horizontal Dividers: Горизонтальные разделители:
Vertical Dividers: Вертикальные разделители:
Rectangular Grid Tool Инструмент «Прямоугольная сетка»
Red Красный
Redefine Character Style Переопределить стиль символа

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -2140,6 +2140,9 @@
Rectangle: Sunshine Прямокутник: Сонце
Rectangle… Прямокутник…
Rectangular Grid Прямокутна сітка
# Rectangular Grid click dialog.
Horizontal Dividers: Горизонтальні розділювачі:
Vertical Dividers: Вертикальні розділювачі:
Rectangular Grid Tool Інструмент «Прямокутна сітка»
Red Червоний
Redefine Character Style Перевизначити стиль символу

Can't render this file because it has a wrong number of fields in line 13.

View File

@ -2072,6 +2072,9 @@
Rectangle: Sunshine 矩形:阳光
Rectangle… 矩形…
Rectangular Grid 矩形网格
# Rectangular Grid click dialog.
Horizontal Dividers: 水平分隔线:
Vertical Dividers: 垂直分隔线:
Rectangular Grid Tool 矩形网格工具
Red 红
Redefine Character Style 重新定义字符样式

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -2126,6 +2126,9 @@
Rectangle: Sunshine 矩形:陽光
Rectangle… 矩形…
Rectangular Grid 矩形格線
# Rectangular Grid click dialog.
Horizontal Dividers: 水平分隔線:
Vertical Dividers: 垂直分隔線:
Rectangular Grid Tool 矩形格線工具
Red 紅色
Redefine Character Style 重新定義字元樣式

Can't render this file because it contains an unexpected character in line 6 and column 47.