From 7fe832b284e4eddab10d2b6f8755c467242f3809 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 24 Oct 2026 08:01:54 -0700 Subject: [PATCH 1/2] MEASUREGEOM: a Volume option, and Tools > Inquiry > Volume MEASUREGEOM offered Distance, Radius and ARea, and Tools > Inquiry had no Volume item. The Volume option measures a polygon of three or more points, or a closed 2D object as AREA measures it, and asks for a height. It reports the area times the absolute value of the height. Add volume and Subtract volume keep a running total, as AREA's Add area and Subtract area do. Tools > Inquiry > Volume (volume.measure) starts the same prompts, and the JSON form takes points or a handle and a height. Engineering and architectural units show the volume as a decimal number. The de, fr and ru catalogs get a "Volume" row, and docs/parity-checklist.md is regenerated with cargo xtask parity. --- crates/engine/src/cmd/inquiry.rs | 91 ++++++++++++++++++++-- crates/engine/src/cmd/utility.rs | 13 +++- crates/engine/tests/it/inquiry_keywords.rs | 53 ++++++++++++- crates/ui-egui/src/i18n/de.tsv | 1 + crates/ui-egui/src/i18n/fr.tsv | 1 + crates/ui-egui/src/i18n/ru.tsv | 1 + docs/parity-checklist.md | 13 +--- 7 files changed, 151 insertions(+), 22 deletions(-) diff --git a/crates/engine/src/cmd/inquiry.rs b/crates/engine/src/cmd/inquiry.rs index 1f7f6f2ab..c6fdebc64 100644 --- a/crates/engine/src/cmd/inquiry.rs +++ b/crates/engine/src/cmd/inquiry.rs @@ -415,6 +415,34 @@ fn area_text(s: &Session, a: f64, per: f64) -> String { format!("Area = {}, Perimeter = {}", fmt_d(s, a), fmt_d(s, per)) } +/// MEASUREGEOM's Volume option: the area of a polygon of three or more points, or of a closed 2D +/// object (as AREA measures it), times the absolute value of a height. +pub(super) fn run_volume(s: &mut Session, p: &Value) -> Result { + let a = if let Some(pts) = points_param(p, "points") { + if pts.len() < 3 { + return Err(bad("volume.measure", "`points` needs 3 or more [x,y] points")); + } + area_of(&pts).0 + } else { + let h = targets(s, p)?.first().copied().ok_or_else(|| bad("volume.measure", "`points` or `handle` is required"))?; + object_area(s, h)?.0 + }; + let height = f64_req("volume.measure", p, "height")?; + let v = a * height.abs(); + Ok(json!({ "volume": v, "area": a, "height": height, "message": volume_text(s, v) })) +} + +fn volume_text(s: &Session, v: f64) -> String { + format!("Volume = {}", fmt_volume(s, v)) +} + +/// Format a volume as a number in the drawing's `LUPREC`. Engineering and architectural units +/// show it as a decimal number, not in feet and inches. +fn fmt_volume(s: &Session, v: f64) -> String { + let (u, p) = s.doc().map(|d| (d.header.i64("LUNITS", 2), d.header.i64("LUPREC", 4))).unwrap_or((2, 4)); + format_distance(v, if matches!(u, 3 | 4) { 2 } else { u }, p) +} + /// Area and perimeter of a closed object (AREA's Object option). pub(super) fn object_area(s: &Session, h: Handle) -> Result<(f64, f64)> { let d = s.doc()?; @@ -889,9 +917,17 @@ pub(super) struct AreaM { object: bool, /// Asking for the length of a segment (the Length option). length: bool, + /// Measuring volumes (MEASUREGEOM's Volume option): each area is multiplied by the absolute value of a height. + volume: bool, + /// Areas measured in volume mode that still wait for their height, oldest first. + pending: Vec, } impl AreaM { + /// MEASUREGEOM's Volume option: AREA's prompts, then a height for each area. + pub(super) fn volume() -> Self { + AreaM { volume: true, ..AreaM::default() } + } fn mode_prefix(&self) -> &'static str { match self.mode { AreaMode::Single => "", @@ -899,8 +935,29 @@ impl AreaM { AreaMode::Subtract => "(SUBTRACT mode) ", } } + /// The Add and Subtract keywords: "Add area" and "Subtract area", or "Add volume" and + /// "Subtract volume". + fn mode_keywords(&self) -> (&'static str, &'static str) { + if self.volume { ("Add volume", "Subtract volume") } else { ("Add area", "Subtract area") } + } + /// Report a volume (and the running total in add/subtract mode); ends the command in single mode. + fn volume_measured(&mut self, s: &mut Session, v: f64) -> Step { + s.echo(volume_text(s, v)); + match self.mode { + AreaMode::Single => return Step::Done, + AreaMode::Add => self.total += v, + AreaMode::Subtract => self.total -= v, + } + s.echo(format!("Total volume = {}", fmt_volume(s, self.total))); + Step::Continue + } /// Report an area (and the running total in add/subtract mode); ends the command in single mode. + /// In volume mode the area waits for its height instead. fn measured(&mut self, s: &mut Session, a: f64, per: f64) -> Step { + if self.volume { + self.pending.push(a); + return Step::Continue; + } s.echo(area_text(s, a, per)); match self.mode { AreaMode::Single => return Step::Done, @@ -923,9 +980,12 @@ impl AreaM { impl Interactive for AreaM { fn name(&self) -> &'static str { - "AREA" + if self.volume { "MEASUREGEOM" } else { "AREA" } } fn prompt(&self, _s: &Session) -> Prompt { + if !self.pending.is_empty() { + return Prompt::new(format!("{}Specify height", self.mode_prefix()), Accept::NUMBER); + } if self.object { return Prompt::new(format!("{}Select objects", self.mode_prefix()), Accept::SELECT); } @@ -933,17 +993,28 @@ impl Interactive for AreaM { return length_prompt(); } let next = format!("{}Specify next point", self.mode_prefix()); + let (add, subtract) = self.mode_keywords(); match (self.pts.len(), self.mode) { - (0, AreaMode::Single) => { - Prompt::new("Specify first corner point", Accept::POINT).kw(&["Object", "Add area", "Subtract area"]).default("Object") - } - (0, AreaMode::Add) => Prompt::new("Specify first corner point", Accept::POINT).kw(&["Object", "Subtract area"]), - (0, AreaMode::Subtract) => Prompt::new("Specify first corner point", Accept::POINT).kw(&["Object", "Add area"]), + (0, AreaMode::Single) => Prompt::new("Specify first corner point", Accept::POINT).kw(&["Object", add, subtract]).default("Object"), + (0, AreaMode::Add) => Prompt::new("Specify first corner point", Accept::POINT).kw(&["Object", subtract]), + (0, AreaMode::Subtract) => Prompt::new("Specify first corner point", Accept::POINT).kw(&["Object", add]), (1, _) => Prompt::new(next, Accept::POINT).kw(&["Arc", "Length", "Undo"]).base_opt(self.pts.last().copied()), _ => Prompt::new(next, Accept::POINT).kw(&["Arc", "Length", "Undo", "Total"]).default("Total").base_opt(self.pts.last().copied()), } } fn input(&mut self, s: &mut Session, i: Input) -> Result { + if !self.pending.is_empty() { + if let Input::Text(t) = i { + match crate::units::parse_distance(&t).filter(|v| v.is_finite()) { + Some(h) => { + let a = self.pending.remove(0); + return Ok(self.volume_measured(s, a * h.abs())); + } + None => s.echo("Requires a distance."), + } + } + return Ok(Step::Continue); + } if self.object { match i { Input::Pick(hs) => { @@ -953,6 +1024,10 @@ impl Interactive for AreaM { if self.measured(s, a, per) == Step::Done { return Ok(Step::Done); } + // A single volume takes the first object with an area. + if self.volume && self.mode == AreaMode::Single { + break; + } } Err(_) => s.echo("The selected object has no area."), } @@ -980,8 +1055,8 @@ impl Interactive for AreaM { Input::Point(p) => self.pts.push(p), Input::Keyword(k) => match k.as_str() { "Object" => self.object = true, - "Add area" => self.mode = AreaMode::Add, - "Subtract area" => self.mode = AreaMode::Subtract, + "Add area" | "Add volume" => self.mode = AreaMode::Add, + "Subtract area" | "Subtract volume" => self.mode = AreaMode::Subtract, "Undo" => { self.pts.pop(); } diff --git a/crates/engine/src/cmd/utility.rs b/crates/engine/src/cmd/utility.rs index 89c00f7a6..be5db348c 100644 --- a/crates/engine/src/cmd/utility.rs +++ b/crates/engine/src/cmd/utility.rs @@ -27,11 +27,16 @@ pub fn specs() -> Vec { .interactive(|_| Ok(Box::::default())), CommandSpec::new("find", "Find...", run_find).menu(&["Edit", "Find..."]).params("{find, replace?, matchCase?: bool, wholeWord?: bool}"), CommandSpec::new("measuregeom", "Measure Geometry", run_measuregeom) - .params("{mode: distance|radius|angle|area, ...}") + .params("{mode: distance|radius|angle|area|volume, ...}") .noundo() .interactive(|_| Ok(Box::::default())), CommandSpec::new("radius.measure", "Radius", run_radius).menu(&["Tools", "Inquiry", "Radius"]).params("{handle}").noundo(), CommandSpec::new("angle.measure", "Angle", run_angle).menu(&["Tools", "Inquiry", "Angle"]).params("{vertex, p1, p2} | {h1, h2}").noundo(), + CommandSpec::new("volume.measure", "Volume", super::inquiry::run_volume) + .menu(&["Tools", "Inquiry", "Volume"]) + .params("{points: [[x,y],...] (3 or more) | handle (a closed 2D object), height} → {volume, area, height}") + .noundo() + .interactive(|_| Ok(Box::new(super::inquiry::AreaM::volume()))), ] } @@ -318,7 +323,8 @@ fn run_measuregeom(s: &mut Session, p: &Value) -> Result { "radius" | "r" => run_radius(s, p), "angle" | "a" => run_angle(s, p), "area" | "ar" => s.execute("area", p), - _ => Err(bad("measuregeom", "mode must be distance, radius, angle or area")), + "volume" | "v" => super::inquiry::run_volume(s, p), + _ => Err(bad("measuregeom", "mode must be distance, radius, angle, area or volume")), } } @@ -339,7 +345,7 @@ impl Interactive for MeasureM { Some(m) => m.prompt(s), None if self.radius => Prompt::new("Select arc or circle", Accept::POINT), None => Prompt::new("Enter an option", Accept { point: false, number: false, text: false, select: false, enter: true }) - .kw(&["Distance", "Radius", "ARea"]) + .kw(&["Distance", "Radius", "ARea", "Volume"]) .default("Distance"), } } @@ -366,6 +372,7 @@ impl Interactive for MeasureM { Input::Keyword(k) => match k.as_str() { "Radius" => self.radius = true, "ARea" => self.inner = Some(Box::new(super::inquiry::AreaM::default())), + "Volume" => self.inner = Some(Box::new(super::inquiry::AreaM::volume())), _ => self.inner = Some(Box::new(super::inquiry::DistM::default())), }, _ => {} diff --git a/crates/engine/tests/it/inquiry_keywords.rs b/crates/engine/tests/it/inquiry_keywords.rs index 920dd7ee5..166b374ef 100644 --- a/crates/engine/tests/it/inquiry_keywords.rs +++ b/crates/engine/tests/it/inquiry_keywords.rs @@ -1,4 +1,5 @@ -//! AREA's Object / Add area / Subtract area and DIST's Multiple points, typed as at the command line. +//! AREA's Object / Add area / Subtract area, MEASUREGEOM's Volume and DIST's Multiple points, typed as +//! at the command line. use cadcraft_engine::Session; use serde_json::json; @@ -103,3 +104,53 @@ fn area_of_an_ellipse_reports_its_exact_perimeter() { let (_, log) = run(&["ellipse c 0,0 10,0 4", "area", "o", "10,0"]); assert!(has(&log, "Perimeter = 46.0262"), "{log:?}"); } + +#[test] +fn measuregeom_volume_multiplies_an_area_by_a_height() { + // Points: a 4 x 3 triangle's area is 6, times a height of 10. + let (s, log) = run(&["measuregeom", "v", "0,0", "4,0", "4,3", "", "10"]); + assert!(has(&log, "Volume = 60.0000"), "{log:?}"); + assert!(s.running.is_none(), "{}", s.prompt_text()); + + // Object (the default): a 10 x 10 square polyline, height 2. + let (s, log) = run(&["rectang 0,0 10,10", "measuregeom", "v", "", "10,5", "2"]); + assert!(has(&log, "Volume = 200.0000"), "{log:?}"); + assert!(s.running.is_none(), "{}", s.prompt_text()); + + // A height that is not a distance is asked for again. + let (s, log) = run(&["measuregeom", "v", "0,0", "4,0", "4,3", "", "tall"]); + assert!(has(&log, "Requires a distance."), "{log:?}"); + assert!(s.prompt_text().contains("Specify height"), "{}", s.prompt_text()); + + // The JSON forms, by points and by handle. + let mut s = Session::new(); + let r = s.execute("measuregeom", &json!({"mode": "volume", "points": [[0, 0], [4, 0], [4, 3]], "height": 10})).unwrap(); + assert_eq!(r["volume"], json!(60.0)); + let c = s.execute("circle", &json!({"center": [0, 0], "radius": 1})).unwrap(); + let r = s.execute("volume.measure", &json!({"handle": c["handle"], "height": -3})).unwrap(); + assert!((r["volume"].as_f64().unwrap() - 3.0 * std::f64::consts::PI).abs() < 1e-12, "{r}"); + assert!(s.execute("volume.measure", &json!({"points": [[0, 0], [1, 0]], "height": 1})).is_err()); + assert!(s.execute("volume.measure", &json!({"points": [[0, 0], [1, 0], [1, 1]]})).is_err()); +} + +#[test] +fn tools_inquiry_volume_adds_and_subtracts_a_running_total() { + let spec = cadcraft_engine::find_command("volume.measure").expect("no command volume.measure"); + assert_eq!(spec.menu, ["Tools", "Inquiry", "Volume"]); + + // Add a 10 x 10 square by points at height 3, subtract a 2 x 2 square object at height 5. + let mut s = Session::new(); + s.cmdline("rectang 20,0 22,2").unwrap(); + s.start("volume.measure").unwrap(); + for l in ["a", "0,0", "10,0", "10,10", "0,10", "", "3", "s", "o", "21,0", "5", "", ""] { + s.cmdline(l).unwrap(); + } + assert!(has(&s.log, "Total volume = 300.0000"), "{:?}", s.log); + assert!(has(&s.log, "Total volume = 280.0000"), "{:?}", s.log); + assert!(s.running.is_none(), "{}", s.prompt_text()); + + // Architectural units show the volume as a number, not as a feet-and-inches length. + let (_, log) = run(&["setvar lunits 4", "measuregeom", "v", "0,0", "10,0", "10,10", "0,10", "", "12"]); + assert!(has(&log, "Volume = 1200"), "{log:?}"); + assert!(!has(&log, "'-"), "{log:?}"); +} diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index e2e92d4c8..daef6939e 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -967,6 +967,7 @@ Viewport Properties Ansichtsfenster-Eigenschaften Viewport {id} Ansichtsfenster {id} Viewports Ansichtsfenster + Volume Volumen W W WCS WKS White Weiß diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index d8b100711..65264262c 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -961,6 +961,7 @@ Viewport Properties Propriétés de la fenêtre Viewport {id} Fenêtre {id} Viewports Fenêtres + Volume Volume W O WCS SCG White Blanc diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index c7a6baaf2..fd97e4589 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -961,6 +961,7 @@ Viewport Properties Свойства видового экрана Viewport {id} Видовой экран {id} Viewports Видовые экраны + Volume Объём W З WCS МСК White Белый diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index 5e1c7cf7f..0f331eb9d 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -1,11 +1,10 @@ # Parity checklist -> **Generated:** 2026-10-15 by `cargo xtask parity`. Do not edit by hand. -> **Generated:** 2026-10-16 by `cargo xtask parity`. Do not edit by hand. +> **Generated:** 2026-10-24 by `cargo xtask parity`. Do not edit by hand. Menu-breadth parity: a reference menu item counts as covered when a CADCraft command is registered under the same menu label. It measures breadth, not depth; see [target-app-parity.md](target-app-parity.md) for the weighted assessment. -**Menu breadth: 283 / 491 items (58%)** · 427 registered commands. +**Menu breadth: 288 / 491 items (59%)** · 434 registered commands. | Menu | Items | Covered | % | |---|---|---|---| @@ -14,7 +13,7 @@ Menu-breadth parity: a reference menu item counts as covered when a CADCraft com | View | 75 | 31 | 41% | | Insert | 11 | 5 | 45% | | Format | 27 | 25 | 93% | -| Tools | 85 | 60 | 71% | +| Tools | 85 | 65 | 76% | | Draw | 87 | 55 | 63% | | Dimension | 28 | 28 | 100% | | Modify | 113 | 52 | 46% | @@ -87,7 +86,6 @@ Menu-breadth parity: a reference menu item counts as covered when a CADCraft com - Format > Transparency - Format > Thickness - Tools > Spelling -- Tools > Inquiry > Volume - Tools > Update Fields - Tools > Edit Xref in Place - Tools > New UCS > World @@ -101,13 +99,8 @@ Menu-breadth parity: a reference menu item counts as covered when a CADCraft com - Tools > New UCS > Z - Tools > Named UCS... - Tools > Customize > Interface (CUI)... -- Tools > Customize > Edit Command Aliases (PGP)... - Tools > Import Settings - Tools > Export Settings -- Tools > Express Tools > Convert Text to Mtext -- Tools > Express Tools > Break-line Symbol -- Tools > Express Tools > Move/Copy/Rotate -- Tools > Express Tools > Explode Attributes to Text - Tools > Express Tools > Replace Block... - Tools > Express Tools > Super Hatch > Image... - Tools > Express Tools > Super Hatch > Xref... From 596ea36465e5221d074ea7ea84564782c73ca765 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 24 Oct 2026 10:45:53 -0700 Subject: [PATCH 2/2] MEASUREGEOM: malformed points and volumes past f64's range are errors AREA and Volume measured the selected object when `points` was given but was not a list of points; they now report an error, and only an absent or null `points` measures the selection. A volume too large for f64 (an area times a height) is an error in the JSON form. At the typed prompt, a volume or an Add/Subtract volume running total too large for f64 makes the prompt say "The volume is too large." and ask for the height again, where it reported "Volume = 0" or "Total volume = 0". --- crates/engine/src/cmd/inquiry.rs | 46 ++++++++++++++++------ crates/engine/tests/it/inquiry_keywords.rs | 43 ++++++++++++++++++++ 2 files changed, 77 insertions(+), 12 deletions(-) diff --git a/crates/engine/src/cmd/inquiry.rs b/crates/engine/src/cmd/inquiry.rs index c6fdebc64..26f074a96 100644 --- a/crates/engine/src/cmd/inquiry.rs +++ b/crates/engine/src/cmd/inquiry.rs @@ -401,8 +401,17 @@ pub(super) fn area_of(pts: &[Vec2]) -> (f64, f64) { (a, per) } +/// `points` as given, `None` when it is absent; a `points` that is not a list of points is an +/// error, never a fall back to the selection. +fn given_points(p: &Value, cmd: &str) -> Result>> { + match p.get("points") { + None | Some(Value::Null) => Ok(None), + Some(_) => points_param(p, "points").map(Some).ok_or_else(|| bad(cmd, "`points` must be a list of [x, y] points")), + } +} + fn run_area(s: &mut Session, p: &Value) -> Result { - let (a, per) = if let Some(pts) = points_param(p, "points") { + let (a, per) = if let Some(pts) = given_points(p, "area")? { area_of(&pts) } else { let h = targets(s, p)?.first().copied().ok_or_else(|| bad("area", "`points` or `handle` is required"))?; @@ -415,10 +424,10 @@ fn area_text(s: &Session, a: f64, per: f64) -> String { format!("Area = {}, Perimeter = {}", fmt_d(s, a), fmt_d(s, per)) } -/// MEASUREGEOM's Volume option: the area of a polygon of three or more points, or of a closed 2D -/// object (as AREA measures it), times the absolute value of a height. +/// MEASUREGEOM's Volume option: the area of a polygon of three or more points, or of a 2D object +/// as AREA measures it (an open polyline as if closed), times the absolute value of a height. pub(super) fn run_volume(s: &mut Session, p: &Value) -> Result { - let a = if let Some(pts) = points_param(p, "points") { + let a = if let Some(pts) = given_points(p, "volume.measure")? { if pts.len() < 3 { return Err(bad("volume.measure", "`points` needs 3 or more [x,y] points")); } @@ -429,6 +438,9 @@ pub(super) fn run_volume(s: &mut Session, p: &Value) -> Result { }; let height = f64_req("volume.measure", p, "height")?; let v = a * height.abs(); + if !v.is_finite() { + return Err(bad("volume.measure", "the volume is too large")); + } Ok(json!({ "volume": v, "area": a, "height": height, "message": volume_text(s, v) })) } @@ -940,14 +952,21 @@ impl AreaM { fn mode_keywords(&self) -> (&'static str, &'static str) { if self.volume { ("Add volume", "Subtract volume") } else { ("Add area", "Subtract area") } } + /// The running total with `v` added or subtracted; in single mode, the total as it is. + fn total_with(&self, v: f64) -> f64 { + match self.mode { + AreaMode::Single => self.total, + AreaMode::Add => self.total + v, + AreaMode::Subtract => self.total - v, + } + } /// Report a volume (and the running total in add/subtract mode); ends the command in single mode. fn volume_measured(&mut self, s: &mut Session, v: f64) -> Step { s.echo(volume_text(s, v)); - match self.mode { - AreaMode::Single => return Step::Done, - AreaMode::Add => self.total += v, - AreaMode::Subtract => self.total -= v, + if self.mode == AreaMode::Single { + return Step::Done; } + self.total = self.total_with(v); s.echo(format!("Total volume = {}", fmt_volume(s, self.total))); Step::Continue } @@ -1006,10 +1025,13 @@ impl Interactive for AreaM { if !self.pending.is_empty() { if let Input::Text(t) = i { match crate::units::parse_distance(&t).filter(|v| v.is_finite()) { - Some(h) => { - let a = self.pending.remove(0); - return Ok(self.volume_measured(s, a * h.abs())); - } + Some(h) => match self.pending.first().map(|a| a * h.abs()) { + Some(v) if v.is_finite() && self.total_with(v).is_finite() => { + self.pending.remove(0); + return Ok(self.volume_measured(s, v)); + } + _ => s.echo("The volume is too large."), + }, None => s.echo("Requires a distance."), } } diff --git a/crates/engine/tests/it/inquiry_keywords.rs b/crates/engine/tests/it/inquiry_keywords.rs index 166b374ef..49b807a57 100644 --- a/crates/engine/tests/it/inquiry_keywords.rs +++ b/crates/engine/tests/it/inquiry_keywords.rs @@ -133,6 +133,49 @@ fn measuregeom_volume_multiplies_an_area_by_a_height() { assert!(s.execute("volume.measure", &json!({"points": [[0, 0], [1, 0], [1, 1]]})).is_err()); } +#[test] +fn measure_with_points_that_are_not_points_is_an_error_not_the_selection() { + let mut s = Session::new(); + let c = s.execute("circle", &json!({"center": [0, 0], "radius": 1})).unwrap(); + s.set_selection(vec![cadcraft_engine::doc::Handle::parse_hex(c["handle"].as_str().unwrap()).unwrap()]); + for bad in [json!([[0, 0], [4, 0], "north"]), json!([[0, 0], [4, 0], [4]]), json!(7)] { + assert!(s.execute("volume.measure", &json!({"points": bad, "height": 10})).is_err(), "{bad}"); + assert!(s.execute("area", &json!({"points": bad})).is_err(), "{bad}"); + } + // Without `points`, both measure the selected circle. + let r = s.execute("volume.measure", &json!({"height": 1})).unwrap(); + assert!((r["volume"].as_f64().unwrap() - std::f64::consts::PI).abs() < 1e-12, "{r}"); + let r = s.execute("area", &json!({"points": null})).unwrap(); + assert!((r["area"].as_f64().unwrap() - std::f64::consts::PI).abs() < 1e-12, "{r}"); +} + +#[test] +fn measuregeom_volume_too_large_to_hold_is_refused() { + // A 2e150 square has an area of 4e300; times 1e10 it is past f64's range. + let p = json!([[0, 0], [2e150, 0], [2e150, 2e150], [0, 2e150]]); + let mut s = Session::new(); + assert!(s.execute("volume.measure", &json!({"points": p, "height": 1e10})).is_err()); + assert!(s.execute("volume.measure", &json!({"points": p, "height": 1})).is_ok()); + + // Typed, the height is asked for again. + let (s, log) = run(&["measuregeom", "v", "0,0", "2e150,0", "2e150,2e150", "0,2e150", "", "1e10"]); + assert!(has(&log, "The volume is too large."), "{log:?}"); + assert!(!has(&log, "Volume = "), "{log:?}"); + assert!(s.prompt_text().contains("Specify height"), "{}", s.prompt_text()); + + // Two volumes that each fit can overflow the running total; the second is asked for again. + let sq = ["0,0", "1,0", "1,1", "0,1", ""]; + let mut lines = vec!["measuregeom", "v", "a"]; + lines.extend(sq); + lines.push("1e308"); + lines.extend(sq); + lines.push("1e308"); + let (s, log) = run(&lines); + assert!(has(&log, "The volume is too large."), "{log:?}"); + assert!(!has(&log, "Total volume = 0"), "{log:?}"); + assert!(s.prompt_text().contains("Specify height"), "{}", s.prompt_text()); +} + #[test] fn tools_inquiry_volume_adds_and_subtracts_a_running_total() { let spec = cadcraft_engine::find_command("volume.measure").expect("no command volume.measure");