Add Storm Sewer module: storm-drain network analysis (Rational + Manning + HGL)

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) <noreply@anthropic.com>
This commit is contained in:
Michael FlynnandClaude Opus 4.8 committed 2026-06-08 17:16:30 -04:00
1 parent 99b7ab1176
commit 9b44eb6400
26 files changed
+2007

No files matched your search

+129
View File
@@ -0,0 +1,129 @@
// Interactive storm-sewer drafting commands: place structure markers and draw
// pipe runs on the canvas.
//
// These create geometry (circle markers, pipe lines). Attaching hydraulic data
// (invert, rim, area, C, diameter) to placed structures — so SS_ANALYZE reads
// the drawn network instead of the built-in sample — is the next step; see
// INTEGRATION.md.
use acadrust::types::Vector3;
use acadrust::{Circle, EntityType, Line};
use glam::Vec3;
use crate::command::{CadCommand, CmdResult};
fn v3(p: Vec3) -> Vector3 {
Vector3::new(p.x as f64, p.y as f64, 0.0)
}
/// Repeatedly place a structure marker (a circle) at picked points.
pub struct PlaceStructure {
label: &'static str,
radius: f64,
}
impl PlaceStructure {
pub fn inlet() -> Self {
Self { label: "inlet", radius: 3.0 }
}
pub fn junction() -> Self {
Self { label: "junction", radius: 4.0 }
}
pub fn outfall() -> Self {
Self { label: "outfall", radius: 6.0 }
}
}
impl CadCommand for PlaceStructure {
fn name(&self) -> &'static str {
"SS_STRUCTURE"
}
fn prompt(&self) -> String {
format!("Storm sewer: pick {} location [Esc = done]", self.label)
}
fn on_point(&mut self, pt: Vec3) -> CmdResult {
let marker = Circle { center: v3(pt), radius: self.radius, ..Default::default() };
// CommitEntity keeps the command active so several can be dropped.
CmdResult::CommitEntity(EntityType::Circle(marker))
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
/// Draw pipe runs as connected line segments (chains like LINE).
pub struct PlacePipe {
last: Option<Vec3>,
}
impl PlacePipe {
pub fn new() -> Self {
Self { last: None }
}
}
impl Default for PlacePipe {
fn default() -> Self {
Self::new()
}
}
impl CadCommand for PlacePipe {
fn name(&self) -> &'static str {
"SS_PIPE"
}
fn prompt(&self) -> String {
if self.last.is_none() {
"Storm sewer pipe: pick start structure".into()
} else {
"Storm sewer pipe: pick next structure [Esc = done]".into()
}
}
fn on_point(&mut self, pt: Vec3) -> CmdResult {
match self.last {
None => {
self.last = Some(pt);
CmdResult::NeedPoint
}
Some(prev) => {
let pipe = Line::from_points(v3(prev), v3(pt));
self.last = Some(pt);
CmdResult::CommitEntity(EntityType::Line(pipe))
}
}
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn structure_commits_a_circle_and_stays_active() {
let mut cmd = PlaceStructure::inlet();
match cmd.on_point(Vec3::new(10.0, 20.0, 0.0)) {
CmdResult::CommitEntity(EntityType::Circle(c)) => {
assert_eq!(c.center.x, 10.0);
assert_eq!(c.center.y, 20.0);
assert!(c.radius > 0.0);
}
_ => panic!("expected CommitEntity(Circle)"),
}
}
#[test]
fn pipe_needs_two_points_then_commits_a_line() {
let mut cmd = PlacePipe::new();
assert!(matches!(cmd.on_point(Vec3::new(0.0, 0.0, 0.0)), CmdResult::NeedPoint));
match cmd.on_point(Vec3::new(100.0, 0.0, 0.0)) {
CmdResult::CommitEntity(EntityType::Line(l)) => {
assert_eq!(l.start.x, 0.0);
assert_eq!(l.end.x, 100.0);
}
_ => panic!("expected CommitEntity(Line)"),
}
}
}