diff --git a/Cargo.lock b/Cargo.lock index 39dbc92af..af5505662 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,7 +29,6 @@ dependencies = [ "rayon", "rfd", "rustc-hash 2.1.2", - "stormsewer", "truck-meshalgo", "truck-modeling", "truck-polymesh", @@ -4170,15 +4169,6 @@ dependencies = [ "serde", ] -[[package]] -name = "quick-xml" -version = "0.37.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" -dependencies = [ - "memchr 2.8.1", -] - [[package]] name = "quick-xml" version = "0.39.4" @@ -5186,13 +5176,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "stormsewer" -version = "0.1.0" -dependencies = [ - "quick-xml 0.37.5", -] - [[package]] name = "strict-num" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index 5f2dfb23d..6677ec817 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,9 +4,6 @@ version = "0.5.0" edition = "2021" build = "build.rs" -[workspace] -members = ["crates/stormsewer"] - [dependencies] # vtkio (pulled in transitively via truck-meshalgo) depends on xz2 → lzma-sys, # which by default links the system liblzma dynamically. On macOS that bakes a @@ -34,9 +31,6 @@ truck-shapeops = "0.4" # Fast non-cryptographic hasher. Handle/u64 keys dominate the hot block, # hatch, draw-depth and wire caches; the default SipHash is overkill there. rustc-hash = "2" -# Storm-sewer hydrology/hydraulics engine (Rational + Manning + HGL). Local -# crate; powers the `storm_sewer` ribbon module. -stormsewer = { path = "crates/stormsewer" } [target.'cfg(target_os = "windows")'.dependencies] windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] } diff --git a/crates/stormsewer/Cargo.toml b/crates/stormsewer/Cargo.toml deleted file mode 100644 index 8b5b63331..000000000 --- a/crates/stormsewer/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "stormsewer" -version = "0.1.0" -edition = "2021" -description = "Native-Rust storm sewer network hydrology & hydraulics (Rational method, Manning, HGL). Engine core for the Open CAD Studio storm-sewer module and HydroComplete." -license = "GPL-3.0-or-later" -repository = "https://github.com/mf4633/stormsewer" -keywords = ["hydrology", "hydraulics", "stormwater", "civil-engineering", "cad"] -categories = ["science", "simulation"] - -# Engine is std-only on purpose: dependency-light and WASM-friendly. -[dependencies] -quick-xml = { version = "0.37", default-features = false } - -[lib] -name = "stormsewer" diff --git a/crates/stormsewer/examples/sample.ssn b/crates/stormsewer/examples/sample.ssn deleted file mode 100644 index f4a57e54c..000000000 --- a/crates/stormsewer/examples/sample.ssn +++ /dev/null @@ -1,18 +0,0 @@ -# Sample storm-sewer network (stormsewer-cli examples/sample.ssn) -# A short, properly-sized trunk line: two catch basins + a junction to outfall. - -IDF 60 10 0.8 # i = 60 / (t+10)^0.8 (in/hr, t in minutes) ~ 5 in/hr -TAILWATER 100.5 # outfall tailwater elevation (ft) -MINTC 10 # minimum time of concentration (min) -JUNCTIONK 0.5 # junction loss coefficient - -# id kind x y invert rim area C tc_inlet -NODE N1 inlet 0 0 104.0 110.0 1.0 0.70 12 -NODE N2 inlet 300 0 102.5 108.5 1.0 0.70 10 -NODE N3 junction 550 0 101.2 107.0 0.5 0.80 8 -NODE OUT outfall 730 0 100.0 106.0 - -# id from to length dia n (dia: 15in=1.25, 18in=1.5, 21in=1.75) -PIPE P1 N1 N2 300 1.25 0.013 -PIPE P2 N2 N3 250 1.50 0.013 -PIPE P3 N3 OUT 180 1.75 0.013 diff --git a/crates/stormsewer/examples/sample_landxml.xml b/crates/stormsewer/examples/sample_landxml.xml deleted file mode 100644 index 9dc51dac1..000000000 --- a/crates/stormsewer/examples/sample_landxml.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - -
0.0 0.0 110.0
- 104.0 - 110.0 -
- -
150.0 0.0 108.0
- 102.0 - 108.0 -
- -
300.0 0.0 106.0
- 100.0 - 106.0 -
-
- - - - IN1 - J1 - - - - J1 - OUT1 - - -
-
-
\ No newline at end of file diff --git a/crates/stormsewer/src/bin/stormsewer-cli.rs b/crates/stormsewer/src/bin/stormsewer-cli.rs deleted file mode 100644 index a40be0e03..000000000 --- a/crates/stormsewer/src/bin/stormsewer-cli.rs +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -//! `stormsewer-cli` — run a storm-sewer analysis from a `.ssn` network file. -//! -//! Usage: stormsewer-cli -//! -//! See `stormsewer::parse` for the file format. - -use std::process::exit; -use stormsewer::parse::parse_ssn; -use stormsewer::report::format_analysis; - -fn die(msg: &str) -> ! { - eprintln!("error: {msg}"); - exit(1); -} - -fn main() { - let path = match std::env::args().nth(1) { - Some(p) => p, - None => die("usage: stormsewer-cli "), - }; - let text = std::fs::read_to_string(&path).unwrap_or_else(|e| die(&format!("cannot read {path}: {e}"))); - let parsed = parse_ssn(&text).unwrap_or_else(|e| die(&e)); - match parsed.network.analyze(&parsed.idf, &parsed.options) { - Ok(a) => print!("{}", format_analysis(&a)), - Err(e) => die(&e.to_string()), - } -} diff --git a/crates/stormsewer/src/catchment.rs b/crates/stormsewer/src/catchment.rs deleted file mode 100644 index 95f9fddd8..000000000 --- a/crates/stormsewer/src/catchment.rs +++ /dev/null @@ -1,84 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -//! Catchment polygon geometry helpers (CAD-agnostic). - -use crate::hydrology::kirpich_minutes; - -/// Shoelace formula area for a closed polygon (square feet). -pub fn shoelace_area_sqft(vertices: &[(f64, f64)]) -> f64 { - let n = vertices.len(); - if n < 3 { - return 0.0; - } - let mut sum = 0.0; - for i in 0..n { - let (x0, y0) = vertices[i]; - let (x1, y1) = vertices[(i + 1) % n]; - sum += x0 * y1 - x1 * y0; - } - (sum / 2.0).abs() -} - -/// Plan centroid of a closed polygon. -pub fn polygon_centroid(vertices: &[(f64, f64)]) -> (f64, f64) { - let n = vertices.len(); - if n < 3 { - let sx: f64 = vertices.iter().map(|v| v.0).sum(); - let sy: f64 = vertices.iter().map(|v| v.1).sum(); - let d = n.max(1) as f64; - return (sx / d, sy / d); - } - let mut a2 = 0.0; - let mut cx = 0.0; - let mut cy = 0.0; - for i in 0..n { - let (x0, y0) = vertices[i]; - let (x1, y1) = vertices[(i + 1) % n]; - let cross = x0 * y1 - x1 * y0; - a2 += cross; - cx += (x0 + x1) * cross; - cy += (y0 + y1) * cross; - } - if a2.abs() < 1e-12 { - let sx: f64 = vertices.iter().map(|v| v.0).sum(); - let sy: f64 = vertices.iter().map(|v| v.1).sum(); - return (sx / n as f64, sy / n as f64); - } - (cx / (3.0 * a2), cy / (3.0 * a2)) -} - -/// Convert square feet to acres. -pub fn sqft_to_acres(area_sqft: f64) -> f64 { - area_sqft / 43_560.0 -} - -/// Default flow-path length: plan distance from catchment centroid to a target point (ft). -pub fn default_flow_length_ft(centroid: (f64, f64), target: (f64, f64)) -> f64 { - let dx = target.0 - centroid.0; - let dy = target.1 - centroid.1; - (dx * dx + dy * dy).sqrt() -} - -/// Kirpich Tc (minutes) for a catchment polygon draining toward a structure. -pub fn catchment_tc_minutes(flow_length_ft: f64, slope: f64) -> f64 { - kirpich_minutes(flow_length_ft, slope) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn unit_square_area() { - let verts = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)]; - assert!((shoelace_area_sqft(&verts) - 100.0).abs() < 1e-6); - assert!((sqft_to_acres(43_560.0) - 1.0).abs() < 1e-9); - } - - #[test] - fn centroid_of_square() { - let verts = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)]; - let (cx, cy) = polygon_centroid(&verts); - assert!((cx - 5.0).abs() < 1e-6 && (cy - 5.0).abs() < 1e-6); - } -} \ No newline at end of file diff --git a/crates/stormsewer/src/design/criteria.rs b/crates/stormsewer/src/design/criteria.rs deleted file mode 100644 index c30f19e1d..000000000 --- a/crates/stormsewer/src/design/criteria.rs +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -//! Design criteria for storm-sewer pipe sizing (velocity, capacity, catalogs). - -/// Standard reinforced-concrete pipe diameters (inches) used in US storm design. -pub const STANDARD_RCP_INCHES: &[u32] = - &[8, 10, 12, 15, 18, 21, 24, 27, 30, 33, 36, 42, 48, 54, 60, 66, 72]; - -/// Convert a catalog diameter from inches to feet. -pub fn inches_to_ft(d_in: u32) -> f64 { - d_in as f64 / 12.0 -} - -/// Default ascending catalog in feet. -pub fn standard_diameters_ft() -> Vec { - STANDARD_RCP_INCHES.iter().map(|&d| inches_to_ft(d)).collect() -} - -/// Agency-style limits used by [`super::sizing::size_pipe_for_flow`]. -#[derive(Clone, Debug, PartialEq)] -pub struct DesignCriteria { - /// Minimum design velocity (ft/s). Pipes below this are rejected. - pub min_velocity: f64, - /// Maximum design velocity (ft/s). Pipes above this are rejected. - pub max_velocity: f64, - /// Maximum design flow as a fraction of just-full Manning capacity. - pub max_pct_full: f64, - /// Ascending catalog of trial diameters (ft). - pub standard_diameters_ft: Vec, - /// When true, reject diameters where design Q exceeds open-channel capacity. - pub require_open_channel: bool, -} - -impl Default for DesignCriteria { - fn default() -> Self { - Self { - min_velocity: 2.0, - max_velocity: 10.0, - max_pct_full: 0.85, - standard_diameters_ft: standard_diameters_ft(), - require_open_channel: true, - } - } -} - -impl DesignCriteria { - /// Typical municipal / DOT storm trunk defaults. - pub fn municipal() -> Self { - Self::default() - } - - /// Slightly relaxed criteria for laterals (allows higher % full). - pub fn lateral() -> Self { - Self { max_pct_full: 0.95, ..Self::default() } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn catalog_is_ascending_in_feet() { - let d = standard_diameters_ft(); - assert!(d.windows(2).all(|w| w[0] < w[1])); - assert!((d[0] - inches_to_ft(8)).abs() < 1e-9); - } -} \ No newline at end of file diff --git a/crates/stormsewer/src/design/inlets.rs b/crates/stormsewer/src/design/inlets.rs deleted file mode 100644 index 1719726fd..000000000 --- a/crates/stormsewer/src/design/inlets.rs +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -//! Inlet interception capacity (HEC-22 style grate on grade, simplified). - -/// Grate-on-grade interception capacity (cfs). -/// -/// Uses the HEC-22 composite gutter approximation for a depressed grate: -/// `Q = Cw * L * d^1.5 * sqrt(S)` with `Cw ≈ 3.0` (US customary calibration). -pub fn grate_capacity_cfs(grate_length_ft: f64, flow_depth_ft: f64, gutter_slope: f64) -> f64 { - if grate_length_ft <= 0.0 || flow_depth_ft <= 0.0 || gutter_slope <= 0.0 { - return 0.0; - } - const CW: f64 = 3.0; - CW * grate_length_ft * flow_depth_ft.powf(1.5) * gutter_slope.sqrt() -} - -/// Check whether an inlet can capture the approach design flow. -#[derive(Clone, Debug, PartialEq)] -pub struct InletCheck { - pub design_q_cfs: f64, - pub capacity_cfs: f64, - pub ok: bool, -} - -pub fn check_inlet(design_q_cfs: f64, grate_length_ft: f64, flow_depth_ft: f64, gutter_slope: f64) -> InletCheck { - let cap = grate_capacity_cfs(grate_length_ft, flow_depth_ft, gutter_slope); - InletCheck { - design_q_cfs, - capacity_cfs: cap, - ok: cap >= design_q_cfs, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn longer_grate_carries_more() { - let a = grate_capacity_cfs(2.0, 0.15, 0.005); - let b = grate_capacity_cfs(5.0, 0.15, 0.005); - assert!(b > a); - } -} \ No newline at end of file diff --git a/crates/stormsewer/src/design/mod.rs b/crates/stormsewer/src/design/mod.rs deleted file mode 100644 index 933b2935e..000000000 --- a/crates/stormsewer/src/design/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -//! Storm-sewer design: criteria catalogs, pipe sizing, and sizing reports. - -pub mod criteria; -pub mod inlets; -pub mod sizing; - -pub use criteria::*; -pub use inlets::*; -pub use sizing::*; \ No newline at end of file diff --git a/crates/stormsewer/src/design/sizing.rs b/crates/stormsewer/src/design/sizing.rs deleted file mode 100644 index 07e59da8e..000000000 --- a/crates/stormsewer/src/design/sizing.rs +++ /dev/null @@ -1,287 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -//! Pipe sizing against design criteria — smallest standard pipe that carries -//! the Rational design flow within velocity and capacity limits. - -use crate::hydraulics::{ - circular_geometry, full_area, full_flow_capacity, max_capacity, normal_depth, K_MANNING_US, -}; -use crate::network::{Analysis, Network, Pipe}; - -use super::criteria::DesignCriteria; - -/// Outcome of sizing a single pipe for a known discharge and slope. -#[derive(Clone, Debug, PartialEq)] -pub enum SizeOutcome { - /// Smallest catalog pipe that meets all criteria. - Sized, - /// Current diameter already meets criteria (may equal recommended). - Adequate, - /// No catalog pipe satisfies the criteria at this slope. - NoSolution, -} - -/// Result of sizing one pipe cross-section. -#[derive(Clone, Debug, PartialEq)] -pub struct PipeSizeResult { - pub diameter_ft: f64, - pub velocity: f64, - pub pct_full: f64, - pub normal_depth: Option, - pub surcharged: bool, - pub outcome: SizeOutcome, -} - -/// Per-pipe recommendation for a sized network. -#[derive(Clone, Debug, PartialEq)] -pub struct PipeSizeRecommendation { - pub pipe_id: String, - pub design_q: f64, - pub slope: f64, - pub current_diameter_ft: f64, - pub recommended_diameter_ft: f64, - pub meets_criteria: bool, - pub velocity: f64, - pub pct_full: f64, - pub surcharged: bool, - pub outcome: SizeOutcome, - pub note: String, -} - -/// Evaluate whether diameter `d` carries `q` on slope `s` within criteria. -fn evaluate_diameter(q: f64, slope: f64, n: f64, d: f64, criteria: &DesignCriteria) -> Option { - if d <= 0.0 || q < 0.0 { - return None; - } - let k = K_MANNING_US; - let (q_max, _) = max_capacity(n, slope, d, k); - if criteria.require_open_channel && q > q_max + 1e-9 { - return None; - } - let yn = normal_depth(q, n, slope, d, k); - let surcharged = yn.is_none(); - if criteria.require_open_channel && surcharged { - return None; - } - let area = if surcharged { - full_area(d) - } else { - circular_geometry(yn.unwrap_or(0.0), d).0 - }; - let velocity = if area > 0.0 { q / area } else { 0.0 }; - let capacity = full_flow_capacity(n, slope, d, k); - let pct_full = if capacity > 0.0 { q / capacity } else { 0.0 }; - - if velocity < criteria.min_velocity - 1e-9 { - return None; - } - if velocity > criteria.max_velocity + 1e-9 { - return None; - } - if pct_full > criteria.max_pct_full + 1e-9 { - return None; - } - - Some(PipeSizeResult { - diameter_ft: d, - velocity, - pct_full, - normal_depth: yn, - surcharged, - outcome: SizeOutcome::Sized, - }) -} - -/// Pick the smallest catalog diameter that carries `q` on slope `s`. -pub fn size_pipe_for_flow(q: f64, slope: f64, n: f64, criteria: &DesignCriteria) -> PipeSizeResult { - for &d in &criteria.standard_diameters_ft { - if let Some(r) = evaluate_diameter(q, slope, n, d, criteria) { - return r; - } - } - // No solution — report the largest catalog pipe's hydraulics for diagnostics. - let d = *criteria.standard_diameters_ft.last().unwrap_or(&0.0); - let k = K_MANNING_US; - let yn = normal_depth(q, n, slope, d, k); - let surcharged = yn.is_none(); - let area = if surcharged { - full_area(d) - } else { - circular_geometry(yn.unwrap_or(0.0), d).0 - }; - let velocity = if area > 0.0 { q / area } else { 0.0 }; - let capacity = full_flow_capacity(n, slope, d, k); - PipeSizeResult { - diameter_ft: d, - velocity, - pct_full: if capacity > 0.0 { q / capacity } else { 0.0 }, - normal_depth: yn, - surcharged, - outcome: SizeOutcome::NoSolution, - } -} - -/// Check whether an existing diameter meets criteria (no upsizing). -pub fn check_pipe(q: f64, slope: f64, n: f64, diameter_ft: f64, criteria: &DesignCriteria) -> PipeSizeResult { - if let Some(mut r) = evaluate_diameter(q, slope, n, diameter_ft, criteria) { - r.outcome = SizeOutcome::Adequate; - r - } else { - size_pipe_for_flow(q, slope, n, criteria) - } -} - -fn format_diameter_in(d_ft: f64) -> String { - let inches = (d_ft * 12.0).round() as i32; - format!("{inches}\"") -} - -fn recommend_for_pipe(p: &Pipe, design_q: f64, slope: f64, criteria: &DesignCriteria) -> PipeSizeRecommendation { - let current = p.diameter; - let check = check_pipe(design_q, slope, p.n, current, criteria); - let sized = size_pipe_for_flow(design_q, slope, p.n, criteria); - - let meets = check.outcome == SizeOutcome::Adequate - && (check.diameter_ft - current).abs() < 1e-6 - && !check.surcharged; - - let (recommended, outcome, note) = if meets { - ( - current, - SizeOutcome::Adequate, - format!("{} meets criteria ({:.1}% full, {:.2} ft/s)", p.id, check.pct_full * 100.0, check.velocity), - ) - } else if sized.outcome == SizeOutcome::NoSolution { - ( - sized.diameter_ft, - SizeOutcome::NoSolution, - format!( - "{}: no catalog pipe meets criteria (Q={:.2} cfs, S={:.4}); largest tried {} still surcharged={}", - p.id, - design_q, - slope, - format_diameter_in(sized.diameter_ft), - sized.surcharged - ), - ) - } else { - ( - sized.diameter_ft, - SizeOutcome::Sized, - format!( - "{}: upsize {} → {} ({:.1}% full, {:.2} ft/s)", - p.id, - format_diameter_in(current), - format_diameter_in(sized.diameter_ft), - sized.pct_full * 100.0, - sized.velocity - ), - ) - }; - - PipeSizeRecommendation { - pipe_id: p.id.clone(), - design_q, - slope, - current_diameter_ft: current, - recommended_diameter_ft: recommended, - meets_criteria: meets, - velocity: if meets { check.velocity } else { sized.velocity }, - pct_full: if meets { check.pct_full } else { sized.pct_full }, - surcharged: if meets { check.surcharged } else { sized.surcharged }, - outcome, - note, - } -} - -/// Size every pipe in `net` using flows from a completed [`Analysis`]. -pub fn size_network(net: &Network, analysis: &Analysis, criteria: &DesignCriteria) -> Vec { - net.pipes - .iter() - .map(|p| { - let pr = analysis.pipes.iter().find(|r| r.id == p.id); - let (q, slope) = pr.map(|r| (r.design_q, r.slope)).unwrap_or((0.0, 0.0)); - recommend_for_pipe(p, q, slope, criteria) - }) - .collect() -} - -/// Apply recommended diameters to a network (returns a cloned, sized network). -pub fn apply_sizing(net: &Network, recs: &[PipeSizeRecommendation]) -> Network { - let mut sized = net.clone(); - for (p, r) in sized.pipes.iter_mut().zip(recs.iter()) { - if r.outcome != SizeOutcome::NoSolution { - p.diameter = r.recommended_diameter_ft; - } - } - sized -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::network::{Network, Node}; - - fn heavy_net() -> Network { - Network { - nodes: vec![ - Node::inlet("N1", 100.0, 105.0, 2.0, 0.7), - Node::inlet("N2", 99.0, 104.0, 3.0, 0.8), - Node::outfall("OUT", 98.0, 103.0), - ], - pipes: vec![ - Pipe::new("P1", "N1", "N2", 100.0, 1.5, 0.013), - Pipe::new("P2", "N2", "OUT", 100.0, 1.5, 0.013), - ], - } - } - - #[test] - fn adequately_sized_pipe_is_adequate() { - let net = heavy_net(); - let pipes = net.analyze_rational(2.0).unwrap(); - let p2_q = pipes.iter().find(|x| x.id == "P2").unwrap().design_q; - let criteria = DesignCriteria::default(); - let recs = size_network(&net, &Analysis { pipes, nodes: vec![] }, &criteria); - let p2r = recs.iter().find(|r| r.pipe_id == "P2").unwrap(); - assert!(p2r.meets_criteria || p2r.recommended_diameter_ft >= 1.5); - assert!(p2_q > 0.0); - } - - #[test] - fn undersized_pipe_gets_larger_recommendation() { - let net = heavy_net(); - let pipes = net.analyze_rational(4.0).unwrap(); - assert!(pipes.iter().find(|x| x.id == "P2").unwrap().surcharged); - let criteria = DesignCriteria::default(); - let recs = size_network(&net, &Analysis { pipes, nodes: vec![] }, &criteria); - let p2r = recs.iter().find(|r| r.pipe_id == "P2").unwrap(); - assert!(p2r.recommended_diameter_ft > 1.5, "got {}", p2r.recommended_diameter_ft); - assert_eq!(p2r.outcome, SizeOutcome::Sized); - } - - #[test] - fn apply_sizing_updates_diameters() { - let net = heavy_net(); - let pipes = net.analyze_rational(4.0).unwrap(); - let criteria = DesignCriteria::default(); - let recs = size_network(&net, &Analysis { pipes, nodes: vec![] }, &criteria); - let sized = apply_sizing(&net, &recs); - let p2 = sized.pipes.iter().find(|p| p.id == "P2").unwrap(); - assert!(p2.diameter > 1.5); - } - - #[test] - fn sample_network_has_catalog_solutions() { - let text = include_str!("../../examples/sample.ssn"); - let parsed = crate::parse::parse_ssn(text).unwrap(); - let a = parsed.network.analyze(&parsed.idf, &parsed.options).unwrap(); - assert!(a.pipes.iter().all(|p| !p.surcharged), "sample should not surcharge"); - let recs = size_network(&parsed.network, &a, &DesignCriteria::default()); - assert!( - recs.iter().all(|r| r.outcome != SizeOutcome::NoSolution), - "unexpected: {:?}", - recs.iter().map(|r| (&r.pipe_id, &r.note)).collect::>() - ); - } -} \ No newline at end of file diff --git a/crates/stormsewer/src/drawing.rs b/crates/stormsewer/src/drawing.rs deleted file mode 100644 index 86c7c3e51..000000000 --- a/crates/stormsewer/src/drawing.rs +++ /dev/null @@ -1,288 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -//! Convert an analyzed network into drawable primitives (CAD-agnostic). -//! -//! Produces a **plan** view (pipes as lines, structures as markers, flow/HGL -//! labels) and a **profile** (HGL long-section of the main stem: ground, -//! invert, and hydraulic-grade-line polylines). Coordinates are plain `f64` -//! drawing units; the host CAD turns these into its own entities. - -use crate::network::{Analysis, Network, NodeKind}; -use std::collections::HashMap; - -/// A pipe drawn in plan. -#[derive(Clone, Debug)] -pub struct PlanPipe { - pub x1: f64, - pub y1: f64, - pub x2: f64, - pub y2: f64, - pub surcharged: bool, -} - -/// A structure marker drawn in plan. -#[derive(Clone, Debug)] -pub struct PlanNode { - pub x: f64, - pub y: f64, - pub radius: f64, - pub kind: NodeKind, -} - -/// A text label placed at a point. -#[derive(Clone, Debug)] -pub struct Label { - pub x: f64, - pub y: f64, - pub text: String, - pub height: f64, -} - -/// Which line of the profile a polyline represents. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum ProfileRole { - Ground, - Invert, - Hgl, -} - -/// A polyline (sequence of points) with a role. -#[derive(Clone, Debug)] -pub struct Polyline { - pub pts: Vec<(f64, f64)>, - pub role: ProfileRole, -} - -/// The full set of primitives for a network drawing. -#[derive(Clone, Debug, Default)] -pub struct NetworkDrawing { - pub plan_pipes: Vec, - pub plan_nodes: Vec, - pub plan_labels: Vec