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