Skip to main content

squid_n_core/model/
member_detail.rs

1//! 部材の付帯情報(ハンチ・継手位置)。
2//!
3//! - [`Haunch`] — 部材端部のハンチ(1 端分の形状)。
4//! - [`JointKind`] — 継手の種別(現場継手/工場継手)。
5//! - [`MemberJoint`] — 継手 1 箇所(部材軸上の位置)。
6//! - [`MemberDetailAttr`] — 部材 1 本分の付帯情報(`Model::member_detail_attrs`)。
7//!
8//! いずれも**剛性・応力解析には影響しない**(設計書 §6.2 の「ハンチ/テーパーの
9//! 剛性は一様断面として無視する(慣用)」の方針どおり、剛性は基準断面のまま)。
10//! 断面算定の検定位置の追加(ハンチ端・継手位置。§6.2.3「位置はユーザが追加・
11//! 変更可能」)と、数量拾い・製作情報の保持に用いる。
12
13use super::*;
14
15/// 部材端部のハンチ(1 端分)。剛性には影響しない付帯情報。
16///
17/// 鉛直ハンチ(せい増分)・水平ハンチ(幅増分)の双方を表現できる。
18/// 寸法は数量拾い(増打ちコンクリート量・鉄骨重量等)の基礎データとして保持し、
19/// 設計上は「ハンチ端」(フェースから `length` だけ内側)が基準断面の始まりと
20/// なるため検定位置に追加する。
21#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
22pub struct Haunch {
23    /// ハンチ長 [mm](柱フェース(`RigidZone::face_i/j`)から部材内側へ測る)。
24    pub length: f64,
25    /// せい増分 [mm](鉛直ハンチ。フェース位置での基準断面せいへの増分。0=なし)。
26    #[serde(default)]
27    pub depth_increase: f64,
28    /// 幅増分 [mm](水平ハンチ。フェース位置での基準断面幅への増分。0=なし)。
29    #[serde(default)]
30    pub width_increase: f64,
31}
32
33/// 継手の種別(数量拾い・工程区分用)。
34#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
35pub enum JointKind {
36    /// 現場継手(建方時の接合。既定)。
37    #[default]
38    Site,
39    /// 工場継手(工場溶接等)。
40    Shop,
41}
42
43/// 継手(ジョイント)1 箇所。剛性には影響しない付帯情報。
44///
45/// 位置は始端(i 端)節点芯からの距離で保持する。設計上は継手位置の内力で
46/// 継手部の検定(継手部の断面欠損は [`SteelDesignAttr`] の欠損率を使用)を
47/// 行うため検定位置に追加し、数量上は継手箇所数・種別の集計に用いる。
48#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
49pub struct MemberJoint {
50    /// 始端(i 端)節点芯からの距離 [mm]。
51    pub distance: f64,
52    /// 継手の種別(現場/工場)。
53    #[serde(default)]
54    pub kind: JointKind,
55}
56
57/// 部材 1 本分の付帯情報(ハンチ・継手位置)。`Model::member_detail_attrs` に
58/// 要素 ID と対で保持する側テーブル属性。剛性・応力解析には影響しない。
59#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
60pub struct MemberDetailAttr {
61    pub elem: ElemId,
62    /// 始端(i 端)のハンチ。`None` はハンチなし。
63    #[serde(default)]
64    pub haunch_i: Option<Haunch>,
65    /// 終端(j 端)のハンチ。`None` はハンチなし。
66    #[serde(default)]
67    pub haunch_j: Option<Haunch>,
68    /// 継手の一覧(位置順でなくてよい)。
69    #[serde(default)]
70    pub joints: Vec<MemberJoint>,
71}
72
73impl MemberDetailAttr {
74    /// 空の付帯情報(ハンチなし・継手なし)を作る。
75    pub fn new(elem: ElemId) -> Self {
76        Self {
77            elem,
78            haunch_i: None,
79            haunch_j: None,
80            joints: Vec::new(),
81        }
82    }
83
84    /// ハンチも継手も持たない(=側テーブルから削除してよい)か。
85    pub fn is_empty(&self) -> bool {
86        self.haunch_i.is_none() && self.haunch_j.is_none() && self.joints.is_empty()
87    }
88
89    /// 付帯情報が追加する検定位置(正規化座標 \[0,1\])を算定する。
90    ///
91    /// - ハンチ端: フェース距離+ハンチ長を正規化し、既定の危険断面位置
92    ///   (§6.2.3、`design_positions` / `eval_sections`)と同じ規則で
93    ///   i 端側は \[0.0, 0.5)、j 端側は (0.5, 1.0\] へクランプする。
94    /// - 継手: 始端節点芯からの距離を正規化する。部材長の外側(0 以下・
95    ///   部材長以上)の値は不正入力として黙って除外する。
96    ///
97    /// 応力の評価断面(`squid_n_element` の `eval_sections`)と検定位置
98    /// (squid-n-app / squid-n-mcp の `design_positions`)の双方から呼ばれる
99    /// 共通実装。両者の位置一致判定(1e-6)はこの単一実装により保証される。
100    /// `geom_len` が 0 以下(不正な幾何)のときは空を返す。
101    pub fn extra_check_positions(&self, rigid_zone: &RigidZone, geom_len: f64) -> Vec<f64> {
102        if geom_len <= 1e-12 {
103            return Vec::new();
104        }
105        let mut xs = Vec::new();
106        if let Some(h) = &self.haunch_i {
107            if h.length > 0.0 {
108                // ハンチ長は柱フェースから測る。フェース距離が未算定の端は
109                // 節点位置から測る(従来の 0 と同じ扱い)。
110                let base = rigid_zone.face_i_or_zero();
111                xs.push(((base + h.length) / geom_len).clamp(0.0, 0.5 - 1e-9));
112            }
113        }
114        if let Some(h) = &self.haunch_j {
115            if h.length > 0.0 {
116                let base = rigid_zone.face_j_or_zero();
117                xs.push((1.0 - (base + h.length) / geom_len).clamp(0.5 + 1e-9, 1.0));
118            }
119        }
120        for j in &self.joints {
121            let xi = j.distance / geom_len;
122            if xi > 0.0 && xi < 1.0 {
123                xs.push(xi);
124            }
125        }
126        xs
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    fn rigid(face_i: f64, face_j: f64) -> RigidZone {
135        RigidZone {
136            face_i: Some(face_i),
137            face_j: Some(face_j),
138            ..Default::default()
139        }
140    }
141
142    /// ハンチ端は「フェース+ハンチ長」の正規化位置になる(i 端は [0,0.5)、
143    /// j 端は (0.5,1] へクランプ)。継手は始端からの距離の正規化位置になる。
144    #[test]
145    fn test_extra_check_positions() {
146        let attr = MemberDetailAttr {
147            elem: ElemId(0),
148            haunch_i: Some(Haunch {
149                length: 700.0,
150                depth_increase: 200.0,
151                width_increase: 0.0,
152            }),
153            haunch_j: Some(Haunch {
154                length: 500.0,
155                depth_increase: 200.0,
156                width_increase: 0.0,
157            }),
158            joints: vec![MemberJoint {
159                distance: 1000.0,
160                kind: JointKind::Site,
161            }],
162        };
163        let xs = attr.extra_check_positions(&rigid(300.0, 250.0), 4000.0);
164        // i 端ハンチ端 (300+700)/4000 = 0.25、j 端ハンチ端 1-(250+500)/4000 = 0.8125、
165        // 継手 1000/4000 = 0.25(ハンチ端との重複は評価断面側の dedup に任せる)
166        assert_eq!(xs.len(), 3);
167        assert!((xs[0] - 0.25).abs() < 1e-12);
168        assert!((xs[1] - 0.8125).abs() < 1e-12);
169        assert!((xs[2] - 0.25).abs() < 1e-12);
170    }
171
172    /// 部材長の外の継手位置・長さ 0 のハンチは黙って除外される。
173    /// 幾何長 0 では空を返す。
174    #[test]
175    fn test_extra_check_positions_invalid_input() {
176        let attr = MemberDetailAttr {
177            elem: ElemId(0),
178            haunch_i: Some(Haunch {
179                length: 0.0,
180                depth_increase: 100.0,
181                width_increase: 0.0,
182            }),
183            haunch_j: None,
184            joints: vec![
185                MemberJoint {
186                    distance: 0.0,
187                    kind: JointKind::Site,
188                },
189                MemberJoint {
190                    distance: 5000.0,
191                    kind: JointKind::Shop,
192                },
193            ],
194        };
195        assert!(attr
196            .extra_check_positions(&rigid(300.0, 300.0), 4000.0)
197            .is_empty());
198        assert!(attr
199            .extra_check_positions(&rigid(300.0, 300.0), 0.0)
200            .is_empty());
201    }
202
203    /// 過大なハンチ長でも i 端は [0,0.5)、j 端は (0.5,1] を越えない
204    /// (既定の危険断面位置と同じクランプ規則)。
205    #[test]
206    fn test_extra_check_positions_clamped() {
207        let attr = MemberDetailAttr {
208            elem: ElemId(0),
209            haunch_i: Some(Haunch {
210                length: 3000.0,
211                depth_increase: 0.0,
212                width_increase: 0.0,
213            }),
214            haunch_j: Some(Haunch {
215                length: 3000.0,
216                depth_increase: 0.0,
217                width_increase: 0.0,
218            }),
219            joints: Vec::new(),
220        };
221        let xs = attr.extra_check_positions(&rigid(0.0, 0.0), 4000.0);
222        assert_eq!(xs.len(), 2);
223        assert!(xs[0] < 0.5);
224        assert!(xs[1] > 0.5);
225    }
226
227    /// is_empty はハンチ・継手のいずれもないときだけ真。
228    #[test]
229    fn test_is_empty() {
230        let mut attr = MemberDetailAttr::new(ElemId(1));
231        assert!(attr.is_empty());
232        attr.joints.push(MemberJoint {
233            distance: 100.0,
234            kind: JointKind::Shop,
235        });
236        assert!(!attr.is_empty());
237    }
238}