Properties palette: numeric fields take feet and inches, show lengths in the drawing's units, and report what they can't read (fixes #849) (#870)

Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-10-13 05:23:59 -04:00
1 parent 3b16f2d516
commit 5630529464
10 files changed
+148 -55

No files matched your search

+1
View File
@@ -1213,3 +1213,4 @@ menu Dimension Kótování
menu Modify Modifikace
Hatch Pattern Vzor šrafy
Search for pattern Hledat vzor
Not a number or distance: {text} Není číslo ani vzdálenost: {text}
1 # CADCraft Czech interface catalog — original translations by @Losminek (AI-assisted).
1213 menu
1214
1215
1216
+1
View File
@@ -1024,3 +1024,4 @@
Left device button: zoom extents. Right device button: calibrate. Linke Gerätetaste: auf Grenzen zoomen. Rechte Gerätetaste: kalibrieren.
Hatch Pattern Schraffurmuster
Search for pattern Muster suchen
Not a number or distance: {text} Keine Zahl und keine Entfernung: {text}
1 # CADCraft German interface catalog — original translations by the CADCraft contributors.
1024
1025
1026
1027
+1
View File
@@ -1239,3 +1239,4 @@
Left device button: zoom extents. Right device button: calibrate. Botón izquierdo del dispositivo: zoom a la extensión. Botón derecho: calibrar.
Hatch Pattern Patrón de sombreado
Search for pattern Buscar patrón
Not a number or distance: {text} No es un número ni una distancia: {text}
1 # CADCraft Spanish interface catalog — original translations.
1239
1240
1241
1242
+1
View File
@@ -1003,3 +1003,4 @@
Batch Publish... Publication par lots...
Hatch Pattern Motif de hachure
Search for pattern Rechercher un motif
Not a number or distance: {text} Ni un nombre ni une distance : {text}
1 # CADCraft French interface catalog — original translations by @pechnikov2026-star.
1003
1004
1005
1006
+1
View File
@@ -1240,3 +1240,4 @@
Hatch Pattern Padrão de hachura
Search for pattern Pesquisar padrão
Run Script... Executar script...
Not a number or distance: {text} Não é um número nem uma distância: {text}
1 # CADCraft Brazilian Portuguese (pt-BR) interface catalog.
1240
1241
1242
1243
+1
View File
@@ -1004,3 +1004,4 @@
Hatch Pattern Образец штриховки
Search for pattern Поиск образца
Sheet Books to PDF... Книги листов в PDF...
Not a number or distance: {text} Не число и не расстояние: {text}
1 # CADCraft Russian interface catalog — original translations by @pechnikov2026-star.
1004
1005
1006
1007
+1
View File
@@ -1276,3 +1276,4 @@
Ignore Ігнорувати
Origin: {x}, {y} Початок: {x}, {y}
Sheet Books to PDF... Книги аркушів у PDF...
Not a number or distance: {text} Не число й не відстань: {text}
1 # CADCraft Ukrainian interface catalog — original translations by @dmatviichuk.
1276
1277
1278
1279
+2
View File
@@ -1017,3 +1017,5 @@
Calibrate 校准
Move sideways to pan; push/pull or twist to zoom. 向侧面移动以平移;推拉或扭转以缩放。
Left device button: zoom extents. Right device button: calibrate. 左侧设备按钮:范围缩放。右侧设备按钮:校准。
Left device button: zoom extents. Right device button: calibrate. 左侧设备按钮:范围缩放。右侧设备按钮:校准。
Not a number or distance: {text} 不是数字或距离:{text}
1 # CADCraft Simplified Chinese interface catalog — original translations.
1017
1018
1019
1020
1021
+62 -55
View File
@@ -537,6 +537,24 @@ fn edit_field(ui: &mut egui::Ui, id: egui::Id, value: String) -> Option<String>
out
}
/// A committed numeric field read as the command line reads numbers (`36`, `3'`, `3'-6 1/2"`,
/// `6 1/2`). Text it can't read goes to `bad`, to be reported rather than dropped.
fn number(typed: Option<String>, bad: &mut Option<String>) -> Option<f64> {
let typed = typed?;
let v = cadcraft_engine::units::parse_distance(&typed);
if v.is_none() {
*bad = Some(typed);
}
v
}
/// Reports a numeric field the user typed that isn't a number; the field shows its value again.
fn report_bad(app: &mut CadApp, bad: Option<String>) {
if let Some(text) = bad {
app.session.echo(crate::tf!("Not a number or distance: {text}", text = text.trim()));
}
}
/// A drop-down of `items` showing `current`; returns the item clicked.
fn pick_menu(ui: &mut egui::Ui, current: &str, items: impl IntoIterator<Item = String>) -> Option<String> {
// Names are the drawing's own; only the ByLayer/ByBlock keywords are translated.
@@ -645,6 +663,10 @@ pub(crate) fn properties_section(app: &mut CadApp, ui: &mut egui::Ui) {
let Ok(d) = app.session.doc() else { return };
ui.add_space(4.0);
let h = &d.header;
// Lengths are shown as the drawing's units say (UNITS), and read back in any format.
let (lu, lp) = (h.i64("LUNITS", 2), h.i64("LUPREC", 4));
let len = |v: f64| cadcraft_engine::units::format_distance(v, lu, lp);
let mut bad: Option<String> = None;
if sel.is_empty() {
// Current properties for new objects; each row runs the command that sets it.
let layers: Vec<String> = d.layers.iter().map(|l| l.name.clone()).collect();
@@ -690,9 +712,7 @@ pub(crate) fn properties_section(app: &mut CadApp, ui: &mut egui::Ui) {
}
});
prop_row(ui, "Linetype scale", |ui| {
if let Some(v) = edit_field(ui, ui.id().with("cur_celtscale"), format!("{:.4}", h.f64("CELTSCALE", 1.0)))
&& let Ok(f) = v.trim().parse::<f64>()
{
if let Some(f) = number(edit_field(ui, ui.id().with("cur_celtscale"), format!("{:.4}", h.f64("CELTSCALE", 1.0))), &mut bad) {
act("setvar", json!({ "name": "CELTSCALE", "value": f }));
}
});
@@ -714,9 +734,7 @@ pub(crate) fn properties_section(app: &mut CadApp, ui: &mut egui::Ui) {
});
prop_row(ui, "Transparency", |ui| unavailable(ui, "ByLayer"));
prop_row(ui, "Thickness", |ui| {
if let Some(v) = edit_field(ui, ui.id().with("cur_thickness"), format!("{:.4}", h.f64("THICKNESS", 0.0)))
&& let Ok(f) = v.trim().parse::<f64>()
{
if let Some(f) = number(edit_field(ui, ui.id().with("cur_thickness"), len(h.f64("THICKNESS", 0.0))), &mut bad) {
act("setvar", json!({ "name": "THICKNESS", "value": f }));
}
});
@@ -742,9 +760,7 @@ pub(crate) fn properties_section(app: &mut CadApp, ui: &mut egui::Ui) {
});
prop_row(ui, "Annotation scale", |ui| unavailable(ui, "1:1"));
prop_row(ui, "Text height", |ui| {
if let Some(v) = edit_field(ui, ui.id().with("cur_textsize"), format!("{:.4}", h.f64("TEXTSIZE", 0.2)))
&& let Ok(f) = v.trim().parse::<f64>()
{
if let Some(f) = number(edit_field(ui, ui.id().with("cur_textsize"), len(h.f64("TEXTSIZE", 0.2))), &mut bad) {
act("setvar", json!({ "name": "TEXTSIZE", "value": f }));
}
});
@@ -782,6 +798,7 @@ pub(crate) fn properties_section(app: &mut CadApp, ui: &mut egui::Ui) {
if let Some((c, p)) = action {
let _ = app.run(c, p);
}
report_bad(app, bad);
return;
}
// Selected objects: common + geometry for a single object.
@@ -856,9 +873,7 @@ pub(crate) fn properties_section(app: &mut CadApp, ui: &mut egui::Ui) {
});
});
prop_row(ui, "Linetype scale", |ui| {
if let Some(v) = edit_field(ui, ui.id().with(("lts", &ids)), lts.clone())
&& let Ok(f) = v.trim().parse::<f64>()
{
if let Some(f) = number(edit_field(ui, ui.id().with(("lts", &ids)), lts.clone()), &mut bad) {
set = Some(json!({ "handles": ids, "ltscale": f }));
}
});
@@ -908,11 +923,10 @@ pub(crate) fn properties_section(app: &mut CadApp, ui: &mut egui::Ui) {
});
if sel.len() == 1 {
group(ui, "Geometry");
let num = |ui: &mut egui::Ui, label: &str, key: &str, v: f64, set: &mut Option<Value>, ids: &Vec<String>| {
let real = |v: f64| format!("{v:.4}");
let num = |ui: &mut egui::Ui, label: &str, key: &str, shown: String, set: &mut Option<Value>, bad: &mut Option<String>, ids: &Vec<String>| {
prop_row(ui, label, |ui| {
if let Some(s) = edit_field(ui, ui.id().with((key, ids)), format!("{v:.4}"))
&& let Ok(f) = s.trim().parse::<f64>()
{
if let Some(f) = number(edit_field(ui, ui.id().with((key, ids)), shown), bad) {
*set = Some(json!({ "handles": ids, key: f }));
}
});
@@ -920,47 +934,39 @@ pub(crate) fn properties_section(app: &mut CadApp, ui: &mut egui::Ui) {
match &e.kind {
EntityKind::Line(l) => {
prop_row(ui, "Start X", |ui| {
if let Some(s) = edit_field(ui, ui.id().with(("sx", &ids)), format!("{:.4}", l.a.x))
&& let Ok(f) = s.trim().parse::<f64>()
{
if let Some(f) = number(edit_field(ui, ui.id().with(("sx", &ids)), len(l.a.x)), &mut bad) {
set = Some(json!({ "handles": ids, "start": [f, l.a.y] }));
}
});
prop_row(ui, "Start Y", |ui| {
if let Some(s) = edit_field(ui, ui.id().with(("sy", &ids)), format!("{:.4}", l.a.y))
&& let Ok(f) = s.trim().parse::<f64>()
{
if let Some(f) = number(edit_field(ui, ui.id().with(("sy", &ids)), len(l.a.y)), &mut bad) {
set = Some(json!({ "handles": ids, "start": [l.a.x, f] }));
}
});
prop_row(ui, "End X", |ui| {
if let Some(s) = edit_field(ui, ui.id().with(("ex", &ids)), format!("{:.4}", l.b.x))
&& let Ok(f) = s.trim().parse::<f64>()
{
if let Some(f) = number(edit_field(ui, ui.id().with(("ex", &ids)), len(l.b.x)), &mut bad) {
set = Some(json!({ "handles": ids, "end": [f, l.b.y] }));
}
});
prop_row(ui, "End Y", |ui| {
if let Some(s) = edit_field(ui, ui.id().with(("ey", &ids)), format!("{:.4}", l.b.y))
&& let Ok(f) = s.trim().parse::<f64>()
{
if let Some(f) = number(edit_field(ui, ui.id().with(("ey", &ids)), len(l.b.y)), &mut bad) {
set = Some(json!({ "handles": ids, "end": [l.b.x, f] }));
}
});
let dlt = l.b.xy() - l.a.xy();
prop_row(ui, "Delta X", |ui| read_only(ui, &format!("{:.4}", dlt.x)));
prop_row(ui, "Delta Y", |ui| read_only(ui, &format!("{:.4}", dlt.y)));
prop_row(ui, "Delta X", |ui| read_only(ui, &len(dlt.x)));
prop_row(ui, "Delta Y", |ui| read_only(ui, &len(dlt.y)));
// Length and angle keep the start point and move the end point.
num(ui, "Length", "length", dlt.len(), &mut set, &ids);
num(ui, "Angle", "angle", dlt.angle().to_degrees(), &mut set, &ids);
num(ui, "Length", "length", len(dlt.len()), &mut set, &mut bad, &ids);
num(ui, "Angle", "angle", real(dlt.angle().to_degrees()), &mut set, &mut bad, &ids);
}
EntityKind::Circle(c) => {
num(ui, "Center X", "cx", c.center.x, &mut set, &ids);
num(ui, "Center Y", "cy", c.center.y, &mut set, &ids);
num(ui, "Radius", "radius", c.radius, &mut set, &ids);
num(ui, "Diameter", "diameter", c.radius * 2.0, &mut set, &ids);
num(ui, "Circumference", "circumference", c.radius * std::f64::consts::TAU, &mut set, &ids);
num(ui, "Area", "area", c.radius * c.radius * std::f64::consts::PI, &mut set, &ids);
num(ui, "Center X", "cx", len(c.center.x), &mut set, &mut bad, &ids);
num(ui, "Center Y", "cy", len(c.center.y), &mut set, &mut bad, &ids);
num(ui, "Radius", "radius", len(c.radius), &mut set, &mut bad, &ids);
num(ui, "Diameter", "diameter", len(c.radius * 2.0), &mut set, &mut bad, &ids);
num(ui, "Circumference", "circumference", len(c.radius * std::f64::consts::TAU), &mut set, &mut bad, &ids);
num(ui, "Area", "area", real(c.radius * c.radius * std::f64::consts::PI), &mut set, &mut bad, &ids);
if let Some(v) = &mut set
&& let Some(o) = v.as_object_mut()
{
@@ -973,9 +979,9 @@ pub(crate) fn properties_section(app: &mut CadApp, ui: &mut egui::Ui) {
}
}
EntityKind::Arc(a) => {
num(ui, "Radius", "radius", a.radius, &mut set, &ids);
num(ui, "Start angle", "startAngle", a.start.to_degrees(), &mut set, &ids);
num(ui, "End angle", "endAngle", a.end.to_degrees(), &mut set, &ids);
num(ui, "Radius", "radius", len(a.radius), &mut set, &mut bad, &ids);
num(ui, "Start angle", "startAngle", real(a.start.to_degrees()), &mut set, &mut bad, &ids);
num(ui, "End angle", "endAngle", real(a.end.to_degrees()), &mut set, &mut bad, &ids);
}
EntityKind::Text(tx) => {
prop_row(ui, "Contents", |ui| {
@@ -983,9 +989,9 @@ pub(crate) fn properties_section(app: &mut CadApp, ui: &mut egui::Ui) {
set = Some(json!({ "handles": ids, "text": s }));
}
});
num(ui, "Height", "height", tx.height, &mut set, &ids);
num(ui, "Rotation", "rotation", tx.rotation.to_degrees(), &mut set, &ids);
num(ui, "Width factor", "widthFactor", tx.width_factor, &mut set, &ids);
num(ui, "Height", "height", len(tx.height), &mut set, &mut bad, &ids);
num(ui, "Rotation", "rotation", real(tx.rotation.to_degrees()), &mut set, &mut bad, &ids);
num(ui, "Width factor", "widthFactor", real(tx.width_factor), &mut set, &mut bad, &ids);
}
EntityKind::MText(tx) => {
prop_row(ui, "Contents", |ui| {
@@ -993,13 +999,13 @@ pub(crate) fn properties_section(app: &mut CadApp, ui: &mut egui::Ui) {
set = Some(json!({ "handles": ids, "text": s }));
}
});
num(ui, "Text height", "height", tx.height, &mut set, &ids);
num(ui, "Defined width", "width", tx.width, &mut set, &ids);
num(ui, "Text height", "height", len(tx.height), &mut set, &mut bad, &ids);
num(ui, "Defined width", "width", len(tx.width), &mut set, &mut bad, &ids);
}
EntityKind::LwPolyline(pl) => {
num(ui, "Global width", "width", pl.const_width, &mut set, &ids);
num(ui, "Global width", "width", len(pl.const_width), &mut set, &mut bad, &ids);
let g = cadcraft_geom::Polyline { vertices: pl.vertices.clone(), closed: pl.closed };
prop_row(ui, "Length", |ui| read_only(ui, &format!("{:.4}", g.len())));
prop_row(ui, "Length", |ui| read_only(ui, &len(g.len())));
if pl.closed {
prop_row(ui, "Area", |ui| read_only(ui, &format!("{:.4}", g.area().abs())));
}
@@ -1020,8 +1026,8 @@ pub(crate) fn properties_section(app: &mut CadApp, ui: &mut egui::Ui) {
}
});
});
num(ui, "Scale", "scale", hh.scale, &mut set, &ids);
num(ui, "Angle", "angle", hh.angle.to_degrees(), &mut set, &ids);
num(ui, "Scale", "scale", real(hh.scale), &mut set, &mut bad, &ids);
num(ui, "Angle", "angle", real(hh.angle.to_degrees()), &mut set, &mut bad, &ids);
}
EntityKind::Dimension(dm) => {
prop_row(ui, "Text override", |ui| {
@@ -1037,11 +1043,11 @@ pub(crate) fn properties_section(app: &mut CadApp, ui: &mut egui::Ui) {
}
EntityKind::Insert(i) => {
prop_row(ui, "Name", |ui| read_only(ui, &i.block));
prop_row(ui, "Position X", |ui| read_only(ui, &format!("{:.4}", i.insert.x)));
prop_row(ui, "Position Y", |ui| read_only(ui, &format!("{:.4}", i.insert.y)));
prop_row(ui, "Position Z", |ui| read_only(ui, &format!("{:.4}", i.insert.z)));
num(ui, "Rotation", "rotation", i.rotation.to_degrees(), &mut set, &ids);
num(ui, "Scale", "scale", i.scale.x, &mut set, &ids);
prop_row(ui, "Position X", |ui| read_only(ui, &len(i.insert.x)));
prop_row(ui, "Position Y", |ui| read_only(ui, &len(i.insert.y)));
prop_row(ui, "Position Z", |ui| read_only(ui, &len(i.insert.z)));
num(ui, "Rotation", "rotation", real(i.rotation.to_degrees()), &mut set, &mut bad, &ids);
num(ui, "Scale", "scale", real(i.scale.x), &mut set, &mut bad, &ids);
}
_ => {}
}
@@ -1049,4 +1055,5 @@ pub(crate) fn properties_section(app: &mut CadApp, ui: &mut egui::Ui) {
if let Some(p) = set {
let _ = app.run("properties.set", p);
}
report_bad(app, bad);
}
+77
View File
@@ -0,0 +1,77 @@
//! Properties palette numeric fields (#849): typed values are read as the command line reads
//! them (feet and inches included), distances are shown in the drawing's units, and text that
//! isn't a number is reported instead of being dropped.
use cadcraft_engine::Session;
use cadcraft_engine::doc::EntityKind;
use cadcraft_ui_egui::{CadApp, Services};
use egui::{Key, vec2};
use egui_kittest::Harness;
use egui_kittest::kittest::{Queryable, by};
use serde_json::json;
/// A selected TEXT 2.5 high, with lengths in `lunits`.
fn palettes(lunits: i64) -> Harness<'static, CadApp> {
let mut app = CadApp::new(Session::default(), Services::default());
app.run("setvar", json!({ "name": "LUNITS", "value": lunits })).unwrap();
let h = app.run("text", json!({ "at": [0, 0], "text": "Note", "height": 2.5 })).unwrap();
app.run("select", json!({ "handles": [h["handle"]] })).unwrap();
let mut h = Harness::builder().with_size(vec2(400.0, 1400.0)).build_ui_state(
|ui, app: &mut CadApp| {
app.logic(&ui.ctx().clone());
cadcraft_ui_egui::palettes::right_palettes(app, ui);
},
app,
);
h.run_steps(4);
h
}
fn height(app: &CadApp) -> f64 {
let d = app.session.doc().unwrap();
d.model.iter().find_map(|e| if let EntityKind::Text(t) = &e.kind { Some(t.height) } else { None }).unwrap()
}
/// The text box showing `shown` (its text run child carries the same value).
fn field<'h>(h: &'h Harness<'static, CadApp>, shown: &'h str) -> egui_kittest::Node<'h> {
h.get(by().role(egui::accesskit::Role::TextInput).value(shown))
}
fn type_into(h: &mut Harness<'static, CadApp>, shown: &str, text: &str) {
field(h, shown).focus();
h.run_steps(2);
h.key_press_modifiers(egui::Modifiers::COMMAND, Key::A);
h.run_steps(1);
field(h, shown).type_text(text);
h.run_steps(2);
h.key_press(Key::Enter);
h.run_steps(3);
}
#[test]
fn text_height_takes_feet_and_inches() {
let mut h = palettes(2);
type_into(&mut h, "2.5000", "3'");
assert_eq!(height(h.state()), 36.0);
type_into(&mut h, "36.0000", "1'6 1/2\"");
assert_eq!(height(h.state()), 18.5);
}
#[test]
fn text_height_is_shown_in_the_drawing_units() {
let mut h = palettes(4);
type_into(&mut h, "0'-2 1/2\"", "3'6\"");
assert_eq!(height(h.state()), 42.0);
field(&h, "3'-6\"");
}
#[test]
fn unreadable_text_is_reported_not_dropped() {
let mut h = palettes(2);
let before = h.state().session.log.len();
type_into(&mut h, "2.5000", "abc");
assert_eq!(height(h.state()), 2.5);
let log = &h.state().session.log[before..];
assert!(log.iter().any(|l| l.contains("abc")), "{log:?}");
field(&h, "2.5000");
}