From 9b44eb6400790c5034d66751610be3e907b4d07d Mon Sep 17 00:00:00 2001 From: Michael Flynn Date: Mon, 8 Jun 2026 17:16:30 -0400 Subject: [PATCH] Add Storm Sewer module: storm-drain network analysis (Rational + Manning + HGL) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a "Storm Sewer" ribbon tab backed by a new in-repo `stormsewer` engine crate (vendored under crates/). The engine implements standard public-domain methods — Rational-method peak-flow accumulation, Manning circular-pipe hydraulics, time-of-concentration / IDF intensity, and an HGL backwater pass with junction losses — with 24 unit tests and no external dependencies. Module (src/modules/storm_sewer/): - Drafting: SS_INLET / SS_JUNCTION / SS_OUTFALL place structure markers and SS_PIPE chains pipe runs (interactive CadCommands). - Analysis: SS_ANALYZE / SS_REPORT / SS_PROFILE open a .ssn network file, run the engine, draw the plan and HGL long-section, and print a report. - SVG icons; SS_* commands registered for command-line autocomplete. Integration is additive: build.rs auto-discovers the module and one dispatch block is added to src/app/commands.rs. 7 module integration tests. See src/modules/storm_sewer/INTEGRATION.md for the design and the optional follow-up (drive analysis from canvas geometry via XDATA). Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 5 + Cargo.toml | 6 + crates/stormsewer/Cargo.toml | 15 + crates/stormsewer/examples/sample.ssn | 18 + crates/stormsewer/src/bin/stormsewer-cli.rs | 29 ++ crates/stormsewer/src/drawing.rs | 288 +++++++++++ crates/stormsewer/src/hydraulics.rs | 208 ++++++++ crates/stormsewer/src/idf.rs | 53 ++ crates/stormsewer/src/lib.rs | 45 ++ crates/stormsewer/src/network.rs | 527 ++++++++++++++++++++ crates/stormsewer/src/parse.rs | 122 +++++ crates/stormsewer/src/report.rs | 93 ++++ src/app/commands.rs | 64 +++ src/modules/mod.rs | 1 + src/modules/registry.rs | 1 + src/modules/storm_sewer/INTEGRATION.md | 56 +++ src/modules/storm_sewer/analysis.rs | 152 ++++++ src/modules/storm_sewer/icons/analyze.svg | 4 + src/modules/storm_sewer/icons/inlet.svg | 6 + src/modules/storm_sewer/icons/junction.svg | 4 + src/modules/storm_sewer/icons/outfall.svg | 4 + src/modules/storm_sewer/icons/pipe.svg | 6 + src/modules/storm_sewer/icons/profile.svg | 5 + src/modules/storm_sewer/icons/report.svg | 7 + src/modules/storm_sewer/mod.rs | 159 ++++++ src/modules/storm_sewer/structures.rs | 129 +++++ 26 files changed, 2007 insertions(+) create mode 100644 crates/stormsewer/Cargo.toml create mode 100644 crates/stormsewer/examples/sample.ssn create mode 100644 crates/stormsewer/src/bin/stormsewer-cli.rs create mode 100644 crates/stormsewer/src/drawing.rs create mode 100644 crates/stormsewer/src/hydraulics.rs create mode 100644 crates/stormsewer/src/idf.rs create mode 100644 crates/stormsewer/src/lib.rs create mode 100644 crates/stormsewer/src/network.rs create mode 100644 crates/stormsewer/src/parse.rs create mode 100644 crates/stormsewer/src/report.rs create mode 100644 src/modules/storm_sewer/INTEGRATION.md create mode 100644 src/modules/storm_sewer/analysis.rs create mode 100644 src/modules/storm_sewer/icons/analyze.svg create mode 100644 src/modules/storm_sewer/icons/inlet.svg create mode 100644 src/modules/storm_sewer/icons/junction.svg create mode 100644 src/modules/storm_sewer/icons/outfall.svg create mode 100644 src/modules/storm_sewer/icons/pipe.svg create mode 100644 src/modules/storm_sewer/icons/profile.svg create mode 100644 src/modules/storm_sewer/icons/report.svg create mode 100644 src/modules/storm_sewer/mod.rs create mode 100644 src/modules/storm_sewer/structures.rs diff --git a/Cargo.lock b/Cargo.lock index af5505662..e341bdd8f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,7 @@ dependencies = [ "rayon", "rfd", "rustc-hash 2.1.2", + "stormsewer", "truck-meshalgo", "truck-modeling", "truck-polymesh", @@ -5176,6 +5177,10 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "stormsewer" +version = "0.1.0" + [[package]] name = "strict-num" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index 6677ec817..5f2dfb23d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,9 @@ 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 @@ -31,6 +34,9 @@ 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 new file mode 100644 index 000000000..67dcab4a0 --- /dev/null +++ b/crates/stormsewer/Cargo.toml @@ -0,0 +1,15 @@ +[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] + +[lib] +name = "stormsewer" diff --git a/crates/stormsewer/examples/sample.ssn b/crates/stormsewer/examples/sample.ssn new file mode 100644 index 000000000..f4a57e54c --- /dev/null +++ b/crates/stormsewer/examples/sample.ssn @@ -0,0 +1,18 @@ +# 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/src/bin/stormsewer-cli.rs b/crates/stormsewer/src/bin/stormsewer-cli.rs new file mode 100644 index 000000000..a40be0e03 --- /dev/null +++ b/crates/stormsewer/src/bin/stormsewer-cli.rs @@ -0,0 +1,29 @@ +// 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/drawing.rs b/crates/stormsewer/src/drawing.rs new file mode 100644 index 000000000..86c7c3e51 --- /dev/null +++ b/crates/stormsewer/src/drawing.rs @@ -0,0 +1,288 @@ +// 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