Skip to main content

squid_n_core/model/
slab.rs

1//! 床板(スラブ)。
2//!
3//! 床領域([`FloorRegion`]、[`super::region`])は大梁の 1 スパン区画そのもので、
4//! 版の仕様は持たない。版の仕様(厚さ・材料・仕上げ荷重・室用途)は本モジュールの
5//! [`Slab`] が持つ。1 つの床領域は、床領域内が小梁でさらに細かい打設単位に
6//! 分かれていれば複数の [`Slab`] を持ちうる([`FloorRegion::slab_ids`])。
7//! 片持ちスラブ・バルコニー・出隅はどの床領域からも参照されない、独立した [`Slab`]
8//! として存在する。
9//!
10//! - [`Slab`] — 床板本体(形状+版仕様)。
11//! - [`SlabShape`] — 床板の形(大梁または小梁で囲まれた領域 / 主架構に取り付く領域)。
12//! - [`SlabPlate`] — 版の仕様(断面・仕上げ荷重・室用途・分配方法)。
13//! - [`DistributionMethod`] — 床荷重の分配方法。
14//! - [`AreaLoad`] — 面荷重。
15//! - [`OneWayDir`] — 一方向スラブの伝達方向。
16//! - [`LoadPurpose`] — 積載荷重の用途(床用/骨組用/地震用。令85条1項)。
17//! - [`SlabUsage`] — 室用途(令別表第1 の積載荷重プリセット)。
18
19use super::*;
20
21/// 床板の版(仕様)。
22#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
23pub struct SlabPlate {
24    /// スラブ断面(符号・板厚・コンクリート材料)。
25    ///
26    /// **板厚と自重はこの断面から解決する**([`Model::slab_plate_thickness`]・
27    /// [`Model::slab_self_weight_intensity`])。板厚を床板と断面の両方に持たせると
28    /// 同じ数値の持ち主が 2 つになるため、床板側は断面を指すだけとする。
29    ///
30    /// `None` は未割当。板厚も自重も定まらないため、解析前チェックが止める
31    /// (もっともらしい既定厚で補うと、床の自重が過小なまま長期応力が出る)。
32    #[serde(default)]
33    pub section: Option<crate::ids::SectionId>,
34    /// 仕上げ等の面荷重。
35    #[serde(default)]
36    pub loads: Vec<AreaLoad>,
37    /// 室用途(令別表第1)。`Some` のとき積載荷重(LL)を用途別に自動算定する。
38    /// `None` は積載荷重を持たない(`loads` の固定荷重のみ)。
39    #[serde(default)]
40    pub usage: Option<SlabUsage>,
41    /// 床荷重の分配方法。
42    pub method: DistributionMethod,
43    /// 一方向スラブの伝達方向。`None` は境界辺 0・2 が負担する既定。
44    #[serde(default)]
45    pub one_way: Option<OneWayDir>,
46}
47
48impl SlabPlate {
49    /// 仕上げ等の面荷重強度 [N/mm²](`loads` の合算)。**版自身の自重は含まない。**
50    pub fn finish_intensity(&self) -> f64 {
51        self.loads.iter().map(|l| l.value).sum()
52    }
53
54    /// 用途別の積載荷重(LL)の面荷重強度 [N/mm²]。`usage` 未設定なら 0。
55    pub fn live_intensity(&self, purpose: LoadPurpose) -> f64 {
56        self.usage.map(|u| u.live_load(purpose)).unwrap_or(0.0)
57    }
58}
59
60/// 床板の形。
61#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
62pub enum SlabShape {
63    /// 大梁または小梁で囲まれた領域。境界は反時計回りの節点列(始点は繰り返さない)。
64    Enclosed { boundary: Vec<NodeId> },
65    /// 主架構に取り付く領域(片持ち・バルコニー・出隅)。
66    Attached {
67        /// 取付き先。荷重の出口は取付き先の種類ごとに決まる([`RegionAnchor`] 参照)。
68        anchor: RegionAnchor,
69        /// 張り出し量 [mm]。意味は取付き先の種類で決まる([`RegionAnchor`] 参照)。
70        extent: [f64; 2],
71    },
72}
73
74/// 床板。大梁または小梁で囲まれた版、または主架構に取り付く版
75/// (片持ち・バルコニー・出隅)ごとに 1 つ。
76#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
77pub struct Slab {
78    /// 床板 ID(`Model::slabs` の配列インデックスと一致すること)。
79    pub id: SlabId,
80    pub shape: SlabShape,
81    pub plate: SlabPlate,
82}
83
84impl Slab {
85    /// 取り付く床板か。
86    pub fn is_attached(&self) -> bool {
87        matches!(self.shape, SlabShape::Attached { .. })
88    }
89
90    /// 境界の節点列。**大梁または小梁で囲まれた床板のみ**(取り付く床板は
91    /// 自由端に節点を持たないため `None`)。
92    pub fn boundary_nodes(&self) -> Option<&[NodeId]> {
93        match &self.shape {
94            SlabShape::Enclosed { boundary } => Some(boundary),
95            SlabShape::Attached { .. } => None,
96        }
97    }
98
99    /// 境界多角形の座標列 [mm]。取り付く床板は取付き先と張り出し量から算出する。
100    /// 節点が引けない(陳腐化した参照)場合は `None`。
101    pub fn boundary_coords(&self, model: &Model) -> Option<Vec<[f64; 3]>> {
102        self.boundary_coords_with(|n| model.nodes.get(n.index()).map(|n| n.coord))
103    }
104
105    /// 境界多角形の座標列 [mm] を、節点座標の引き方を差し替えて求める。
106    ///
107    /// [`Slab::boundary_coords`] はモデルの節点座標をそのまま使うが、変形図・
108    /// モード形・時刻歴は**変形後の節点座標**で描く必要がある。形状の組み立て方
109    /// (取付き線の内挿・左向き法線への張り出し)は同じなので、座標の引き方だけを
110    /// 差し替えられるようにして実装を 1 つに保つ。
111    pub fn boundary_coords_with(
112        &self,
113        coord_of: impl Fn(NodeId) -> Option<[f64; 3]>,
114    ) -> Option<Vec<[f64; 3]>> {
115        match &self.shape {
116            SlabShape::Enclosed { boundary } => boundary.iter().map(|n| coord_of(*n)).collect(),
117            SlabShape::Attached { anchor, extent } => match anchor {
118                RegionAnchor::Line { nodes, span, .. } => {
119                    let a = coord_of(nodes[0])?;
120                    let b = coord_of(nodes[1])?;
121                    let lerp = |t: f64| {
122                        [
123                            a[0] + (b[0] - a[0]) * t,
124                            a[1] + (b[1] - a[1]) * t,
125                            a[2] + (b[2] - a[2]) * t,
126                        ]
127                    };
128                    let p0 = lerp(span[0]);
129                    let p1 = lerp(span[1]);
130                    let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
131                    let len = (dx * dx + dy * dy).sqrt();
132                    if len <= f64::EPSILON {
133                        return None;
134                    }
135                    // 取付き線の左向き単位法線(符号つき張り出し量の正の向き)。
136                    let n = [-dy / len, dx / len];
137                    Some(vec![
138                        p0,
139                        p1,
140                        [p1[0] + n[0] * extent[1], p1[1] + n[1] * extent[1], p1[2]],
141                        [p0[0] + n[0] * extent[0], p0[1] + n[1] * extent[0], p0[2]],
142                    ])
143                }
144                RegionAnchor::Point(nid) => {
145                    let p = coord_of(*nid)?;
146                    Some(vec![
147                        p,
148                        [p[0] + extent[0], p[1], p[2]],
149                        [p[0] + extent[0], p[1] + extent[1], p[2]],
150                        [p[0], p[1] + extent[1], p[2]],
151                    ])
152                }
153                // 床板の取付き先には使わない(`RegionAnchor::FloorRegion` のドキュメント
154                // 参照。壁側〔`WallPlate` の `Attached` 形〕専用のアンカーであり、
155                // 床板では到達しない)。
156                RegionAnchor::FloorRegion { .. } => None,
157            },
158        }
159    }
160
161    /// 境界の辺 `k` の両端節点。荷重分配の結果(`LoadTarget::Edge`)を主架構の梁へ
162    /// 結びつけるために使う。
163    ///
164    /// 大梁または小梁で囲まれた床板は `boundary[k]`→`boundary[k+1]`、取り付く床板
165    /// (線)は辺 0 が取付き線そのもの(それ以外の辺は自由端なので `None`)。
166    /// 取り付く床板(点)は辺を持たない。
167    pub fn edge_nodes(&self, k: usize) -> Option<[NodeId; 2]> {
168        match &self.shape {
169            SlabShape::Enclosed { boundary } => {
170                let n = boundary.len();
171                (n >= 3 && k < n).then(|| [boundary[k], boundary[(k + 1) % n]])
172            }
173            SlabShape::Attached { anchor, .. } => match anchor {
174                RegionAnchor::Line { nodes, .. } if k == 0 => Some(*nodes),
175                RegionAnchor::Line { .. } | RegionAnchor::Point(_) => None,
176                // 床板では到達しない(`boundary_coords` と同じ理由)。
177                RegionAnchor::FloorRegion { .. } => None,
178            },
179        }
180    }
181
182    /// 床板を代表する節点。大梁または小梁で囲まれた床板は境界の先頭、
183    /// 取り付く床板は取付き先の節点。
184    pub fn reference_node(&self) -> Option<NodeId> {
185        match &self.shape {
186            SlabShape::Enclosed { boundary } => boundary.first().copied(),
187            SlabShape::Attached { anchor, .. } => match anchor {
188                RegionAnchor::Line { nodes, .. } => Some(nodes[0]),
189                RegionAnchor::Point(n) => Some(*n),
190                // 床板では到達しない(`boundary_coords` と同じ理由)。
191                RegionAnchor::FloorRegion { .. } => None,
192            },
193        }
194    }
195
196    /// 床板のレベル Z [mm](境界座標の Z の平均)。境界が引けなければ `None`。
197    pub fn level(&self, model: &Model) -> Option<f64> {
198        let coords = self.boundary_coords(model)?;
199        if coords.is_empty() {
200            return None;
201        }
202        Some(coords.iter().map(|c| c[2]).sum::<f64>() / coords.len() as f64)
203    }
204
205    /// 分配方法。
206    pub fn method(&self) -> DistributionMethod {
207        self.plate.method
208    }
209
210    /// 一方向スラブの伝達方向。
211    pub fn one_way(&self) -> Option<OneWayDir> {
212        self.plate.one_way
213    }
214
215    /// 用途別の積載荷重(LL)の面荷重強度 [N/mm²]。用途未設定は 0。
216    pub fn live_intensity(&self, purpose: LoadPurpose) -> f64 {
217        self.plate.live_intensity(purpose)
218    }
219
220    /// 版の断面(未割当は `None`)。
221    pub fn section(&self) -> Option<crate::ids::SectionId> {
222        self.plate.section
223    }
224
225    /// 室用途(未設定は `None`)。
226    pub fn usage(&self) -> Option<SlabUsage> {
227        self.plate.usage
228    }
229
230    /// 取り付く床板の設計スパン [mm](張り出し量の絶対値の大きい方)。
231    /// 大梁または小梁で囲まれた床板、値が非有限、または 0 以下のときは `None`。
232    pub fn attached_design_span(&self) -> Option<f64> {
233        match &self.shape {
234            SlabShape::Enclosed { .. } => None,
235            SlabShape::Attached { extent, .. } => {
236                let span = extent[0].abs().max(extent[1].abs());
237                (span.is_finite() && span > 0.0).then_some(span)
238            }
239        }
240    }
241}
242
243impl Model {
244    /// 床板 ID から床板を引く。存在しなければ `None`。
245    pub fn slab(&self, id: SlabId) -> Option<&Slab> {
246        match self.slabs.get(id.index()) {
247            Some(s) if s.id == id => Some(s),
248            _ => self.slabs.iter().find(|s| s.id == id),
249        }
250    }
251
252    /// 床板の版へ割り当てた断面。未割当・ダングリングは `None`。
253    pub fn slab_section(&self, slab: &Slab) -> Option<&Section> {
254        slab.section()
255            .and_then(|sid| self.sections.get(sid.index()))
256    }
257
258    /// 版の板厚 [mm]。断面の [`Section::thickness`] をそのまま返す。
259    ///
260    /// 断面が未割当、または断面が板厚を持たない(板状でない形状を割り当てた)場合は `None`。
261    /// **建物一律の [`Model::slab_thickness`] へは退かない**。あちらは「剛性計算に見込む
262    /// スラブ厚」であり、既定の 0 は「スラブ協力幅による梁剛性増大を見込まない」を意味する
263    /// 別概念のためである。
264    pub fn slab_plate_thickness(&self, slab: &Slab) -> Option<f64> {
265        self.slab_section(slab)
266            .and_then(|s| s.thickness)
267            .filter(|t| *t > 0.0)
268    }
269
270    /// 版の自重の面荷重強度 [N/mm²](板厚 × 断面の主材料の単位体積重量)。
271    ///
272    /// 断面または断面の主材料が未割当のときは `None`。自重を面荷重として焼き込まず
273    /// 毎回算定するのは、板厚や材料を変えたときに自重が追随しないという食い違いを
274    /// 作らないためである。
275    pub fn slab_self_weight_intensity(&self, slab: &Slab) -> Option<f64> {
276        let t = self.slab_plate_thickness(slab)?;
277        let mat = self
278            .slab_section(slab)
279            .and_then(|s| s.material)
280            .and_then(|mid| self.materials.get(mid.index()))?;
281        Some(t * mat.density * crate::units::GRAVITY_MM_S2)
282    }
283
284    /// 固定荷重(DL)の面荷重強度 [N/mm²](版の自重 + 仕上げ等)。
285    ///
286    /// 自重が算定できない版(断面・主材料が未割当)は仕上げ分だけを返す。
287    /// 解析前チェックがこの状態を止めるため、ここでは既定厚で補わない。
288    pub fn slab_dead_intensity(&self, slab: &Slab) -> f64 {
289        self.slab_self_weight_intensity(slab).unwrap_or(0.0) + slab.plate.finish_intensity()
290    }
291
292    /// 用途に応じた合成面荷重強度 [N/mm²](固定 DL + 積載 LL(purpose))。
293    /// 長期骨組解析は `Frame`、地震用重量は `Seismic`、床・小梁設計は `Floor`。
294    pub fn slab_intensity(&self, slab: &Slab, purpose: LoadPurpose) -> f64 {
295        self.slab_dead_intensity(slab) + slab.plate.live_intensity(purpose)
296    }
297}
298
299/// 床荷重の分配方法。既定は三角形・台形分配。
300#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
301pub enum DistributionMethod {
302    #[default]
303    TriTrapezoid,
304    OneWay,
305    TributaryArea,
306}
307
308/// 積載荷重の用途(令85条1項・令別表第1 の 3 欄)。
309/// - `Floor`(床用): 床スラブ・小梁の設計用。最も大きい。
310/// - `Frame`(骨組用): 大梁・柱・基礎の設計用(長期骨組解析に用いる)。
311/// - `Seismic`(地震用): 地震力(地震用重量)の算定用。最も小さい。
312#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
313pub enum LoadPurpose {
314    Floor,
315    Frame,
316    Seismic,
317}
318
319/// 室の用途(積載荷重プリセット)。`live_load` で用途別の積載荷重 [N/mm²] を返す。
320/// `Custom` は 3 欄(床版・小梁計算用/大梁・柱・基礎計算用/地震力計算用)を
321/// 直接持つ(内部単位 N/mm²)。
322///
323/// 出典: 建築基準法施行令 第85条第1項・令別表第1、および国土交通省官庁営繕部
324/// 「建築構造設計基準」令和3年度版(同資料は令85条を準用しつつ、官庁施設に特有の
325/// 室用途〔書庫・実験室・電算室・機械室・体育館等〕を追加する)。値は N/m² を
326/// 内部単位 N/mm²(×1e-6)へ換算して返す。
327#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
328pub enum SlabUsage {
329    /// 住宅の居室、住宅以外の建築物における寝室又は病室。
330    Residential,
331    /// 事務室、会議室及び食堂。
332    Office,
333    /// 研究室(値は事務室に同じ。実況に応じて算定する)。
334    ResearchRoom,
335    /// 教室。
336    Classroom,
337    /// 百貨店又は店舗の売場。
338    Store,
339    /// 劇場・映画館等の客席又は集会室(固定席の場合)。
340    AssemblyFixed,
341    /// 同上(その他の場合)。
342    AssemblyOther,
343    /// 廊下・玄関・階段(劇場・集会場・売場等に連絡するもの)。
344    Corridor,
345    /// 法務局登記書庫(法務省型鋼製書架 W型8段6連を配置した場合)。
346    RegistryArchive,
347    /// 一般書庫、倉庫等(通常の階高の室に満載の書架を配置した場合)。
348    GeneralArchive,
349    /// 移動書架を設置する書庫、電算室の空調機室、用具庫等(一般書庫の1.5倍程度)。
350    MobileArchive,
351    /// 一般実験室(化学系)。
352    LabChemistry,
353    /// 一般実験室(物理系)。
354    LabPhysics,
355    /// 電算室(床版・小梁計算用は電算室用既製床の耐荷重、他は令85条の店舗の売場を準用)。
356    ComputerRoom,
357    /// 機械室(床版・小梁計算用は機械の平均的な重量、他は令85条の店舗の売場を準用)。
358    MachineRoom,
359    /// 体育館、武道場等(令85条の劇場等〔その他〕を準用)。
360    Gymnasium,
361    /// 自動車車庫及び自動車通路。
362    Garage,
363    /// 片持形式のバルコニー、庇等(令85条のバルコニーを準用)。
364    Balcony,
365    /// 屋上広場(常時人が使用する場合。学校・百貨店の類を除く)。
366    RoofResidential,
367    /// 屋上広場(常時人が使用する場合。学校・百貨店の類)。
368    RoofStore,
369    /// 屋上(通常人が使用しない場合)。
370    RoofUnused,
371    /// 屋上(鉄骨造体育館、武道場等)。短期荷重として扱い、床版・小梁計算用のみ
372    /// 作業荷重を見込む。大梁・柱・基礎計算用および地震力計算用は 0。
373    RoofSteelGym,
374    /// 任意入力(床版・小梁計算用/大梁・柱・基礎計算用/地震力計算用、いずれも N/mm²)。
375    Custom {
376        floor: f64,
377        frame: f64,
378        seismic: f64,
379    },
380}
381
382impl SlabUsage {
383    /// 用途別の積載荷重 [N/mm²](令別表第1)。
384    pub fn live_load(self, purpose: LoadPurpose) -> f64 {
385        // プリセットは令別表第1/国交省営繕基準の [N/m²]。内部単位 N/mm² へ ×1e-6。
386        // 返り値の並びは (床用, 骨組用, 地震用)。
387        let (floor, frame, seismic) = match self {
388            SlabUsage::Residential => (1800.0, 1300.0, 600.0),
389            SlabUsage::Office => (2900.0, 1800.0, 800.0),
390            SlabUsage::ResearchRoom => (2900.0, 1800.0, 800.0),
391            SlabUsage::Classroom => (2300.0, 2100.0, 1100.0),
392            SlabUsage::Store => (2900.0, 2400.0, 1300.0),
393            SlabUsage::AssemblyFixed => (2900.0, 2600.0, 1600.0),
394            SlabUsage::AssemblyOther => (3500.0, 3200.0, 2100.0),
395            SlabUsage::Corridor => (3500.0, 3200.0, 2100.0),
396            SlabUsage::RegistryArchive => (5900.0, 4900.0, 3900.0),
397            SlabUsage::GeneralArchive => (7800.0, 6900.0, 4900.0),
398            SlabUsage::MobileArchive => (11800.0, 10300.0, 7400.0),
399            SlabUsage::LabChemistry => (3900.0, 2400.0, 1600.0),
400            SlabUsage::LabPhysics => (4900.0, 3900.0, 2500.0),
401            SlabUsage::ComputerRoom => (4900.0, 2400.0, 1300.0),
402            SlabUsage::MachineRoom => (4900.0, 2400.0, 1300.0),
403            SlabUsage::Gymnasium => (3500.0, 3200.0, 2100.0),
404            SlabUsage::Garage => (5400.0, 3900.0, 2000.0),
405            SlabUsage::Balcony => (1800.0, 1300.0, 600.0),
406            SlabUsage::RoofResidential => (1800.0, 1300.0, 600.0),
407            SlabUsage::RoofStore => (2900.0, 2400.0, 1300.0),
408            SlabUsage::RoofUnused => (980.0, 600.0, 400.0),
409            SlabUsage::RoofSteelGym => (980.0, 0.0, 0.0),
410            // Custom は内部単位 N/mm² をそのまま返す(×1e-6 しない)。
411            SlabUsage::Custom {
412                floor,
413                frame,
414                seismic,
415            } => {
416                return match purpose {
417                    LoadPurpose::Floor => floor,
418                    LoadPurpose::Frame => frame,
419                    LoadPurpose::Seismic => seismic,
420                };
421            }
422        };
423        let v_n_per_m2 = match purpose {
424            LoadPurpose::Floor => floor,
425            LoadPurpose::Frame => frame,
426            LoadPurpose::Seismic => seismic,
427        };
428        v_n_per_m2 * 1e-6
429    }
430}
431
432#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
433pub struct AreaLoad {
434    pub kind: String,
435    pub value: f64,
436}
437
438/// 一方向スラブの荷重伝達方向(床ごとに指定。床荷重の分配における伝達方向〔X〕〔Y〕)。
439/// `X` は全体座標 X 方向へ伝達(=X 方向両側の辺が負担)、`Y` は Y 方向へ伝達。
440#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
441pub enum OneWayDir {
442    X,
443    Y,
444}
445
446#[cfg(test)]
447mod tests {
448    use super::*;
449
450    #[test]
451    fn test_usage_table_values_n_per_mm2() {
452        // 令別表第1: 事務室 = 床用 2900 / 骨組用 1800 / 地震用 800 [N/m²]。
453        let o = SlabUsage::Office;
454        assert!((o.live_load(LoadPurpose::Floor) - 2900e-6).abs() < 1e-12);
455        assert!((o.live_load(LoadPurpose::Frame) - 1800e-6).abs() < 1e-12);
456        assert!((o.live_load(LoadPurpose::Seismic) - 800e-6).abs() < 1e-12);
457        // 住宅 = 1800 / 1300 / 600。
458        let r = SlabUsage::Residential;
459        assert!((r.live_load(LoadPurpose::Floor) - 1800e-6).abs() < 1e-12);
460        assert!((r.live_load(LoadPurpose::Frame) - 1300e-6).abs() < 1e-12);
461        assert!((r.live_load(LoadPurpose::Seismic) - 600e-6).abs() < 1e-12);
462        // 国交省営繕基準(令和3年度版)で追加した室用途の値([N/m²])。
463        let cases: &[(SlabUsage, f64, f64, f64)] = &[
464            (SlabUsage::ResearchRoom, 2900.0, 1800.0, 800.0),
465            (SlabUsage::RegistryArchive, 5900.0, 4900.0, 3900.0),
466            (SlabUsage::GeneralArchive, 7800.0, 6900.0, 4900.0),
467            (SlabUsage::MobileArchive, 11800.0, 10300.0, 7400.0),
468            (SlabUsage::LabChemistry, 3900.0, 2400.0, 1600.0),
469            (SlabUsage::LabPhysics, 4900.0, 3900.0, 2500.0),
470            (SlabUsage::ComputerRoom, 4900.0, 2400.0, 1300.0),
471            (SlabUsage::MachineRoom, 4900.0, 2400.0, 1300.0),
472            (SlabUsage::Gymnasium, 3500.0, 3200.0, 2100.0),
473            (SlabUsage::Balcony, 1800.0, 1300.0, 600.0),
474            (SlabUsage::RoofUnused, 980.0, 600.0, 400.0),
475            (SlabUsage::RoofSteelGym, 980.0, 0.0, 0.0),
476        ];
477        for &(u, floor, frame, seismic) in cases {
478            assert!((u.live_load(LoadPurpose::Floor) - floor * 1e-6).abs() < 1e-12);
479            assert!((u.live_load(LoadPurpose::Frame) - frame * 1e-6).abs() < 1e-12);
480            assert!((u.live_load(LoadPurpose::Seismic) - seismic * 1e-6).abs() < 1e-12);
481        }
482
483        // 積載は 床用 ≥ 骨組用 ≥ 地震用 の順(全用途で成り立つ)。
484        for u in [
485            SlabUsage::Residential,
486            SlabUsage::Office,
487            SlabUsage::ResearchRoom,
488            SlabUsage::Classroom,
489            SlabUsage::Store,
490            SlabUsage::AssemblyFixed,
491            SlabUsage::AssemblyOther,
492            SlabUsage::Corridor,
493            SlabUsage::RegistryArchive,
494            SlabUsage::GeneralArchive,
495            SlabUsage::MobileArchive,
496            SlabUsage::LabChemistry,
497            SlabUsage::LabPhysics,
498            SlabUsage::ComputerRoom,
499            SlabUsage::MachineRoom,
500            SlabUsage::Gymnasium,
501            SlabUsage::Garage,
502            SlabUsage::Balcony,
503            SlabUsage::RoofResidential,
504            SlabUsage::RoofStore,
505            SlabUsage::RoofUnused,
506            SlabUsage::RoofSteelGym,
507        ] {
508            let f = u.live_load(LoadPurpose::Floor);
509            let g = u.live_load(LoadPurpose::Frame);
510            let s = u.live_load(LoadPurpose::Seismic);
511            assert!(f >= g && g >= s, "床用≥骨組用≥地震用: {u:?}");
512        }
513    }
514
515    #[test]
516    fn test_usage_custom_is_internal_units() {
517        // Custom は内部単位 N/mm² をそのまま返す(換算しない)。
518        let c = SlabUsage::Custom {
519            floor: 3.0e-3,
520            frame: 2.0e-3,
521            seismic: 1.0e-3,
522        };
523        assert_eq!(c.live_load(LoadPurpose::Floor), 3.0e-3);
524        assert_eq!(c.live_load(LoadPurpose::Frame), 2.0e-3);
525        assert_eq!(c.live_load(LoadPurpose::Seismic), 1.0e-3);
526    }
527
528    /// `RegionAnchor::FloorRegion` は床板の取付き先には使わない
529    /// (壁側〔自立壁〕専用のアンカー。モジュール doc・用語集「取付き先」参照)。
530    /// `Slab` のメソッドはこの分岐に来ないはずだが、`RegionAnchor` を壁と共有する
531    /// 都合上、網羅性のために持つ受け皿の戻り値を固定する。
532    #[test]
533    fn test_floor_region_anchor_never_yields_slab_geometry() {
534        let model = Model::default();
535        let slab = Slab {
536            id: crate::ids::SlabId(0),
537            shape: SlabShape::Attached {
538                anchor: RegionAnchor::FloorRegion {
539                    nodes: [NodeId(0), NodeId(1)],
540                },
541                extent: [0.0, 0.0],
542            },
543            plate: SlabPlate::default(),
544        };
545        assert_eq!(slab.boundary_coords(&model), None);
546        assert_eq!(slab.reference_node(), None);
547        assert_eq!(slab.edge_nodes(0), None);
548    }
549}