Skip to main content

squid_n_core/model/
aggregate.rs

1//! モデル全体を束ねる集約型。
2//!
3//! - [`ElemAttrs`] — 要素の側テーブル属性スナップショット(undo 用)。
4//! - [`Model`] — 構造モデル全体(節点・要素・断面・材料・階・荷重等)。
5
6use super::*;
7
8/// 1 つの要素に紐づく側テーブル属性のスナップショット。要素の削除・挿入
9/// ([`Model::take_elem_attrs`] / [`Model::restore_elem_attrs`])で属性の
10/// 退避・復元に用いる(undo 用の一時保持。直列化はしない)。
11#[derive(Clone, Debug, Default, PartialEq)]
12pub struct ElemAttrs {
13    pub wall: Option<WallAttr>,
14    pub steel_design: Option<SteelDesignAttr>,
15    pub brb: Option<BrbAttr>,
16    pub pca: Option<PcaBeamAttr>,
17    pub isolator: Option<IsolatorAttr>,
18    pub hysteresis: Option<MemberHysteresisAttr>,
19    pub damper: Option<DamperAttr>,
20    pub detail: Option<MemberDetailAttr>,
21}
22
23#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
24pub struct Model {
25    pub nodes: Vec<Node>,
26    pub elements: Vec<ElementData>,
27    pub sections: Vec<Section>,
28    pub materials: Vec<Material>,
29    pub stories: Vec<Story>,
30    /// 通り芯(各通りを識別するための呼称。[`AxisGroup`])。**構造計算には
31    /// 用いない**表示・識別専用のデータで、解析結果・設計結果には影響しない。
32    #[serde(default)]
33    pub axes: Vec<AxisGroup>,
34    pub floor_regions: Vec<FloorRegion>,
35    pub constraints: Vec<Constraint>,
36    pub load_cases: Vec<LoadCase>,
37    pub combinations: Vec<LoadCombination>,
38    /// 立体時刻歴応答解析の振動ケース(実行時に upsert。静的荷重ケースとは別)。
39    #[serde(default)]
40    pub vibration_cases: Vec<VibrationCase>,
41    /// 質点系時刻歴応答解析の振動ケース。
42    #[serde(default)]
43    pub lumped_vibration_cases: Vec<LumpedVibrationCase>,
44    /// 階の自動生成が作る剛床代表節点(慣性力重心に置く仮想節点)の ID。
45    /// 構造節点と区別するために保持し、再生成時に再利用する。
46    #[serde(default)]
47    pub generated_masters: Vec<NodeId>,
48    /// 動的解析の質量モデルの方式([`MassMethod`])。階の自動生成が剛床マスターへ
49    /// 与える質点質量の算定と、質量行列組立での部材密度質量の要否を規定する。
50    /// 旧スキーマは補正質点方式(従来の密度質量+節点質量と同じ組立)扱い。
51    #[serde(default)]
52    pub mass_method: MassMethod,
53    /// 剛性計算用の床スラブ厚 [mm](建物全体で一律。スラブ協力幅による梁剛性
54    /// 増大の算定に用いる。RC 規準)。0 以下でスラブ協力幅による梁剛性増大を無効化(既定)。
55    #[serde(default)]
56    pub slab_thickness: f64,
57    /// 自重算定の付加設定(鉄骨重量割増率・部材付加線重量)。`None` は既定値。
58    #[serde(default)]
59    pub load_cfg: Option<LoadCfg>,
60    /// 壁要素の付帯属性(開口・柱際スリット)。
61    #[serde(default)]
62    pub wall_attrs: Vec<WallAttr>,
63    /// 複数開口の取り扱い(建物一律。耐震壁の開口。RC 規準)。
64    /// 剛性の開口低減・耐震壁判定・検定への開口供給に適用する
65    /// (自重控除は常に生の開口面積和)。既定は「等価開口とする」。
66    #[serde(default)]
67    pub multi_opening_mode: MultiOpeningMode,
68    /// 応力解析の計算条件(令82条の応力解析。長期軸力を負担させない部材の指定)。
69    #[serde(default)]
70    pub stress_cfg: StressAnalysisCfg,
71    /// S 造部材の断面検定用属性(継手部・スカラップ欠損、横座屈長さ指定。
72    /// 鋼構造設計規準)。
73    #[serde(default)]
74    pub steel_design_attrs: Vec<SteelDesignAttr>,
75    /// 座屈補剛ブレース(BRB)の断面検定用属性(メーカー許容値。
76    /// 各メーカーの製品技術資料)。
77    #[serde(default)]
78    pub brb_attrs: Vec<BrbAttr>,
79    /// PCa(プレキャスト)梁の水平接合面検定用属性(水平接合面のせん断摩擦検定)。
80    #[serde(default)]
81    pub pca_attrs: Vec<PcaBeamAttr>,
82    /// 免震支承材の非線形特性(`ElementKind::Isolator` 要素、各免震部材指針)。
83    #[serde(default)]
84    pub isolator_attrs: Vec<IsolatorAttr>,
85    /// 部材の履歴則の個別指定(各履歴則の原典)。
86    /// 未指定の部材は構造種別ごとの既定([`default_member_hysteresis`])に従う。
87    #[serde(default)]
88    pub member_hysteresis_attrs: Vec<MemberHysteresisAttr>,
89    /// 制振ダンパー要素(`ElementKind::Damper`)の特性(各制振部材の力学モデル)。
90    #[serde(default)]
91    pub damper_attrs: Vec<DamperAttr>,
92    /// 部材の付帯情報(端部ハンチ・継手位置)。剛性・応力解析には影響しない
93    /// (設計書 §6.2。剛性は基準断面のまま)。断面算定の検定位置の追加
94    /// (ハンチ端・継手位置、§6.2.3)と数量拾いに用いる。
95    #[serde(default)]
96    pub member_detail_attrs: Vec<MemberDetailAttr>,
97    /// 所属未割当の小梁(`rebuild_floor_regions` でどの床領域にも入らなかったもの)。
98    #[serde(default)]
99    pub unassigned_joists: Vec<SecondaryMember>,
100    /// 所属未割当の間柱(`rebuild_wall_regions` でどの壁領域にも入らなかったもの)。
101    #[serde(default)]
102    pub unassigned_posts: Vec<SecondaryMember>,
103    /// 一本部材の指定(断面検定の採用応力。一本部材指定時の採用応力の扱い)。
104    /// 各エントリは**軸方向に連続する梁要素の ID を並び順**で持ち、
105    /// 断面検定の採用応力(端部・中央モーメント、部材長、内法長、せん断スパン比
106    /// 代表値)をグループ 1 本の部材として評価する。要素の解析(剛性・内力)は
107    /// 分割部材のまま行い、検定の文脈だけを合成する。
108    #[serde(default)]
109    pub beam_groups: Vec<Vec<ElemId>>,
110    /// 名前付き制振ダンパー定義(プリセットライブラリ)。`ElemId` への参照を
111    /// 持たないため、要素の追加・削除に伴う ID 繰上げ/繰下げ(`shift_elem_attr_refs`・
112    /// `take_elem_attrs`・`restore_elem_attrs`)の対象外。部材への割当は
113    /// `DamperDef::props` の値コピー(`Model::damper_attrs` へ追加)で行う。
114    ///
115    /// **msgpack(.scz)は位置ベース配列で直列化されるため、新しいフィールドは
116    /// 必ずこの構造体の末尾(`dof_map` の手前)へ追加すること**。中間に挿入すると
117    /// 旧バージョンで保存された .scz の後続フィールドの値がずれて読み込まれ、
118    /// `#[serde(default)]` があっても救済されない(default 補完は末尾欠損のみ有効)。
119    #[serde(default)]
120    pub damper_defs: Vec<DamperDef>,
121    /// 梁(水平材)のねじり剛性の扱い(建物一律。既定は i 端ねじれ解放)。
122    /// 旧スキーマ(フィールド無し)は既定=`ReleaseIEnd` で補完される。
123    #[serde(default)]
124    pub beam_torsion: BeamTorsionMode,
125    /// 仕口パネル(柱梁接合部パネル)のモデル化(建物一律。既定はモデル化する)。
126    /// 旧スキーマ(フィールド無し)は既定=`Model` で補完されるため、旧ファイルも
127    /// パネルをモデル化した状態で開く。
128    #[serde(default)]
129    pub panel_zone: PanelZoneMode,
130    /// 壁領域(壁版と付属間柱のグループ)。旧スキーマは空として補完。
131    #[serde(default)]
132    pub wall_regions: Vec<WallRegion>,
133    /// 床板(版)。大梁または小梁で囲まれた版、または主架構に取り付く版
134    /// (片持ち・バルコニー・出隅)ごとに 1 つ。`floor_regions` が「大梁の1スパン区画」
135    /// であるのに対し、こちらが版の仕様(厚さ・材料・仕上げ荷重・室用途)を持つ
136    /// ([`Slab`])。取り付く床板はどの `FloorRegion` からも参照されないことがある。
137    /// 旧スキーマ(フィールド無し)は空として補完。
138    #[serde(default)]
139    pub slabs: Vec<Slab>,
140    /// 壁版(版仕様。断面・開口。[`WallPlate`])。柱・梁が囲む鉛直構面内の版、
141    /// または主架構・床領域に取り付く版(パラペット・腰壁・垂れ壁・自立壁)ごとに 1 つ。
142    /// `wall_regions` が「柱・梁が囲む鉛直構面内の閉領域」であるのに対し、こちらが
143    /// 版の仕様を持つ(床の `floor_regions`/`slabs` と同じ関係)。取り付く壁版は
144    /// どの `WallRegion` からも参照されないことがある。旧スキーマは空として補完。
145    #[serde(default)]
146    pub wall_plates: Vec<WallPlate>,
147    #[serde(skip)]
148    pub dof_map: crate::dof::DofMap,
149}
150
151/// コレクション内の id が「配列添字 == id.index()」かつ重複しないことを検証する。
152/// `coll` は配列名(例 "nodes")、`id_name` は id 型名(例 "NodeId")。
153fn check_id_consistency<T>(
154    items: &[T],
155    coll: &str,
156    id_name: &str,
157    index_of: impl Fn(&T) -> usize,
158    raw_of: impl Fn(&T) -> u32,
159) -> Result<(), crate::error::CoreError> {
160    use crate::error::CoreError;
161    for (i, item) in items.iter().enumerate() {
162        if index_of(item) != i {
163            return Err(CoreError::IndexMismatch(format!(
164                "{coll}[{i}] has {id_name}({})",
165                raw_of(item)
166            )));
167        }
168    }
169    let mut seen = std::collections::HashSet::new();
170    for item in items {
171        if !seen.insert(index_of(item)) {
172            return Err(CoreError::DuplicateId(format!(
173                "{id_name}({})",
174                raw_of(item)
175            )));
176        }
177    }
178    Ok(())
179}
180
181/// 床板が参照する節点(境界節点、または取付き先の節点)。
182///
183/// 取り付く床板は自由端に節点を持たないため、取付き先の節点だけを返す。
184fn slab_node_refs(slab: &Slab) -> Vec<NodeId> {
185    match &slab.shape {
186        SlabShape::Enclosed { boundary } => boundary.clone(),
187        SlabShape::Attached { anchor, .. } => match anchor {
188            RegionAnchor::Line { nodes, .. } => nodes.to_vec(),
189            RegionAnchor::Point(n) => vec![*n],
190            // 床板の取付き先には使わない(`RegionAnchor::FloorRegion` のドキュメント参照。
191            // 壁側〔`WallPlate` の `Attached` 形〕専用のアンカーであり、床板では到達しない)。
192            RegionAnchor::FloorRegion { .. } => Vec::new(),
193        },
194    }
195}
196
197/// 壁版が参照する節点(境界節点、または取付き先の節点)。
198///
199/// 取り付く壁版は自由端に節点を持たないため、取付き先の節点だけを返す
200/// (`slab_node_refs` と同じ考え方)。`RegionAnchor::Point` は壁の取付き先としては
201/// 使わない(`WallPlate::boundary_coords` のドキュメント参照)。
202fn wall_plate_node_refs(plate: &WallPlate) -> Vec<NodeId> {
203    match &plate.shape {
204        WallPlateShape::Enclosed { boundary } => boundary.clone(),
205        WallPlateShape::Attached { anchor, .. } => match anchor {
206            RegionAnchor::Line { nodes, .. } => nodes.to_vec(),
207            RegionAnchor::FloorRegion { nodes, .. } => nodes.to_vec(),
208            RegionAnchor::Point(_) => Vec::new(),
209        },
210    }
211}
212
213impl Model {
214    pub fn validate(&self) -> Result<(), crate::error::CoreError> {
215        use crate::error::CoreError;
216
217        check_id_consistency(&self.nodes, "nodes", "NodeId", |n| n.id.index(), |n| n.id.0)?;
218
219        for (i, elem) in self.elements.iter().enumerate() {
220            if elem.id.index() != i {
221                return Err(CoreError::IndexMismatch(format!(
222                    "elements[{}] has ElemId({})",
223                    i, elem.id.0
224                )));
225            }
226        }
227
228        let mut seen_elems = std::collections::HashSet::new();
229        for elem in &self.elements {
230            if !seen_elems.insert(elem.id) {
231                return Err(CoreError::DuplicateId(format!("ElemId({})", elem.id.0)));
232            }
233            for &nid in &elem.nodes {
234                if nid.index() >= self.nodes.len() || self.nodes[nid.index()].id != nid {
235                    return Err(CoreError::DanglingRef(format!(
236                        "Elem {} -> Node {}",
237                        elem.id.0, nid.0
238                    )));
239                }
240            }
241            if let Some(sid) = elem.section {
242                if sid.index() >= self.sections.len() || self.sections[sid.index()].id != sid {
243                    return Err(CoreError::DanglingRef(format!(
244                        "Elem {} -> Section {}",
245                        elem.id.0, sid.0
246                    )));
247                }
248            }
249        }
250
251        // 断面が参照する材料が実在すること(材料は断面が持つ)。
252        for sec in &self.sections {
253            for (role, mid) in [
254                ("Material", sec.material),
255                ("RebarMaterial", sec.rebar_material),
256                ("ShearRebarMaterial", sec.shear_rebar_material),
257                ("SteelMaterial", sec.steel_material),
258            ] {
259                if let Some(mid) = mid {
260                    if mid.index() >= self.materials.len() || self.materials[mid.index()].id != mid
261                    {
262                        return Err(CoreError::DanglingRef(format!(
263                            "Section {} -> {role} {}",
264                            sec.id.0, mid.0
265                        )));
266                    }
267                }
268            }
269        }
270
271        check_id_consistency(
272            &self.stories,
273            "stories",
274            "StoryId",
275            |s| s.id.index(),
276            |s| s.id.0,
277        )?;
278        // 階は標高の昇順に並ぶこと。階への帰属区間は直下階のレベルで決まる
279        // ([`Model::story_spans`])ため、並びが崩れると区間が反転し、節点が
280        // 無言で別の階へ入る・どの階にも入らないという壊れ方をする。
281        for pair in self.stories.windows(2) {
282            if pair[1].elevation < pair[0].elevation {
283                return Err(CoreError::DanglingRef(format!(
284                    "Story {} ({}) の標高 {} が直下の Story {} ({}) の標高 {} より低い(階は標高の昇順に並べる)",
285                    pair[1].id.0, pair[1].name, pair[1].elevation,
286                    pair[0].id.0, pair[0].name, pair[0].elevation,
287                )));
288            }
289        }
290        // 通り芯が参照する節点が実在すること(陳腐化した参照の検出)。通り芯は
291        // 計算に用いないが、節点の削除で参照が壊れたまま保存されるのを防ぐ。
292        for group in &self.axes {
293            for axis in &group.axes {
294                for &nid in &axis.nodes {
295                    if nid.index() >= self.nodes.len() || self.nodes[nid.index()].id != nid {
296                        return Err(CoreError::DanglingRef(format!(
297                            "Axis {}/{} -> Node {}",
298                            group.name, axis.name, nid.0
299                        )));
300                    }
301                }
302            }
303        }
304        check_id_consistency(
305            &self.floor_regions,
306            "floor_regions",
307            "FloorRegionId",
308            |s| s.id.index(),
309            |s| s.id.0,
310        )?;
311        // 床領域の境界が参照する節点が実在すること(陳腐化した参照の検出)。
312        for region in &self.floor_regions {
313            for &nid in &region.boundary {
314                if nid.index() >= self.nodes.len() || self.nodes[nid.index()].id != nid {
315                    return Err(CoreError::DanglingRef(format!(
316                        "FloorRegion {} -> Node {}",
317                        region.id.0, nid.0
318                    )));
319                }
320            }
321            for &sid in &region.slab_ids {
322                if sid.index() >= self.slabs.len() || self.slabs[sid.index()].id != sid {
323                    return Err(CoreError::DanglingRef(format!(
324                        "FloorRegion {} -> Slab {}",
325                        region.id.0, sid.0
326                    )));
327                }
328            }
329        }
330        // 床領域は、同じ境界(大梁の区画)を持つものが 2 つあってはならない(D1)。
331        // 2 つあると、その床領域の小梁・床板の帰属が二重になる。
332        {
333            let mut seen: std::collections::HashSet<Vec<u32>> = std::collections::HashSet::new();
334            for region in &self.floor_regions {
335                if region.boundary.is_empty() {
336                    continue;
337                }
338                let mut key: Vec<u32> = region.boundary.iter().map(|n| n.0).collect();
339                key.sort_unstable();
340                key.dedup();
341                if !seen.insert(key) {
342                    return Err(CoreError::DuplicateId(format!(
343                        "FloorRegion {} は他の領域と同じ境界を持つ",
344                        region.id.0
345                    )));
346                }
347            }
348        }
349        // 床板 ID は「配列添字と一致」かつ「複数の床領域から共有されない」こと。
350        check_id_consistency(&self.slabs, "slabs", "SlabId", |s| s.id.index(), |s| s.id.0)?;
351        // 壁版 ID は「配列添字と一致」かつ「複数の壁領域から共有されない」こと
352        // (床板と同じ規約)。
353        check_id_consistency(
354            &self.wall_plates,
355            "wall_plates",
356            "WallPlateId",
357            |p| p.id.index(),
358            |p| p.id.0,
359        )?;
360        check_id_consistency(
361            &self.wall_regions,
362            "wall_regions",
363            "WallRegionId",
364            |r| r.id.index(),
365            |r| r.id.0,
366        )?;
367        {
368            let mut owner: std::collections::HashMap<u32, u32> = std::collections::HashMap::new();
369            for region in &self.wall_regions {
370                let mut seen_here = std::collections::HashSet::new();
371                for &pid in &region.wall_plate_ids {
372                    if !seen_here.insert(pid) {
373                        return Err(CoreError::DuplicateId(format!(
374                            "WallRegion {} wall_plate_ids has WallPlateId({})",
375                            region.id.0, pid.0
376                        )));
377                    }
378                    if pid.index() >= self.wall_plates.len()
379                        || self.wall_plates[pid.index()].id != pid
380                    {
381                        return Err(CoreError::DanglingRef(format!(
382                            "WallRegion {} -> WallPlate {}",
383                            region.id.0, pid.0
384                        )));
385                    }
386                    if let Some(&other) = owner.get(&pid.0) {
387                        if other != region.id.0 {
388                            return Err(CoreError::DuplicateId(format!(
389                                "WallPlate {} は複数の WallRegion({}・{})から参照されている",
390                                pid.0, other, region.id.0
391                            )));
392                        }
393                    }
394                    owner.insert(pid.0, region.id.0);
395                }
396            }
397        }
398        // 壁領域の境界が参照する節点が実在すること(陳腐化した参照の検出)。
399        for region in &self.wall_regions {
400            for &nid in &region.boundary {
401                if nid.index() >= self.nodes.len() || self.nodes[nid.index()].id != nid {
402                    return Err(CoreError::DanglingRef(format!(
403                        "WallRegion {} -> Node {}",
404                        region.id.0, nid.0
405                    )));
406                }
407            }
408        }
409        // 壁領域は、同じ境界(柱・梁の閉路)を持つものが 2 つあってはならない(D1)。
410        {
411            let mut seen: std::collections::HashSet<Vec<u32>> = std::collections::HashSet::new();
412            for region in &self.wall_regions {
413                if region.boundary.is_empty() {
414                    continue;
415                }
416                let mut key: Vec<u32> = region.boundary.iter().map(|n| n.0).collect();
417                key.sort_unstable();
418                key.dedup();
419                if !seen.insert(key) {
420                    return Err(CoreError::DuplicateId(format!(
421                        "WallRegion {} は他の領域と同じ境界を持つ",
422                        region.id.0
423                    )));
424                }
425            }
426        }
427        {
428            let mut owner: std::collections::HashMap<u32, u32> = std::collections::HashMap::new();
429            for region in &self.floor_regions {
430                let mut seen_here = std::collections::HashSet::new();
431                for &sid in &region.slab_ids {
432                    if !seen_here.insert(sid) {
433                        return Err(CoreError::DuplicateId(format!(
434                            "FloorRegion {} slab_ids has SlabId({})",
435                            region.id.0, sid.0
436                        )));
437                    }
438                    if let Some(&other) = owner.get(&sid.0) {
439                        if other != region.id.0 {
440                            return Err(CoreError::DuplicateId(format!(
441                                "Slab {} は複数の FloorRegion({}・{})から参照されている",
442                                sid.0, other, region.id.0
443                            )));
444                        }
445                    }
446                    owner.insert(sid.0, region.id.0);
447                }
448            }
449        }
450        // 床板の境界・取付き先が参照する節点・断面が実在すること(陳腐化した参照の検出)。
451        for slab in &self.slabs {
452            for nid in slab_node_refs(slab) {
453                if nid.index() >= self.nodes.len() || self.nodes[nid.index()].id != nid {
454                    return Err(CoreError::DanglingRef(format!(
455                        "Slab {} -> Node {}",
456                        slab.id.0, nid.0
457                    )));
458                }
459            }
460            if let Some(sid) = slab.section() {
461                if sid.index() >= self.sections.len() || self.sections[sid.index()].id != sid {
462                    return Err(CoreError::DanglingRef(format!(
463                        "Slab {} -> Section {}",
464                        slab.id.0, sid.0
465                    )));
466                }
467            }
468        }
469        // 壁版の境界・取付き先が参照する節点・断面・床領域が実在すること(同上)。
470        for plate in &self.wall_plates {
471            for nid in wall_plate_node_refs(plate) {
472                if nid.index() >= self.nodes.len() || self.nodes[nid.index()].id != nid {
473                    return Err(CoreError::DanglingRef(format!(
474                        "WallPlate {} -> Node {}",
475                        plate.id.0, nid.0
476                    )));
477                }
478            }
479            if let Some(sid) = plate.section {
480                if sid.index() >= self.sections.len() || self.sections[sid.index()].id != sid {
481                    return Err(CoreError::DanglingRef(format!(
482                        "WallPlate {} -> Section {}",
483                        plate.id.0, sid.0
484                    )));
485                }
486            }
487            // 自立壁(`RegionAnchor::FloorRegion`)が荷重を渡す床領域は保存しない
488            // (壁の位置から都度解決する。`RegionAnchor::FloorRegion` のドキュメント)。
489            // 検査すべき床領域参照は存在しない。荷重を流せる床領域に載っているかは
490            // 幾何の問題であり、解析前チェック(`model_issues`)が見る。
491        }
492        check_id_consistency(
493            &self.sections,
494            "sections",
495            "SectionId",
496            |s| s.id.index(),
497            |s| s.id.0,
498        )?;
499        check_id_consistency(
500            &self.materials,
501            "materials",
502            "MaterialId",
503            |m| m.id.index(),
504            |m| m.id.0,
505        )?;
506
507        // 二次部材(小梁・間柱)の参照整合(D6: 領域内または未割当リストに実体を保持)。
508        for (ri, region) in self.floor_regions.iter().enumerate() {
509            for (ji, sm) in region.secondary_joists.iter().enumerate() {
510                Self::validate_secondary_member(
511                    sm,
512                    &format!("FloorRegion {ri} secondary_joists[{ji}]"),
513                    &self.nodes,
514                    &self.sections,
515                )?;
516                if sm.kind != SecondaryMemberKind::Joist {
517                    return Err(CoreError::DanglingRef(format!(
518                        "FloorRegion {} secondary_joists[{ji}] は Joist でない",
519                        region.id.0
520                    )));
521                }
522            }
523        }
524        for (i, sm) in self.unassigned_joists.iter().enumerate() {
525            Self::validate_secondary_member(
526                sm,
527                &format!("unassigned_joists[{i}]"),
528                &self.nodes,
529                &self.sections,
530            )?;
531            if sm.kind != SecondaryMemberKind::Joist {
532                return Err(CoreError::DanglingRef(format!(
533                    "unassigned_joists[{i}] は Joist でない"
534                )));
535            }
536        }
537        for (ri, region) in self.wall_regions.iter().enumerate() {
538            for (pi, sm) in region.posts.iter().enumerate() {
539                Self::validate_secondary_member(
540                    sm,
541                    &format!("WallRegion {ri} posts[{pi}]"),
542                    &self.nodes,
543                    &self.sections,
544                )?;
545                if sm.kind != SecondaryMemberKind::Post {
546                    return Err(CoreError::DanglingRef(format!(
547                        "WallRegion {} posts[{pi}] は Post でない",
548                        region.id.0
549                    )));
550                }
551            }
552        }
553        for (i, sm) in self.unassigned_posts.iter().enumerate() {
554            Self::validate_secondary_member(
555                sm,
556                &format!("unassigned_posts[{i}]"),
557                &self.nodes,
558                &self.sections,
559            )?;
560            if sm.kind != SecondaryMemberKind::Post {
561                return Err(CoreError::DanglingRef(format!(
562                    "unassigned_posts[{i}] は Post でない"
563                )));
564            }
565        }
566
567        // 同じ種別・同じ端点の二次部材は 1 本だけ(床をまたいだ手編集の重複を止める)。
568        {
569            use std::collections::HashSet;
570            let mut seen = HashSet::new();
571            for sm in self.joists().chain(self.posts()) {
572                let a = sm.nodes[0].0.min(sm.nodes[1].0);
573                let b = sm.nodes[0].0.max(sm.nodes[1].0);
574                if !seen.insert((sm.kind, a, b)) {
575                    return Err(CoreError::DanglingRef(format!(
576                        "二次部材が重複しています({:?} 節点 {}-{})",
577                        sm.kind, a, b
578                    )));
579                }
580            }
581        }
582
583        // 取付き線の無次元区間 `span`([t_i, t_j])は 0.0〜1.0 の範囲で始端 < 終端でなければ
584        // ならない。荷重は `squid_n_load::floor::distribute_cantilever` が `span` から引いた
585        // 部分区間へ幾何解決で載せる(`squid_n_load::floor::LoadTarget::Span::t`)。
586        // 逆順・範囲外・非有限は取付き線上に定義できないため弾く。
587        for slab in &self.slabs {
588            if let SlabShape::Attached {
589                anchor: RegionAnchor::Line { span, .. },
590                ..
591            } = &slab.shape
592            {
593                if !crate::model::span_is_valid(*span) {
594                    return Err(CoreError::DanglingRef(format!(
595                        "Slab {} の取付き線の区間 span が不正(0.0 <= t_i < t_j <= 1.0 であること)",
596                        slab.id.0
597                    )));
598                }
599            }
600        }
601        // 壁版の取付き線の無次元区間も同じ規約(`RegionAnchor::FloorRegion` は
602        // `span` を持たないため対象外。壁版自体の始点・終点全長を使う)。
603        for plate in &self.wall_plates {
604            if let WallPlateShape::Attached {
605                anchor: RegionAnchor::Line { span, .. },
606                ..
607            } = &plate.shape
608            {
609                if !crate::model::span_is_valid(*span) {
610                    return Err(CoreError::DanglingRef(format!(
611                        "WallPlate {} の取付き線の区間 span が不正(0.0 <= t_i < t_j <= 1.0 であること)",
612                        plate.id.0
613                    )));
614                }
615            }
616        }
617        // 立ち上がり高さの未指定(=階高いっぱい)を許すのは自立壁だけである。
618        // 取付き線に取り付く全高の壁は、囲む柱梁が高さを決めるので囲まれた壁版
619        // (`WallPlateShape::Enclosed`)で表す。線アンカーで未指定を許すと、
620        // `squid_n_element::wall::misc_wall` が階高分の腰壁せいを取付き先の梁 1 本へ
621        // 丸ごと算入し、梁の剛性を過大に、変形を過小に見る危険側の評価になる。
622        for plate in &self.wall_plates {
623            if let WallPlateShape::Attached {
624                anchor,
625                extent: None,
626            } = &plate.shape
627            {
628                if !matches!(anchor, RegionAnchor::FloorRegion { .. }) {
629                    return Err(CoreError::DanglingRef(format!(
630                        "WallPlate {} は立ち上がり高さが未指定(階高いっぱい)だが、\
631                         取付き先が床領域ではない。未指定を許すのは自立壁だけで、\
632                         取付き線に取り付く全高の壁は囲まれた壁版として入力する",
633                        plate.id.0
634                    )));
635                }
636            }
637        }
638
639        // 大梁または小梁で囲まれた床板は、同じ境界を持つものが 2 つあってはならない。
640        // 2 つあると、その床領域の荷重が二重に分配される。
641        {
642            let mut seen: std::collections::HashSet<Vec<u32>> = std::collections::HashSet::new();
643            for slab in &self.slabs {
644                let SlabShape::Enclosed { boundary } = &slab.shape else {
645                    continue; // 取り付く床板は数の制限を置かない。
646                };
647                if boundary.is_empty() {
648                    continue;
649                }
650                let mut key: Vec<u32> = boundary.iter().map(|n| n.0).collect();
651                key.sort_unstable();
652                key.dedup();
653                if !seen.insert(key) {
654                    return Err(CoreError::DuplicateId(format!(
655                        "Slab {} は他の床板と同じ境界を持つ",
656                        slab.id.0
657                    )));
658                }
659            }
660        }
661        // 柱・梁が囲む壁版も、同じ境界を持つものが 2 つあってはならない(同上)。
662        {
663            let mut seen: std::collections::HashSet<Vec<u32>> = std::collections::HashSet::new();
664            for plate in &self.wall_plates {
665                let WallPlateShape::Enclosed { boundary } = &plate.shape else {
666                    continue; // 取り付く壁版は数の制限を置かない。
667                };
668                if boundary.is_empty() {
669                    continue;
670                }
671                let mut key: Vec<u32> = boundary.iter().map(|n| n.0).collect();
672                key.sort_unstable();
673                key.dedup();
674                if !seen.insert(key) {
675                    return Err(CoreError::DuplicateId(format!(
676                        "WallPlate {} は他の壁版と同じ境界を持つ",
677                        plate.id.0
678                    )));
679                }
680            }
681        }
682
683        // 一本部材指定(beam_groups)の参照整合。検定の採用応力がグループの要素を
684        // 直接引くため、ダングリングすると無関係な部材の応力を合成してしまう。
685        for (gi, group) in self.beam_groups.iter().enumerate() {
686            for &eid in group {
687                if eid.index() >= self.elements.len() || self.elements[eid.index()].id != eid {
688                    return Err(CoreError::DanglingRef(format!(
689                        "BeamGroup {} -> Elem {}",
690                        gi, eid.0
691                    )));
692                }
693            }
694        }
695
696        Ok(())
697    }
698
699    /// 節点 ID から節点を引く。存在しなければ `None`。
700    ///
701    /// `nodes[i].id == NodeId(i)`(節点の削除・挿入で `squid-n-edit` が維持する
702    /// 不変条件)を利用して添字で引くため O(1)。不変条件が崩れたモデルでも
703    /// 正しく引けるよう、添字の ID が一致しない場合のみ線形探索へ落とす。
704    ///
705    /// **ID から実体を引くところは常にこのメソッドを使う**(各所での線形探索は、
706    /// 不変条件が成り立つのに O(n) を払ううえ、探索規則が散らばる)。
707    pub fn node(&self, id: NodeId) -> Option<&Node> {
708        match self.nodes.get(id.index()) {
709            Some(n) if n.id == id => Some(n),
710            _ => self.nodes.iter().find(|n| n.id == id),
711        }
712    }
713
714    /// 要素 ID から要素を引く。存在しなければ `None`。
715    /// 引き方は [`Model::node`] と同じ(`elements[i].id == ElemId(i)`)。
716    pub fn element(&self, id: ElemId) -> Option<&ElementData> {
717        match self.elements.get(id.index()) {
718            Some(e) if e.id == id => Some(e),
719            _ => self.elements.iter().find(|e| e.id == id),
720        }
721    }
722
723    /// 断面 ID から断面を引く。存在しなければ `None`。
724    /// 引き方は [`Model::node`] と同じ(`sections[i].id == SectionId(i)`)。
725    ///
726    /// この不変条件は [`Model::validate`] が要素・二次部材からの参照ごとに
727    /// 検証しており(`sections[sid.index()].id != sid` をダングリング参照として弾く)、
728    /// [`Model::element_section`] も添字で引いている。ID から断面を引くところは
729    /// 常にこのメソッドを使う。
730    pub fn section(&self, id: SectionId) -> Option<&Section> {
731        match self.sections.get(id.index()) {
732            Some(s) if s.id == id => Some(s),
733            _ => self.sections.iter().find(|s| s.id == id),
734        }
735    }
736
737    /// 指定した節点が部材・節点荷重・階・床・拘束のいずれかから参照されているかを判定する。
738    /// 参照中の節点を削除すると参照が壊れる(ダングリング)ため、削除前にこれで確認する。
739    pub fn node_in_use(&self, id: NodeId) -> bool {
740        self.elements.iter().any(|e| e.nodes.contains(&id))
741            || self.node_referenced_outside_elements(id)
742    }
743
744    /// [`Model::node_in_use`] と同様だが、要素からの参照は `excl` を除いて判定する
745    /// (支点免震要素の接地節点が「この要素以外から孤立しているか」の判定に使う。
746    /// `excl` 要素自身が `id` を参照していても、それだけでは「使用中」とみなさない)。
747    pub fn node_in_use_excluding_elem(&self, id: NodeId, excl: ElemId) -> bool {
748        self.elements
749            .iter()
750            .any(|e| e.id != excl && e.nodes.contains(&id))
751            || self.node_referenced_outside_elements(id)
752    }
753
754    /// 要素以外(節点荷重・階・床領域・床板・壁領域・壁版・二次部材・拘束)からの
755    /// 参照有無。[`Model::node_in_use`]・[`Model::node_in_use_excluding_elem`] の共通部分。
756    ///
757    /// 大半は [`Model::node_referenced_by_regions_or_plates`] へ委譲し、ここでは
758    /// それに含まれない `stories`(利用者の節点削除を防ぐ目的では見る必要があるが、
759    /// D21 の判定である `region_rebuild::node_has_structural_ref` は準備計算のたびに
760    /// 埋め直されるため意図的に除外している)だけを追加で見る。
761    fn node_referenced_outside_elements(&self, id: NodeId) -> bool {
762        self.stories.iter().any(|s| s.node_ids.contains(&id))
763            || self.node_referenced_by_regions_or_plates(id)
764    }
765
766    /// 節点 `id` が床領域・床板・壁領域・壁版・二次部材・拘束・節点荷重の
767    /// いずれかから参照されているか。
768    ///
769    /// [`Model::node_referenced_outside_elements`](squid-n-edit の節点削除ガード)と
770    /// [`crate::region_rebuild::node_has_structural_ref`](D21 の節点削除判定)が
771    /// この判定を共有する。両者が異なるのは、前者が追加で見る `stories`
772    /// (利用者の節点削除を防ぐ)と、後者が追加で見る節点自身の支点・質量・
773    /// 剛床マスター(`stories`/`axes` は意図的に除外)だけである。
774    ///
775    /// **`NodeId` を持つフィールドを `Model` へ新設したら、まず [`Model::visit_node_ids`]
776    /// を更新し、次に該当フィールドがここでも参照有無を判定できることを確認すること**
777    /// (壁領域(`wall_regions`)を追加し忘れて `visit_node_ids`・本関数の双方が
778    /// `wall_regions` を見ていなかったことが敵対的レビューで発覚した前例がある。
779    /// 床領域・床板・壁領域・壁版・二次部材・拘束・節点荷重を1箇所へ集約したのは
780    /// この事故を踏まえた是正である)。
781    pub(crate) fn node_referenced_by_regions_or_plates(&self, id: NodeId) -> bool {
782        self.load_cases
783            .iter()
784            .any(|lc| lc.nodal.iter().any(|nl| nl.node == id))
785            || self.floor_regions.iter().any(|r| r.boundary.contains(&id))
786            || self.slabs.iter().any(|sl| slab_node_refs(sl).contains(&id))
787            || self.wall_regions.iter().any(|r| r.boundary.contains(&id))
788            || self
789                .wall_plates
790                .iter()
791                .any(|p| wall_plate_node_refs(p).contains(&id))
792            || self
793                .joists()
794                .chain(self.posts())
795                .any(|sm| sm.nodes.contains(&id))
796            || self.constraints.iter().any(|c| match c {
797                Constraint::RigidDiaphragm { master, slaves, .. } => {
798                    *master == id || slaves.contains(&id)
799                }
800                Constraint::Mpc { master, terms } => {
801                    *master == id || terms.iter().any(|(n, _, _)| *n == id)
802                }
803                Constraint::RigidLink { master, slaves, .. } => {
804                    *master == id || slaves.contains(&id)
805                }
806            })
807    }
808
809    /// 要素が「支点免震要素」(squid-n-edit の `PlaceSupportIsolator` が生成する配置形。
810    /// 対象節点と同一座標の接地節点との間に設置する零長 Isolator 要素)であるかを判定する。
811    /// 該当すれば `(上部節点, 接地節点)` を返す(上部節点=支点として振る舞う対象節点、
812    /// 接地節点=自動生成された `restraint=FIXED` の孤立節点)。
813    ///
814    /// 条件: `ElementKind::Isolator` の2節点要素で、両端が同一座標(零長)かつ、
815    /// 一方の節点が `restraint=FIXED` でこの要素以外から参照されていない(孤立)こと。
816    /// 通常の(支点ではない)免震要素はこの条件を満たさず `None` を返す。
817    pub fn support_isolator_ends(&self, elem: ElemId) -> Option<(NodeId, NodeId)> {
818        let e = self.elements.get(elem.index()).filter(|e| e.id == elem)?;
819        if e.kind != ElementKind::Isolator || e.nodes.len() != 2 {
820            return None;
821        }
822        let (n0, n1) = (e.nodes[0], e.nodes[1]);
823        let node0 = self.node(n0)?;
824        let node1 = self.node(n1)?;
825        if node0.coord != node1.coord {
826            return None;
827        }
828        let is_isolated_ground = |id: NodeId, restraint: crate::dof::Dof6Mask| {
829            restraint == crate::dof::Dof6Mask::FIXED && !self.node_in_use_excluding_elem(id, elem)
830        };
831        if is_isolated_ground(n0, node0.restraint) {
832            Some((n1, n0))
833        } else if is_isolated_ground(n1, node1.restraint) {
834            Some((n0, n1))
835        } else {
836            None
837        }
838    }
839
840    pub fn eq_ignoring_dofmap(&self, other: &Self) -> bool {
841        self.nodes == other.nodes
842            && self.elements == other.elements
843            && self.sections == other.sections
844            && self.materials == other.materials
845            && self.stories == other.stories
846            && self.floor_regions == other.floor_regions
847            && self.slabs == other.slabs
848            && self.constraints == other.constraints
849            && self.load_cases == other.load_cases
850            && self.combinations == other.combinations
851            && self.vibration_cases == other.vibration_cases
852            && self.lumped_vibration_cases == other.lumped_vibration_cases
853            && self.generated_masters == other.generated_masters
854            && self.mass_method == other.mass_method
855            && self.load_cfg == other.load_cfg
856            && self.wall_attrs == other.wall_attrs
857            && self.wall_plates == other.wall_plates
858            && self.stress_cfg == other.stress_cfg
859            && self.steel_design_attrs == other.steel_design_attrs
860            && self.brb_attrs == other.brb_attrs
861            && self.pca_attrs == other.pca_attrs
862            && self.unassigned_joists == other.unassigned_joists
863            && self.unassigned_posts == other.unassigned_posts
864            && self.axes == other.axes
865            && self.beam_groups == other.beam_groups
866            && self.isolator_attrs == other.isolator_attrs
867            && self.member_hysteresis_attrs == other.member_hysteresis_attrs
868            && self.damper_attrs == other.damper_attrs
869            && self.damper_defs == other.damper_defs
870            && self.member_detail_attrs == other.member_detail_attrs
871            && self.beam_torsion == other.beam_torsion
872            && self.panel_zone == other.panel_zone
873            && self.wall_regions == other.wall_regions
874    }
875
876    /// ダンパー要素の特性を返す(`Model::damper_attrs` から要素 ID で検索)。
877    pub fn damper_props(&self, elem: ElemId) -> Option<DamperProps> {
878        self.damper_attrs
879            .iter()
880            .find(|a| a.elem == elem)
881            .map(|a| a.props)
882    }
883
884    /// ダンパー要素の特性を設定/解除する。`None` を渡すと指定を解除する。
885    /// 戻り値は変更前の指定(undo 用)。
886    pub fn set_damper_props(
887        &mut self,
888        elem: ElemId,
889        props: Option<DamperProps>,
890    ) -> Option<DamperProps> {
891        let old = self.damper_props(elem);
892        self.damper_attrs.retain(|a| a.elem != elem);
893        if let Some(p) = props {
894            self.damper_attrs.push(DamperAttr { elem, props: p });
895        }
896        old
897    }
898
899    /// モデル内の全ての `NodeId` 参照(節点自身の ID を含む)へ `f` を適用する。
900    /// 節点の削除・挿入に伴う ID 繰り上げ/繰り下げ(squid-n-edit)で用いる。
901    ///
902    /// **`NodeId` を持つフィールドを `Model` へ追加したら必ずここへ追随すること**
903    /// (`validate`・`eq_ignoring_dofmap` と同様)。かつては走査が編集側に散在して
904    /// おり、`secondary_members` の追随漏れが「節点削除後に二次部材が別の節点へ
905    /// 張り付く」ダングリング参照を生んでいた。フィールド定義と同じクレートに
906    /// 走査を置くことで、追加時の抜けを構造的に防ぐ。
907    pub fn visit_node_ids(&mut self, mut f: impl FnMut(&mut NodeId)) {
908        for node in &mut self.nodes {
909            f(&mut node.id);
910        }
911        for id in &mut self.generated_masters {
912            f(id);
913        }
914        for elem in &mut self.elements {
915            for n in &mut elem.nodes {
916                f(n);
917            }
918        }
919        for story in &mut self.stories {
920            for n in &mut story.node_ids {
921                f(n);
922            }
923        }
924        for group in &mut self.axes {
925            for axis in &mut group.axes {
926                for n in &mut axis.nodes {
927                    f(n);
928                }
929            }
930        }
931        for region in &mut self.floor_regions {
932            for n in &mut region.boundary {
933                f(n);
934            }
935            for sm in &mut region.secondary_joists {
936                for n in &mut sm.nodes {
937                    f(n);
938                }
939            }
940        }
941        for slab in &mut self.slabs {
942            match &mut slab.shape {
943                SlabShape::Enclosed { boundary } => {
944                    for n in boundary {
945                        f(n);
946                    }
947                }
948                SlabShape::Attached { anchor, .. } => match anchor {
949                    RegionAnchor::Line { nodes, .. } => {
950                        for n in nodes {
951                            f(n);
952                        }
953                    }
954                    RegionAnchor::Point(n) => f(n),
955                    // 床板では到達しない(`slab_node_refs` と同じ理由)。
956                    RegionAnchor::FloorRegion { .. } => {}
957                },
958            }
959        }
960        for plate in &mut self.wall_plates {
961            match &mut plate.shape {
962                WallPlateShape::Enclosed { boundary } => {
963                    for n in boundary {
964                        f(n);
965                    }
966                }
967                WallPlateShape::Attached { anchor, .. } => match anchor {
968                    RegionAnchor::Line { nodes, .. } => {
969                        for n in nodes {
970                            f(n);
971                        }
972                    }
973                    RegionAnchor::FloorRegion { nodes, .. } => {
974                        for n in nodes {
975                            f(n);
976                        }
977                    }
978                    // 壁の取付き先としては使わない(`wall_plate_node_refs` と同じ理由)。
979                    RegionAnchor::Point(_) => {}
980                },
981            }
982        }
983        for region in &mut self.wall_regions {
984            for n in &mut region.boundary {
985                f(n);
986            }
987            for sm in &mut region.posts {
988                for n in &mut sm.nodes {
989                    f(n);
990                }
991            }
992        }
993        for sm in &mut self.unassigned_joists {
994            for n in &mut sm.nodes {
995                f(n);
996            }
997        }
998        for sm in &mut self.unassigned_posts {
999            for n in &mut sm.nodes {
1000                f(n);
1001            }
1002        }
1003        for c in &mut self.constraints {
1004            match c {
1005                Constraint::RigidDiaphragm { master, slaves, .. }
1006                | Constraint::RigidLink { master, slaves, .. } => {
1007                    f(master);
1008                    for s in slaves {
1009                        f(s);
1010                    }
1011                }
1012                Constraint::Mpc { master, terms } => {
1013                    f(master);
1014                    for (n, _, _) in terms {
1015                        f(n);
1016                    }
1017                }
1018            }
1019        }
1020        for lc in &mut self.load_cases {
1021            for nl in &mut lc.nodal {
1022                f(&mut nl.node);
1023            }
1024        }
1025    }
1026
1027    /// モデル内の全ての `StoryId` 参照(階自身の ID を含む)へ `f` を適用する
1028    /// ([`Model::visit_node_ids`] と同じ規約)。
1029    ///
1030    /// 階の追加・削除では「ID=配列位置」の不変条件を保つために ID の繰り上げが
1031    /// 必要になる。参照箇所を呼び出し側へ散らさないよう、走査はここに集約する。
1032    pub fn visit_story_ids(&mut self, mut f: impl FnMut(&mut StoryId)) {
1033        for story in &mut self.stories {
1034            f(&mut story.id);
1035        }
1036        for node in &mut self.nodes {
1037            if let Some(sid) = &mut node.story {
1038                f(sid);
1039            }
1040        }
1041        for c in &mut self.constraints {
1042            if let Constraint::RigidDiaphragm { story, .. } = c {
1043                f(story);
1044            }
1045        }
1046    }
1047
1048    /// モデル内の全ての `SectionId` 参照(断面自身の ID を含む)へ `f` を適用する
1049    /// ([`Model::visit_node_ids`] と同じ規約)。
1050    pub fn visit_section_ids(&mut self, mut f: impl FnMut(&mut crate::ids::SectionId)) {
1051        for sec in &mut self.sections {
1052            f(&mut sec.id);
1053        }
1054        for elem in &mut self.elements {
1055            if let Some(sid) = &mut elem.section {
1056                f(sid);
1057            }
1058        }
1059        for region in &mut self.floor_regions {
1060            for sm in &mut region.secondary_joists {
1061                if let Some(sid) = &mut sm.section {
1062                    f(sid);
1063                }
1064            }
1065        }
1066        for slab in &mut self.slabs {
1067            if let Some(sid) = &mut slab.plate.section {
1068                f(sid);
1069            }
1070        }
1071        for plate in &mut self.wall_plates {
1072            if let Some(sid) = &mut plate.section {
1073                f(sid);
1074            }
1075        }
1076        for sm in self
1077            .unassigned_joists
1078            .iter_mut()
1079            .chain(self.unassigned_posts.iter_mut())
1080        {
1081            if let Some(sid) = &mut sm.section {
1082                f(sid);
1083            }
1084        }
1085        for region in &mut self.wall_regions {
1086            for sm in &mut region.posts {
1087                if let Some(sid) = &mut sm.section {
1088                    f(sid);
1089                }
1090            }
1091        }
1092    }
1093
1094    /// モデル内の全ての `MaterialId` 参照(材料自身の ID を含む)へ `f` を適用する
1095    /// ([`Model::visit_node_ids`] と同じ規約)。
1096    pub fn visit_material_ids(&mut self, mut f: impl FnMut(&mut crate::ids::MaterialId)) {
1097        for mat in &mut self.materials {
1098            f(&mut mat.id);
1099        }
1100        // 材料参照は断面が持つ(部材・二次部材は持たない)。
1101        for sec in &mut self.sections {
1102            for mid in [
1103                &mut sec.material,
1104                &mut sec.rebar_material,
1105                &mut sec.shear_rebar_material,
1106                &mut sec.steel_material,
1107            ]
1108            .into_iter()
1109            .flatten()
1110            {
1111                f(mid);
1112            }
1113        }
1114    }
1115
1116    /// モデル内の全ての `ElemId` 参照(要素自身の ID・部材荷重・側テーブル属性・
1117    /// 一本部材指定)へ `f` を適用する([`Model::visit_node_ids`] と同じ規約)。
1118    pub fn visit_elem_ids(&mut self, mut f: impl FnMut(&mut ElemId)) {
1119        for elem in &mut self.elements {
1120            f(&mut elem.id);
1121        }
1122        for lc in &mut self.load_cases {
1123            for ml in &mut lc.member {
1124                f(&mut ml.elem);
1125            }
1126        }
1127        // WallRegion/WallPlate は ElemId を持たない(壁の解析要素は準備計算からの
1128        // 生成物であり、モデルには残さない。D4・D5)。
1129        self.shift_elem_attr_refs(&mut f);
1130    }
1131
1132    /// 要素に紐づく全ての側テーブル属性(壁・鉄骨・BRB・PCa・免震・履歴則・ダンパー)と
1133    /// 一本部材指定(`beam_groups`)の `elem` 参照に `f` を適用する。
1134    /// 要素の追加・削除に伴う ID 繰上げ/繰下げで、参照整合を保つために用いる
1135    /// (要素自身の ID・部材荷重も含めた全参照は [`Model::visit_elem_ids`])。
1136    pub fn shift_elem_attr_refs(&mut self, mut f: impl FnMut(&mut ElemId)) {
1137        for a in &mut self.wall_attrs {
1138            f(&mut a.elem);
1139        }
1140        for a in &mut self.steel_design_attrs {
1141            f(&mut a.elem);
1142        }
1143        for a in &mut self.brb_attrs {
1144            f(&mut a.elem);
1145        }
1146        for a in &mut self.pca_attrs {
1147            f(&mut a.elem);
1148        }
1149        for a in &mut self.isolator_attrs {
1150            f(&mut a.elem);
1151        }
1152        for a in &mut self.member_hysteresis_attrs {
1153            f(&mut a.elem);
1154        }
1155        for a in &mut self.damper_attrs {
1156            f(&mut a.elem);
1157        }
1158        for a in &mut self.member_detail_attrs {
1159            f(&mut a.elem);
1160        }
1161        for group in &mut self.beam_groups {
1162            for e in group.iter_mut() {
1163                f(e);
1164            }
1165        }
1166    }
1167
1168    /// 指定要素に紐づく全ての側テーブル属性を取り外して返す(要素削除時の退避用)。
1169    pub fn take_elem_attrs(&mut self, elem: ElemId) -> ElemAttrs {
1170        /// `elem` フィールドが一致する最初の要素を取り外して返す。
1171        fn take_first<T>(v: &mut Vec<T>, get: impl Fn(&T) -> ElemId, elem: ElemId) -> Option<T> {
1172            v.iter()
1173                .position(|a| get(a) == elem)
1174                .map(|pos| v.remove(pos))
1175        }
1176        ElemAttrs {
1177            wall: take_first(&mut self.wall_attrs, |a| a.elem, elem),
1178            steel_design: take_first(&mut self.steel_design_attrs, |a| a.elem, elem),
1179            brb: take_first(&mut self.brb_attrs, |a| a.elem, elem),
1180            pca: take_first(&mut self.pca_attrs, |a| a.elem, elem),
1181            isolator: take_first(&mut self.isolator_attrs, |a| a.elem, elem),
1182            hysteresis: take_first(&mut self.member_hysteresis_attrs, |a| a.elem, elem),
1183            damper: take_first(&mut self.damper_attrs, |a| a.elem, elem),
1184            detail: take_first(&mut self.member_detail_attrs, |a| a.elem, elem),
1185        }
1186    }
1187
1188    /// 取り外した側テーブル属性を、指定要素 ID へ紐づけ直して復元する
1189    /// (要素削除の undo 用)。各属性の `elem` は `elem` へ上書きする。
1190    pub fn restore_elem_attrs(&mut self, elem: ElemId, attrs: ElemAttrs) {
1191        if let Some(mut a) = attrs.wall {
1192            a.elem = elem;
1193            self.wall_attrs.push(a);
1194        }
1195        if let Some(mut a) = attrs.steel_design {
1196            a.elem = elem;
1197            self.steel_design_attrs.push(a);
1198        }
1199        if let Some(mut a) = attrs.brb {
1200            a.elem = elem;
1201            self.brb_attrs.push(a);
1202        }
1203        if let Some(mut a) = attrs.pca {
1204            a.elem = elem;
1205            self.pca_attrs.push(a);
1206        }
1207        if let Some(mut a) = attrs.isolator {
1208            a.elem = elem;
1209            self.isolator_attrs.push(a);
1210        }
1211        if let Some(mut a) = attrs.hysteresis {
1212            a.elem = elem;
1213            self.member_hysteresis_attrs.push(a);
1214        }
1215        if let Some(mut a) = attrs.damper {
1216            a.elem = elem;
1217            self.damper_attrs.push(a);
1218        }
1219        if let Some(mut a) = attrs.detail {
1220            a.elem = elem;
1221            self.member_detail_attrs.push(a);
1222        }
1223    }
1224
1225    /// 部材の付帯情報(ハンチ・継手位置)を返す(未指定は `None`)。
1226    pub fn member_detail(&self, elem: ElemId) -> Option<&MemberDetailAttr> {
1227        self.member_detail_attrs.iter().find(|a| a.elem == elem)
1228    }
1229
1230    /// 部材に指定された履歴則(増分解析用)を返す(未指定は `None`=既定に従う)。
1231    pub fn member_hysteresis(&self, elem: ElemId) -> Option<HysteresisModel> {
1232        self.member_hysteresis_attrs
1233            .iter()
1234            .find(|a| a.elem == elem)
1235            .map(|a| a.rule)
1236    }
1237
1238    /// 部材に指定された履歴則(時刻歴応答解析用)を返す。
1239    /// 時刻歴用スロットが未指定(`None`)の部材は増分用の指定に従う。
1240    /// どちらも未指定は `None`=既定に従う。
1241    pub fn member_hysteresis_th(&self, elem: ElemId) -> Option<HysteresisModel> {
1242        self.member_hysteresis_attrs
1243            .iter()
1244            .find(|a| a.elem == elem)
1245            .map(|a| a.rule_th.unwrap_or(a.rule))
1246    }
1247
1248    /// 部材に指定された時刻歴用スロットの生値を返す(`None`=増分用と同じ)。
1249    /// UI の「増分と同じ」表示の判定に用いる([`Self::member_hysteresis_th`] は
1250    /// 増分用へフォールバックした解決後の値を返す)。
1251    pub fn member_hysteresis_th_raw(&self, elem: ElemId) -> Option<HysteresisModel> {
1252        self.member_hysteresis_attrs
1253            .iter()
1254            .find(|a| a.elem == elem)
1255            .and_then(|a| a.rule_th)
1256    }
1257
1258    /// 属性が既定(増分=Auto・時刻歴=増分と同じ)と等価なら側テーブルから除去する。
1259    fn prune_default_hysteresis(&mut self, elem: ElemId) {
1260        self.member_hysteresis_attrs.retain(|a| {
1261            !(a.elem == elem && a.rule == HysteresisModel::Auto && a.rule_th.is_none())
1262        });
1263    }
1264
1265    /// 部材の履歴則(増分解析用)を設定する。`HysteresisModel::Auto` を指定した
1266    /// 場合は増分用の指定を解除(既定に従う)。時刻歴用スロットは変更しない。
1267    /// 戻り値は変更前の指定(undo 用)。
1268    pub fn set_member_hysteresis(
1269        &mut self,
1270        elem: ElemId,
1271        rule: HysteresisModel,
1272    ) -> Option<HysteresisModel> {
1273        let old = self.member_hysteresis(elem);
1274        if let Some(a) = self
1275            .member_hysteresis_attrs
1276            .iter_mut()
1277            .find(|a| a.elem == elem)
1278        {
1279            a.rule = rule;
1280        } else if rule != HysteresisModel::Auto {
1281            self.member_hysteresis_attrs.push(MemberHysteresisAttr {
1282                elem,
1283                rule,
1284                rule_th: None,
1285            });
1286        }
1287        self.prune_default_hysteresis(elem);
1288        old
1289    }
1290
1291    /// 部材の履歴則(時刻歴応答解析用スロット)を設定する。`None` は
1292    /// 「増分用と同じ」に戻す。増分用の指定は変更しない。
1293    /// 戻り値は変更前のスロット生値(undo 用)。
1294    pub fn set_member_hysteresis_th(
1295        &mut self,
1296        elem: ElemId,
1297        rule_th: Option<HysteresisModel>,
1298    ) -> Option<HysteresisModel> {
1299        let old = self.member_hysteresis_th_raw(elem);
1300        if let Some(a) = self
1301            .member_hysteresis_attrs
1302            .iter_mut()
1303            .find(|a| a.elem == elem)
1304        {
1305            a.rule_th = rule_th;
1306        } else if rule_th.is_some() {
1307            self.member_hysteresis_attrs.push(MemberHysteresisAttr {
1308                elem,
1309                rule: HysteresisModel::Auto,
1310                rule_th,
1311            });
1312        }
1313        self.prune_default_hysteresis(elem);
1314        old
1315    }
1316
1317    /// 標準荷重ケース一式(DL・LL(架構用)・LL(地震用)・EX・EY)と標準荷重組合せ
1318    /// (長期 DL+LL、短期地震 DL+LL±EX・DL+LL±EY)を持つ空モデルを作る
1319    /// (新規作成の既定。[`default_load_cases`]・[`default_combinations`] 参照)。
1320    pub fn with_default_load_cases() -> Self {
1321        Model {
1322            load_cases: default_load_cases(),
1323            combinations: default_combinations(),
1324            ..Model::default()
1325        }
1326    }
1327
1328    /// 二次部材 1 件の節点・断面参照が実在することを検証する(D6)。
1329    pub fn validate_secondary_member(
1330        sm: &SecondaryMember,
1331        label: &str,
1332        nodes: &[Node],
1333        sections: &[Section],
1334    ) -> Result<(), crate::error::CoreError> {
1335        use crate::error::CoreError;
1336        for &nid in &sm.nodes {
1337            if nid.index() >= nodes.len() || nodes[nid.index()].id != nid {
1338                return Err(CoreError::DanglingRef(format!("{label} -> Node {}", nid.0)));
1339            }
1340        }
1341        if let Some(sid) = sm.section {
1342            if sid.index() >= sections.len() || sections[sid.index()].id != sid {
1343                return Err(CoreError::DanglingRef(format!(
1344                    "{label} -> Section {}",
1345                    sid.0
1346                )));
1347            }
1348        }
1349        Ok(())
1350    }
1351
1352    /// 全小梁(床領域内 + 未割当)を走査する。
1353    pub fn joists(&self) -> impl Iterator<Item = &SecondaryMember> {
1354        self.unassigned_joists.iter().chain(
1355            self.floor_regions
1356                .iter()
1357                .flat_map(|r| r.secondary_joists.iter()),
1358        )
1359    }
1360
1361    /// 全間柱(壁領域内 + 未割当)を走査する。
1362    pub fn posts(&self) -> impl Iterator<Item = &SecondaryMember> {
1363        self.unassigned_posts
1364            .iter()
1365            .chain(self.wall_regions.iter().flat_map(|r| r.posts.iter()))
1366    }
1367
1368    /// モデル内の全ての `SlabId` 参照(床板自身の ID・床領域の `slab_ids`)へ
1369    /// `f` を適用する([`Model::visit_node_ids`] と同じ規約)。
1370    ///
1371    /// **`SlabId` を持つフィールドを `Model` へ追加したら必ずここへ追随すること**。
1372    pub fn visit_slab_ids(&mut self, mut f: impl FnMut(&mut SlabId)) {
1373        for slab in &mut self.slabs {
1374            f(&mut slab.id);
1375        }
1376        for region in &mut self.floor_regions {
1377            for sid in &mut region.slab_ids {
1378                f(sid);
1379            }
1380        }
1381    }
1382
1383    /// モデル内の全ての `WallPlateId` 参照(壁版自身の ID・壁領域の `wall_plate_ids`)へ
1384    /// `f` を適用する([`Model::visit_slab_ids`] と同じ規約の壁側版)。
1385    ///
1386    /// **`WallPlateId` を持つフィールドを `Model` へ追加したら必ずここへ追随すること**。
1387    pub fn visit_wall_plate_ids(&mut self, mut f: impl FnMut(&mut WallPlateId)) {
1388        for plate in &mut self.wall_plates {
1389            f(&mut plate.id);
1390        }
1391        for region in &mut self.wall_regions {
1392            for pid in &mut region.wall_plate_ids {
1393                f(pid);
1394            }
1395        }
1396    }
1397
1398    /// `keep` が `false` を返す床板を取り除き、`id == index` の不変条件を
1399    /// 復元したうえで、`FloorRegion.slab_ids` の参照を新しい ID へ張り替える
1400    /// (取り除かれた床板への参照は削除する)。
1401    ///
1402    /// 床板をまとめて間引く処理は必ずこれを通すこと。
1403    pub fn retain_slabs(&mut self, mut keep: impl FnMut(&Slab) -> bool) {
1404        let mut remap: Vec<Option<SlabId>> = Vec::with_capacity(self.slabs.len());
1405        let mut next = 0u32;
1406        for slab in &self.slabs {
1407            if keep(slab) {
1408                remap.push(Some(SlabId(next)));
1409                next += 1;
1410            } else {
1411                remap.push(None);
1412            }
1413        }
1414        if remap.iter().all(|r| r.is_some()) {
1415            return;
1416        }
1417
1418        let mut i = 0usize;
1419        self.slabs.retain(|_| {
1420            let k = remap[i].is_some();
1421            i += 1;
1422            k
1423        });
1424        for (i, slab) in self.slabs.iter_mut().enumerate() {
1425            slab.id = SlabId(i as u32);
1426        }
1427
1428        for region in &mut self.floor_regions {
1429            region
1430                .slab_ids
1431                .retain_mut(|id| match remap.get(id.index()).copied().flatten() {
1432                    Some(new_id) => {
1433                        *id = new_id;
1434                        true
1435                    }
1436                    None => false,
1437                });
1438        }
1439    }
1440
1441    /// 旧スキーマの自動生成荷重ケース名を標準ケース名へ移行する(読込時の後方互換)。
1442    ///
1443    /// - 「床荷重(自動)」→「DL」、「床積載(自動)」→「LL(架構用)」、
1444    ///   「床地震用積載(自動)」→「LL(地震用)」に改名する
1445    ///   (移行先の名前が既に使われている場合は改名しない)。
1446    /// - 「自重(自動)」は DL へ統合する(自重は DL の同期内容に含まれるように
1447    ///   なったため)。DL ケースが存在する場合は「自重(自動)」を削除し、
1448    ///   荷重組合せの参照は DL へ付け替える(同一組合せが既に DL を参照して
1449    ///   いる場合は項を除去して二重計上を防ぐ)。DL がない場合は
1450    ///   「自重(自動)」自体を「DL」へ改名する。
1451    ///
1452    /// ケースの内容は改名/削除のみで書き換えない(自動生成ケースの内容は
1453    /// 解析実行前の同期アクションが毎回再計算して全置換する)。
1454    /// 削除時は `LoadCaseId` の「id == 添字」規約を保つよう後続ケースの ID と
1455    /// 組合せの参照を詰め直す。
1456    pub fn migrate_legacy_auto_load_cases(&mut self) {
1457        const LEGACY_SELF_WEIGHT: &str = "自重(自動)";
1458        let renames = [
1459            ("床荷重(自動)", DL_CASE_NAME),
1460            ("床積載(自動)", LL_FRAME_CASE_NAME),
1461            ("床地震用積載(自動)", LL_SEISMIC_CASE_NAME),
1462        ];
1463        for (old, new) in renames {
1464            if self.load_cases.iter().any(|lc| lc.name == new) {
1465                continue;
1466            }
1467            if let Some(lc) = self.load_cases.iter_mut().find(|lc| lc.name == old) {
1468                lc.name = new.to_string();
1469            }
1470        }
1471
1472        let Some(sw_idx) = self
1473            .load_cases
1474            .iter()
1475            .position(|lc| lc.name == LEGACY_SELF_WEIGHT)
1476        else {
1477            return;
1478        };
1479        let sw_id = self.load_cases[sw_idx].id;
1480        match self
1481            .load_cases
1482            .iter()
1483            .find(|lc| lc.name == DL_CASE_NAME)
1484            .map(|lc| lc.id)
1485        {
1486            None => {
1487                // DL がなければ「自重(自動)」を DL として引き継ぐ。
1488                self.load_cases[sw_idx].name = DL_CASE_NAME.to_string();
1489                self.load_cases[sw_idx].kind = LoadCaseKind::Dead;
1490            }
1491            Some(dl_id) => {
1492                // 組合せの参照を DL へ付け替え(既に DL を含む組合せでは項を除去)。
1493                for combo in &mut self.combinations {
1494                    let has_dl = combo.terms.iter().any(|(id, _)| *id == dl_id);
1495                    if has_dl {
1496                        combo.terms.retain(|(id, _)| *id != sw_id);
1497                    } else {
1498                        for (id, _) in &mut combo.terms {
1499                            if *id == sw_id {
1500                                *id = dl_id;
1501                            }
1502                        }
1503                    }
1504                }
1505                // ケースを削除し、id == 添字の規約を保つよう後続 ID を詰める。
1506                self.load_cases.remove(sw_idx);
1507                for lc in &mut self.load_cases {
1508                    if lc.id.0 > sw_id.0 {
1509                        lc.id.0 -= 1;
1510                    }
1511                }
1512                for combo in &mut self.combinations {
1513                    for (id, _) in &mut combo.terms {
1514                        if id.0 > sw_id.0 {
1515                            id.0 -= 1;
1516                        }
1517                    }
1518                }
1519            }
1520        }
1521    }
1522}
1523
1524#[cfg(test)]
1525mod node_reference_tests {
1526    use super::*;
1527
1528    fn node(id: u32) -> Node {
1529        Node {
1530            id: NodeId(id),
1531            coord: [f64::from(id) * 1000.0, 0.0, 0.0],
1532            restraint: Dof6Mask::FREE,
1533            mass: None,
1534            story: None,
1535            support_spring: None,
1536        }
1537    }
1538
1539    /// `Model::node_referenced_by_regions_or_plates` が、フィールド追加のたびに
1540    /// `visit_node_ids`・`node_referenced_outside_elements`・
1541    /// `region_rebuild::node_has_structural_ref` へ手作業で追随する運用(敵対的
1542    /// レビューで `wall_regions` の抜けが見つかった原因)を集約先1箇所へ寄せた
1543    /// ことを、対象フィールドそれぞれについて固定する。ここに載る全種別が
1544    /// 「参照あり」と判定されることを確認すれば、`Model::node_in_use`
1545    /// (squid-n-edit の削除ガード)と `region_rebuild::node_has_structural_ref`
1546    /// (D21 の削除判定)の両方がその種別を保護できることになる。
1547    #[test]
1548    fn test_node_referenced_by_regions_or_plates_covers_every_field_kind() {
1549        let mut model = Model::default();
1550        for i in 0..12u32 {
1551            model.nodes.push(node(i));
1552        }
1553
1554        // 0: 節点荷重。
1555        model.load_cases.push(LoadCase {
1556            id: LoadCaseId(0),
1557            name: "L".into(),
1558            nodal: vec![NodalLoad {
1559                node: NodeId(0),
1560                values: [0.0; 6],
1561                name: String::new(),
1562                source: LoadSource::Manual,
1563            }],
1564            member: Vec::new(),
1565            kind: LoadCaseKind::default(),
1566        });
1567
1568        // 1: 床領域の境界。2: 床領域が持つ二次部材小梁(`secondary_joists`)。
1569        let mut region = FloorRegion::new(FloorRegionId(0), vec![NodeId(1)]);
1570        region.secondary_joists.push(SecondaryMember {
1571            kind: SecondaryMemberKind::Joist,
1572            nodes: [NodeId(2), NodeId(2)],
1573            section: None,
1574            name: String::new(),
1575        });
1576        model.floor_regions.push(region);
1577
1578        // 3: 床板(Enclosed)。4: 床板(Attached/Line)。
1579        model.slabs.push(Slab {
1580            id: SlabId(0),
1581            shape: SlabShape::Enclosed {
1582                boundary: vec![NodeId(3)],
1583            },
1584            plate: SlabPlate::default(),
1585        });
1586        model.slabs.push(Slab {
1587            id: SlabId(1),
1588            shape: SlabShape::Attached {
1589                anchor: RegionAnchor::Line {
1590                    nodes: [NodeId(4), NodeId(4)],
1591                    span: [0.0, 1.0],
1592                    transfer: LoadTransfer::Anchor,
1593                },
1594                extent: [0.0, 0.0],
1595            },
1596            plate: SlabPlate::default(),
1597        });
1598
1599        // 5: 壁領域の境界。
1600        model
1601            .wall_regions
1602            .push(WallRegion::new(WallRegionId(0), vec![NodeId(5)]));
1603
1604        // 6: 壁版(Enclosed)。7: 壁版(Attached/Line)。
1605        model.wall_plates.push(WallPlate {
1606            id: WallPlateId(0),
1607            shape: WallPlateShape::Enclosed {
1608                boundary: vec![NodeId(6)],
1609            },
1610            section: None,
1611            opening_area: 0.0,
1612            opening_weight: 0.0,
1613            openings: Vec::new(),
1614            loads: vec![],
1615            slit: Default::default(),
1616        });
1617        model.wall_plates.push(WallPlate {
1618            id: WallPlateId(1),
1619            shape: WallPlateShape::Attached {
1620                anchor: RegionAnchor::Line {
1621                    nodes: [NodeId(7), NodeId(7)],
1622                    span: [0.0, 1.0],
1623                    transfer: LoadTransfer::Anchor,
1624                },
1625                extent: Some([0.0, 0.0]),
1626            },
1627            section: None,
1628            opening_area: 0.0,
1629            opening_weight: 0.0,
1630            openings: Vec::new(),
1631            loads: vec![],
1632            slit: Default::default(),
1633        });
1634
1635        // 8: 二次部材(未割当小梁)。領域内(node 2)とは別のフィールドである。
1636        model.unassigned_joists.push(SecondaryMember {
1637            kind: SecondaryMemberKind::Joist,
1638            nodes: [NodeId(8), NodeId(8)],
1639            section: None,
1640            name: String::new(),
1641        });
1642
1643        // 9: 拘束(剛床マスター)。
1644        model.constraints.push(Constraint::RigidDiaphragm {
1645            story: StoryId(0),
1646            master: NodeId(9),
1647            slaves: Vec::new(),
1648            weight: None,
1649            ci_override: None,
1650        });
1651
1652        // 10: 壁版(Attached/FloorRegion。自立壁)。
1653        model.wall_plates.push(WallPlate {
1654            id: WallPlateId(2),
1655            shape: WallPlateShape::Attached {
1656                anchor: RegionAnchor::FloorRegion {
1657                    nodes: [NodeId(10), NodeId(10)],
1658                },
1659                extent: Some([0.0, 0.0]),
1660            },
1661            section: None,
1662            opening_area: 0.0,
1663            opening_weight: 0.0,
1664            openings: Vec::new(),
1665            loads: vec![],
1666            slit: Default::default(),
1667        });
1668
1669        for i in 0..=10u32 {
1670            assert!(
1671                model.node_referenced_by_regions_or_plates(NodeId(i)),
1672                "node {i} は参照されているはず"
1673            );
1674        }
1675        // 11 はどこからも参照されない対照節点。
1676        assert!(!model.node_referenced_by_regions_or_plates(NodeId(11)));
1677    }
1678}