forked from 1b-agent/opencadstudio-upstream
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:
1 parent
99b7ab1176
commit
9b44eb6400
26 files changed
+2007
No files matched your search
@@ -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)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user