Add a Polar Grid canvas-click dialog

A click with the Polar Grid tool opens the shared shape dialog (width, height, concentric and
radial dividers), as the Rectangular Grid tool does, and keeps the divider counts as the tool's
options.
This commit is contained in:
FredApps 2026-10-12 14:22:02 +07:00 committed by bflatastic
parent 3c3c249bc0
commit 1dc61af4d1
16 changed files with 113 additions and 4 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" | "rectangularGrid" | "artboardOptions" => &["width", "height"],
"rectangle" | "ellipse" | "rectangularGrid" | "polarGrid" | "artboardOptions" => &["width", "height"],
"roundedRectangle" => &["width", "height", "radius"],
"polygon" => &["radius"],
"star" => &["radius1", "radius2"],
@ -68,6 +68,7 @@ fn order(kind: &str) -> &'static [&'static str] {
"offsetPath" => &["offset", "joins", "miterLimit"],
"splitIntoGrid" => &["rows", "columns", "gutter"],
"rectangularGrid" => &["width", "height", "rows", "columns"],
"polarGrid" => &["width", "height", "concentric", "radial"],
"artboardOptions" => &["name", "width", "height"],
_ => &[],
}
@ -182,6 +183,8 @@ pub(super) fn grid(ui: &mut egui::Ui, d: &mut Dialog, unit: Unit) {
let label = match (d.kind.as_str(), k.as_str()) {
("rectangularGrid", "rows") => tl!("Horizontal Dividers:").into(),
("rectangularGrid", "columns") => tl!("Vertical Dividers:").into(),
("polarGrid", "concentric") => tl!("Concentric Dividers:").into(),
("polarGrid", "radial") => tl!("Radial Dividers:").into(),
_ => humanized(&k),
};
field(ui, d, &k, &label);

View File

@ -209,7 +209,7 @@ macro_rules! registry {
registry! {
NewDocument: [new_document::KIND] => new_document::SPEC,
Shape: ["rectangle", "roundedRectangle", "ellipse", "polygon", "star", "lineSegment", "rectangularGrid"] => shapes::SPEC,
Shape: ["rectangle", "roundedRectangle", "ellipse", "polygon", "star", "lineSegment", "rectangularGrid", "polarGrid"] => shapes::SPEC,
Transform: ["move", "rotate", "scale", "reflect", "shear"] => transform::SPEC,
PathOp: ["average", "offsetPath", "simplify", "splitIntoGrid"] => path_ops::SPEC,
DocumentSetup: ["documentSetup"] => document_setup::SPEC,

View File

@ -17,6 +17,7 @@ fn title(kind: &str) -> &'static str {
"polygon" => tl!("Polygon"),
"star" => tl!("Star"),
"rectangularGrid" => tl!("Rectangular Grid"),
"polarGrid" => tl!("Polar Grid"),
_ => tl!("Line Segment Tool Options"),
}
}
@ -35,6 +36,11 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
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)}),
),
"polarGrid" => (
"shape.polarGrid",
json!({"x": x, "y": y, "width": d.f64("width", 100.0), "height": d.f64("height", 100.0),
"concentric": (d.f64("concentric", 5.0) as u64).min(999), "radial": (d.f64("radial", 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()}))
@ -45,13 +51,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.
let options = (d.kind == "rectangularGrid").then(|| json!({"rows": params["rows"], "columns": params["columns"]}));
let options = matches!(d.kind.as_str(), "rectangularGrid" | "polarGrid").then(|| params.clone());
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}))?;
app.run("tool.setOption", json!({"tool": d.kind, "values": options}))?;
}
Ok(result)
}

View File

@ -41,6 +41,7 @@ fn kinds_map_to_their_dialogs() {
("roundedRectangle", DialogKind::Shape),
("lineSegment", DialogKind::Shape),
("rectangularGrid", DialogKind::Shape),
("polarGrid", DialogKind::Shape),
("shear", DialogKind::Transform),
("splitIntoGrid", DialogKind::PathOp),
("artboardOptions", DialogKind::ArtboardOptions),
@ -87,6 +88,7 @@ fn every_dialog_draws() {
"star",
"lineSegment",
"rectangularGrid",
"polarGrid",
"rotate",
"reflect",
"scale",
@ -650,3 +652,72 @@ fn zig_zag_has_sliders_and_radio_buttons() {
}
assert!(!lines.contains(&"Ridges:"), "{lines:?}");
}
#[test]
fn polar_grid_shared_canvas_click_defaults_units_counts_and_history() {
use vectorcraft_doc::NodeId;
use vectorcraft_tools::{PointerEvent, PointerKind};
let mut app = app();
app.select_tool("polarGrid");
app.run("tool.setOption", json!({"tool": "polarGrid", "values": {"concentric": 2, "radial": 3}})).unwrap();
let before = app.session.doc().unwrap().doc.clone();
let undo = app.session.doc().unwrap().history.undo.len();
for kind in [PointerKind::Down, PointerKind::Up] {
crate::canvas::dispatch(&mut app, &PointerEvent::new(kind, 30.0, 40.0), Default::default());
}
assert_eq!(DialogKind::of("polarGrid"), Some(DialogKind::Shape));
let d = app.ui.dialog.as_mut().expect("canvas click opens the shared shape form");
assert_eq!(d.fields["width"], json!("100 pt"));
assert_eq!(d.fields["height"], json!("100 pt"));
assert_eq!((d.fields["concentric"].clone(), d.fields["radial"].clone()), (json!(2), json!(3)));
d.fields.insert("width".into(), json!("25.4 mm"));
d.fields.insert("height".into(), json!("0.5 in"));
frame(&mut app, Default::default());
let result = confirm(&mut app).unwrap();
assert!(app.ui.dialog.is_none());
let s = app.session.doc().unwrap();
let group = s.doc.node(NodeId(result["id"].as_u64().unwrap())).unwrap();
let children = group.children().unwrap();
assert_eq!(children.len(), 6);
assert_eq!(children.iter().filter(|n| n.path_data().unwrap().is_closed()).count(), 3);
assert_eq!(s.history.undo.len(), undo + 1);
let made = s.doc.clone();
app.run("edit.undo", json!({})).unwrap();
assert_eq!(app.session.doc().unwrap().doc, before);
app.run("edit.redo", json!({})).unwrap();
assert_eq!(app.session.doc().unwrap().doc, made);
}
#[test]
fn polar_grid_shared_cancel_reopen_zero_counts_and_failed_creation() {
let mut app = app();
app.run("tool.setOption", json!({"tool": "polarGrid", "values": {"concentric": 2, "radial": 3}})).unwrap();
let before = app.session.doc().unwrap().doc.clone();
open_tool_dialog(&mut app, "polarGrid", json!({"x": 10, "y": 20}));
let d = app.ui.dialog.as_mut().expect("shared Polar Grid form opens");
d.fields.insert("concentric".into(), json!(8));
cancel(&mut app);
assert_eq!(app.session.doc().unwrap().doc, before);
open_tool_dialog(&mut app, "polarGrid", json!({"x": 10, "y": 20}));
assert_eq!(app.ui.dialog.as_ref().unwrap().fields["concentric"], json!(2));
for key in ["concentric", "radial"] {
app.ui.dialog.as_mut().unwrap().fields.insert(key.into(), json!(0));
}
let result = confirm(&mut app).unwrap();
let group = app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(result["id"].as_u64().unwrap())).unwrap();
assert_eq!(group.children().unwrap().len(), 1);
assert!(group.children().unwrap()[0].path_data().unwrap().is_closed());
open_tool_dialog(&mut app, "polarGrid", json!({"x": 10, "y": 20}));
assert_eq!(app.ui.dialog.as_ref().unwrap().fields["radial"], json!(0));
cancel(&mut app);
let layer = app.session.doc().unwrap().doc.layers[0].id;
app.run("layer.setProps", json!({"ids": [layer.0], "locked": true})).unwrap();
let options = app.session.tool_options_of("polarGrid");
let before = app.session.doc().unwrap().doc.clone();
open_tool_dialog(&mut app, "polarGrid", json!({"x": 10, "y": 20}));
app.ui.dialog.as_mut().unwrap().fields.insert("radial".into(), json!(9));
assert!(confirm(&mut app).is_err());
assert!(app.ui.dialog.is_none());
assert_eq!(app.session.tool_options_of("polarGrid"), options);
assert_eq!(app.session.doc().unwrap().doc, before);
}

View File

@ -20,6 +20,13 @@ pub fn open_tool_dialog(app: &mut VectorcraftApp, kind: &str, p: Value) {
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"]}))
}
"polarGrid" => {
let options = app.session.tool_options_of(kind);
Dialog::new(
kind,
json!({"x": x, "y": y, "width": "100 pt", "height": "100 pt", "concentric": options["concentric"], "radial": options["radial"]}),
)
}
// Graph tool click: the graph's size.
"graph" => Dialog::new(
"command",

View File

@ -4498,3 +4498,5 @@ mark-style Japanese Japonské
@msg the compressed file unpacks to more than VectorCraft reads ({_1} MB of document, {_2} MB per image) komprimovaný soubor se rozbalí na více, než VectorCraft přečte ({_1} MB dokumentu, {_2} MB na obrázek)
@msg the document is too large to save compressed ({0}): save it with Use Compression off dokument je příliš velký pro komprimované uložení ({0}): uložte ho s vypnutou volbou Použít kompresi
@msg the file stopped being read (please report this bug) čtení souboru se přerušilo (nahlaste prosím tuto chybu)
Concentric Dividers: Soustředné dělicí čáry:
Radial Dividers: Radiální dělicí čáry:

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

View File

@ -4149,3 +4149,5 @@ theme Appearance Erscheinungsbild
@msg the compressed file unpacks to more than VectorCraft reads ({_1} MB of document, {_2} MB per image) die komprimierte Datei entpackt sich auf mehr, als VectorCraft liest ({_1} MB Dokument, {_2} MB pro Bild)
@msg the document is too large to save compressed ({0}): save it with Use Compression off das Dokument ist zu groß, um komprimiert gespeichert zu werden ({0}): speichern Sie es mit deaktivierter Option „Komprimierung verwenden“
@msg the file stopped being read (please report this bug) das Lesen der Datei wurde abgebrochen (bitte melden Sie diesen Fehler)
Concentric Dividers: Konzentrische Trennlinien:
Radial Dividers: Radiale Trennlinien:

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

View File

@ -4192,3 +4192,5 @@ lighting Light Luz
@msg the compressed file unpacks to more than VectorCraft reads ({_1} MB of document, {_2} MB per image) el archivo comprimido ocupa al descomprimirse más de lo que VectorCraft lee ({_1} MB de documento, {_2} MB por imagen)
@msg the document is too large to save compressed ({0}): save it with Use Compression off el documento es demasiado grande para guardarlo comprimido ({0}): guárdelo con Usar compresión desactivado
@msg the file stopped being read (please report this bug) se dejó de leer el archivo (informe de este error)
Concentric Dividers: Divisiones concéntricas:
Radial Dividers: Divisiones radiales:

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

View File

@ -4202,3 +4202,5 @@ lighting Light Lumière
@msg the compressed file unpacks to more than VectorCraft reads ({_1} MB of document, {_2} MB per image) le fichier compressé dépasse une fois décompressé ce que VectorCraft lit ({_1} Mo de document, {_2} Mo par image)
@msg the document is too large to save compressed ({0}): save it with Use Compression off le document est trop volumineux pour être enregistré compressé ({0}) : enregistrez-le avec Utiliser la compression désactivé
@msg the file stopped being read (please report this bug) la lecture du fichier s’est interrompue (veuillez signaler ce bogue)
Concentric Dividers: Divisions concentriques :
Radial Dividers: Divisions radiales :

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

View File

@ -4194,3 +4194,5 @@ lighting Light Luce
@msg the compressed file unpacks to more than VectorCraft reads ({_1} MB of document, {_2} MB per image) il file compresso, una volta decompresso, supera ciò che VectorCraft legge ({_1} MB di documento, {_2} MB per immagine)
@msg the document is too large to save compressed ({0}): save it with Use Compression off il documento è troppo grande per essere salvato compresso ({0}): salvalo con Usa compressione disattivato
@msg the file stopped being read (please report this bug) la lettura del file si è interrotta (segnala questo errore)
Concentric Dividers: Divisori concentrici:
Radial Dividers: Divisori radiali:

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

View File

@ -4188,3 +4188,5 @@ lighting Light ライト
@msg the compressed file unpacks to more than VectorCraft reads ({_1} MB of document, {_2} MB per image) 圧縮ファイルを展開すると VectorCraft が読み込める量を超えます (ドキュメント {_1} MB、画像 1 枚あたり {_2} MB)
@msg the document is too large to save compressed ({0}): save it with Use Compression off ドキュメントが大きすぎて圧縮保存できません ({0}): 「圧縮を使用」をオフにして保存してください
@msg the file stopped being read (please report this bug) ファイルの読み込みが中断されました (このバグを報告してください)
Concentric Dividers: 同心円の分割線:
Radial Dividers: 放射状の分割線:

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

View File

@ -3677,3 +3677,5 @@ axis Both Ambos
@msg the compressed file unpacks to more than VectorCraft reads ({_1} MB of document, {_2} MB per image) o arquivo compactado se expande para mais do que o VectorCraft lê ({_1} MB de documento, {_2} MB por imagem)
@msg the document is too large to save compressed ({0}): save it with Use Compression off o documento é grande demais para ser salvo compactado ({0}): salve-o com Usar Compressão desativado
@msg the file stopped being read (please report this bug) a leitura do arquivo foi interrompida (informe este erro)
Concentric Dividers: Divisórias concêntricas:
Radial Dividers: Divisórias radiais:

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

View File

@ -4190,3 +4190,5 @@ lighting Light Свет
@msg the compressed file unpacks to more than VectorCraft reads ({_1} MB of document, {_2} MB per image) сжатый файл распаковывается в объём больше, чем читает VectorCraft ({_1} МБ документа, {_2} МБ на изображение)
@msg the document is too large to save compressed ({0}): save it with Use Compression off документ слишком велик для сохранения со сжатием ({0}): сохраните его с выключенным параметром «Использовать сжатие»
@msg the file stopped being read (please report this bug) чтение файла прервалось (пожалуйста, сообщите об этой ошибке)
Concentric Dividers: Концентрические разделители:
Radial Dividers: Радиальные разделители:

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

View File

@ -4174,3 +4174,5 @@ lighting Light Світло
@msg the compressed file unpacks to more than VectorCraft reads ({_1} MB of document, {_2} MB per image) стислий файл розпаковується в обсяг більший, ніж читає VectorCraft ({_1} МБ документа, {_2} МБ на зображення)
@msg the document is too large to save compressed ({0}): save it with Use Compression off документ завеликий для збереження зі стиском ({0}): збережіть його з вимкненим параметром «Використовувати стиск»
@msg the file stopped being read (please report this bug) читання файлу перервалося (будь ласка, повідомте про цю помилку)
Concentric Dividers: Концентричні розділювачі:
Radial Dividers: Радіальні розділювачі:

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

View File

@ -3786,3 +3786,5 @@ lighting Light 光源
Artistic 艺术效果
Film Grain… 胶片颗粒…
Highlight Area 高光区域
Concentric Dividers: 同心分隔线:
Radial Dividers: 径向分隔线:

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

View File

@ -3567,3 +3567,5 @@ lighting Light 光源
Artistic 藝術效果
Film Grain… 底片顆粒…
Highlight Area 亮部範圍
Concentric Dividers: 同心分隔線:
Radial Dividers: 徑向分隔線:

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