Skip to main content

squid_n_core/
frame_gen.rs

1//! 立体グリッド(通り芯 × 階レベル)と、そこからの架構生成。
2//!
3//! 日本の構造設計では、まず通り芯と階を決め、その交点に柱を立てて通りに沿って
4//! 大梁を架ける。本モジュールはその手順をそのままデータにしたもので、2 つの役目を持つ。
5//!
6//! - [`SpaceGrid`] — 既存モデルの通り芯と階から立体グリッドを導く。
7//!   3D ビューのグリッド描画と格子点スナップが情報源として使う。
8//! - [`FrameSpec`] / [`generate_frame`] — スパンと階高の入力から、節点・柱・大梁・
9//!   柱脚支点・通り芯・階を一括生成する(架構作成ウィザード)。
10//!
11//! 生成した**部材**の断面と材料は未割当のままとする。断面は利用者が決めるものであり、
12//! もっともらしい既定断面を割り当てると、入力し忘れたまま解析が通ってしまう。
13//! 解析前チェックが「断面が未割当の部材があります」で止めるため、割り当て漏れは
14//! 必ず名指しされる。断面形状が決まらないかぎり材料だけあっても解析は通らないため、
15//! 材料も与えない。
16//!
17//! **床だけは断面と材料を作る。** 床は解析対象の部材ではなく、解析上は荷重
18//! (自重・積載)としてのみ効く二次部材である。板厚さえ決めれば断面が確定し、
19//! 材料は自重を決める入力にすぎないので、部材に既定を与えることの危険とは別問題になる。
20//! 板厚と自重は断面からしか解決できず([`crate::model::Model::slab_thickness_of`])、
21//! 断面や材料が無い床は解析前チェックが止めてしまうため、`S15` の断面 1 枚と
22//! [`FrameSpec::slab_concrete`] のコンクリート 1 つを作り、全階の床へ割り当てる。
23
24use crate::dof::{Dof, Dof6Mask};
25use crate::geom::default_local_ref_vector;
26use crate::ids::{ElemId, MaterialId, NodeId, StoryId};
27use crate::ids::{FloorRegionId, SectionId, SlabId};
28use crate::material_grade::{material_presets, MaterialPreset};
29use crate::model::{
30    default_story_name, Axis, AxisGroup, AxisGroupKind, AxisPlanDir, AxisSource, ElementData,
31    ElementKind, EndCondition, FloorRegion, ForceRegime, LocalAxis, Material, Model, Node, Section,
32    Slab, SlabPlate, SlabShape, SlabUsage, Story,
33};
34
35/// 同一の格子線とみなす座標差 [mm]([`crate::axis_gen::AXIS_TOL_MM`] と同値)。
36pub const GRID_TOL_MM: f64 = crate::axis_gen::AXIS_TOL_MM;
37
38/// 立体グリッド。平面の格子線(X 方向・Y 方向の通り芯の位置)と、鉛直の格子面
39/// (基部を含む階レベル)で構成する。
40///
41/// 通り芯は識別のためのデータであり構造計算には用いない([`crate::model::axis`])。
42/// 本構造体もその位置づけを引き継ぎ、**モデリングの下敷き**としてのみ使う。
43#[derive(Clone, Debug, Default, PartialEq)]
44pub struct SpaceGrid {
45    /// X 方向の通りの位置(グローバル X 座標 [mm]。昇順・重複なし)と通り名。
46    pub x_lines: Vec<GridLine>,
47    /// Y 方向の通りの位置(グローバル Y 座標 [mm]。昇順・重複なし)と通り名。
48    pub y_lines: Vec<GridLine>,
49    /// レベル(グローバル Z 座標 [mm]。昇順・重複なし)と階名。
50    /// 先頭は基部([`Model::base_elevation`])で、階名は `GL` とする。
51    pub levels: Vec<GridLevel>,
52}
53
54/// 平面の格子線 1 本。
55#[derive(Clone, Debug, PartialEq)]
56pub struct GridLine {
57    pub name: String,
58    /// グローバル座標 [mm](X 方向の通りは X 座標、Y 方向の通りは Y 座標)。
59    pub coord: f64,
60}
61
62/// 鉛直方向の格子面 1 枚(階レベル)。
63#[derive(Clone, Debug, PartialEq)]
64pub struct GridLevel {
65    pub name: String,
66    /// 標高 [mm]。
67    pub elevation: f64,
68    /// 対応する階。基部レベルは `None`。
69    pub story: Option<StoryId>,
70}
71
72impl SpaceGrid {
73    /// 格子が平面・鉛直のどちらかで空なら、格子点を作れない。
74    pub fn is_empty(&self) -> bool {
75        self.x_lines.is_empty() || self.y_lines.is_empty() || self.levels.is_empty()
76    }
77
78    /// 全格子点の座標を返す(X → Y → レベルの順に走査)。
79    pub fn points(&self) -> impl Iterator<Item = [f64; 3]> + '_ {
80        self.x_lines.iter().flat_map(move |gx| {
81            self.y_lines.iter().flat_map(move |gy| {
82                self.levels
83                    .iter()
84                    .map(move |gz| [gx.coord, gy.coord, gz.elevation])
85            })
86        })
87    }
88}
89
90/// モデルの通り芯と階から立体グリッドを導く。
91///
92/// - 平面の格子線は、離れを測る向きがグローバル軸に沿う平行芯グループ
93///   ([`AxisGroupKind::plan_dir`])から取る。斜めの平行芯グループと
94///   [`AxisGroupKind::Other`](円弧芯・放射芯・作図芯)は、直交格子として
95///   表せないため対象にしない。
96/// - 離れはグループの原点・方向角で測った符号付きの値なので、グローバル座標へ
97///   戻してから昇順に並べる([`AxisGroupKind::offset_dir`] の向きに沿う 1 次元の
98///   位置なので、原点+離れ×向きの該当成分がそのまま座標になる)。
99/// - レベルは基部([`Model::base_elevation`])と各階の標高。階は標高の昇順に
100///   並ぶ不変条件を持つ([`Model::validate`])ため、そのまま使える。
101///
102/// 同じ位置の通りが複数あるモデル(取り込みで重複した通り)では、[`GRID_TOL_MM`]
103/// 以内の格子線を 1 本にまとめ、名前は先に現れたものを採る。
104pub fn space_grid(model: &Model) -> SpaceGrid {
105    let mut x_lines = plan_lines(model, AxisPlanDir::X);
106    let mut y_lines = plan_lines(model, AxisPlanDir::Y);
107    x_lines.sort_by(|a, b| a.coord.total_cmp(&b.coord));
108    y_lines.sort_by(|a, b| a.coord.total_cmp(&b.coord));
109    dedup_lines(&mut x_lines);
110    dedup_lines(&mut y_lines);
111
112    let base = model.base_elevation();
113    let mut levels = vec![GridLevel {
114        name: "GL".to_string(),
115        elevation: base,
116        story: None,
117    }];
118    for s in &model.stories {
119        if (s.elevation - base).abs() <= GRID_TOL_MM {
120            continue;
121        }
122        levels.push(GridLevel {
123            name: s.name.clone(),
124            elevation: s.elevation,
125            story: Some(s.id),
126        });
127    }
128
129    SpaceGrid {
130        x_lines,
131        y_lines,
132        levels,
133    }
134}
135
136/// 指定した向きの平行芯グループから、グローバル座標に直した格子線を集める。
137fn plan_lines(model: &Model, dir: AxisPlanDir) -> Vec<GridLine> {
138    let mut out = Vec::new();
139    for g in &model.axes {
140        if g.kind.plan_dir() != Some(dir) {
141            continue;
142        }
143        let (AxisGroupKind::Parallel { origin, .. }, Some(off)) = (g.kind, g.kind.offset_dir())
144        else {
145            continue;
146        };
147        // 離れを測る向きは、この分岐ではグローバル軸に沿っている(plan_dir が Some)。
148        // したがって成分は ±1 で、原点+離れ×向き の該当成分がそのまま座標になる。
149        let (o, d) = match dir {
150            AxisPlanDir::X => (origin[0], off[0]),
151            AxisPlanDir::Y => (origin[1], off[1]),
152        };
153        for a in &g.axes {
154            let Some(dist) = a.distance else { continue };
155            out.push(GridLine {
156                name: a.name.clone(),
157                coord: o + dist * d,
158            });
159        }
160    }
161    out
162}
163
164/// 昇順に並んだ格子線から、[`GRID_TOL_MM`] 以内で重なるものを畳む。
165fn dedup_lines(lines: &mut Vec<GridLine>) {
166    lines.dedup_by(|b, a| (b.coord - a.coord).abs() <= GRID_TOL_MM);
167}
168
169/// 柱脚(最下レベルの節点)の支持条件。
170///
171/// 既定はピン。基礎梁は部材として生成されるため、それによる柱脚の回転拘束は
172/// 解析が直接評価する。支点に重ねて固定を与えると、基礎そのものの回転拘束を
173/// 無条件に見込むことになり、出発点のモデルとしては危険側に出やすい。
174#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
175pub enum BaseSupport {
176    /// 固定(6 自由度すべて拘束)。
177    Fixed,
178    /// ピン(並進 3 自由度のみ拘束)。
179    #[default]
180    Pinned,
181}
182
183impl BaseSupport {
184    fn mask(self) -> Dof6Mask {
185        match self {
186            BaseSupport::Fixed => Dof6Mask::FIXED,
187            BaseSupport::Pinned => {
188                let mut m = Dof6Mask::FREE;
189                m.set_fixed(Dof::Ux);
190                m.set_fixed(Dof::Uy);
191                m.set_fixed(Dof::Uz);
192                m
193            }
194        }
195    }
196}
197
198/// 架構作成ウィザードの入力。
199#[derive(Clone, Debug, PartialEq)]
200pub struct FrameSpec {
201    /// 平面の原点 `(x, y)` [mm]。1 本目の通りの位置になる。
202    pub origin: [f64; 2],
203    /// 基部の標高 [mm]。最下レベル(柱脚)になる。
204    pub base_elevation: f64,
205    /// X 方向のスパン [mm](要素数 = 通りの本数 − 1)。空なら通り 1 本。
206    pub x_spans: Vec<f64>,
207    /// Y 方向のスパン [mm](同上)。
208    pub y_spans: Vec<f64>,
209    /// 下から順の階高 [mm](要素数 = 生成する**層**の数)。
210    pub story_heights: Vec<f64>,
211    /// 階(床)の名前。**要素数は `story_heights` より 1 つ多く**、先頭が基部の
212    /// 床の名前である(階は床であり、床レベルは層より 1 つ多いため)。
213    /// 空文字の要素は [`default_story_name`] で補う。
214    pub story_names: Vec<String>,
215    /// X 方向グループの名前(通り名の接頭辞)。
216    pub x_group_name: String,
217    /// Y 方向グループの名前(通り名の接頭辞)。
218    pub y_group_name: String,
219    /// 柱脚の支持条件。
220    pub base_support: BaseSupport,
221    /// 大梁を生成するか。
222    pub with_girders: bool,
223    /// 床を生成するか。各階の各格子区画(隣り合う通りで囲まれた矩形)に 1 枚ずつ。
224    pub with_slabs: bool,
225    /// 床の室用途(積載荷重プリセット)。
226    pub slab_usage: Option<SlabUsage>,
227    /// 床の板厚 [mm](断面 [`SLAB_SECTION_NAME`] の板厚になる)。
228    pub slab_thickness: f64,
229    /// 床のコンクリートのグレード名([`material_presets`] の名称。既定は
230    /// [`DEFAULT_SLAB_CONCRETE`])。この材料を 1 つ作り、床の断面へ割り当てる。
231    pub slab_concrete: String,
232}
233
234/// ウィザードが作る床の断面の符号。
235pub const SLAB_SECTION_NAME: &str = "S15";
236
237/// 床のコンクリートの既定グレード。
238pub const DEFAULT_SLAB_CONCRETE: &str = "Fc21";
239
240impl Default for FrameSpec {
241    fn default() -> Self {
242        Self {
243            origin: [0.0, 0.0],
244            base_elevation: 0.0,
245            x_spans: vec![6000.0, 6000.0],
246            y_spans: vec![6000.0],
247            story_heights: vec![4000.0, 3500.0, 3500.0],
248            story_names: Vec::new(),
249            x_group_name: "X".to_string(),
250            y_group_name: "Y".to_string(),
251            base_support: BaseSupport::Pinned,
252            with_girders: true,
253            with_slabs: true,
254            slab_usage: Some(SlabUsage::Office),
255            slab_thickness: 150.0,
256            slab_concrete: DEFAULT_SLAB_CONCRETE.to_string(),
257        }
258    }
259}
260
261impl FrameSpec {
262    /// X 方向の通りの座標 [mm](原点から各スパンを積み上げる)。
263    pub fn x_coords(&self) -> Vec<f64> {
264        accumulate(self.origin[0], &self.x_spans)
265    }
266
267    /// Y 方向の通りの座標 [mm]。
268    pub fn y_coords(&self) -> Vec<f64> {
269        accumulate(self.origin[1], &self.y_spans)
270    }
271
272    /// 基部を含むレベル [mm](先頭が基部)。
273    pub fn levels(&self) -> Vec<f64> {
274        accumulate(self.base_elevation, &self.story_heights)
275    }
276
277    /// 入力の不備を日本語で返す(`None` なら生成できる)。
278    ///
279    /// スパン・階高は正の値でなければならない。0 以下を許すと同じ位置に節点が
280    /// 重なり、長さ 0 の部材ができて剛性行列が特異になる。
281    pub fn validate(&self) -> Option<String> {
282        if self.story_heights.is_empty() {
283            return Some("階高を 1 つ以上入力してください。".into());
284        }
285        if self.x_spans.iter().chain(&self.y_spans).any(|s| *s <= 0.0) {
286            return Some("スパンは正の値で入力してください。".into());
287        }
288        if self.story_heights.iter().any(|h| *h <= 0.0) {
289            return Some("階高は正の値で入力してください。".into());
290        }
291        if self.with_slabs && self.slab_thickness <= 0.0 {
292            return Some("床の板厚は正の値で入力してください。".into());
293        }
294        if self.with_slabs && self.slab_concrete_preset().is_none() {
295            return Some(format!(
296                "床のコンクリート「{}」は標準材料にありません。",
297                self.slab_concrete
298            ));
299        }
300        None
301    }
302
303    /// 床のコンクリートに対応する標準材料プリセット。名称が一致しなければ `None`。
304    fn slab_concrete_preset(&self) -> Option<MaterialPreset> {
305        material_presets()
306            .into_iter()
307            .find(|p| p.name == self.slab_concrete)
308    }
309
310    /// 生成される節点数・柱本数・梁本数・床枚数。ウィザードが実行前に規模を示すために使う。
311    pub fn counts(&self) -> FrameCounts {
312        let nx = self.x_spans.len() + 1;
313        let ny = self.y_spans.len() + 1;
314        let n_story = self.story_heights.len();
315        // レベル数 = 階高の数 + 1(基部を含む)。
316        let n_level = n_story + 1;
317        let columns = nx * ny * n_story;
318        let per_level = self.x_spans.len() * ny + self.y_spans.len() * nx;
319        // 基部レベルの基礎梁は常に生成し、上のレベルの大梁は `with_girders` に従う。
320        let girders = per_level * if self.with_girders { n_level } else { 1 };
321        let slabs = if self.with_slabs {
322            self.x_spans.len() * self.y_spans.len() * n_level
323        } else {
324            0
325        };
326        FrameCounts {
327            nodes: nx * ny * n_level,
328            columns,
329            girders,
330            slabs,
331        }
332    }
333}
334
335/// [`FrameSpec::counts`] の結果。
336#[derive(Clone, Copy, Debug, PartialEq, Eq)]
337pub struct FrameCounts {
338    pub nodes: usize,
339    pub columns: usize,
340    pub girders: usize,
341    pub slabs: usize,
342}
343
344/// 始点から差分を積み上げた列(先頭は始点そのもの)。
345fn accumulate(start: f64, steps: &[f64]) -> Vec<f64> {
346    let mut out = Vec::with_capacity(steps.len() + 1);
347    let mut v = start;
348    out.push(v);
349    for s in steps {
350        v += *s;
351        out.push(v);
352    }
353    out
354}
355
356/// 架構の生成結果。呼び出し側が新規モデルとして読み込む。
357#[derive(Clone, Debug, PartialEq)]
358pub struct FrameGenResult {
359    pub nodes: Vec<Node>,
360    pub elements: Vec<ElementData>,
361    pub axes: Vec<AxisGroup>,
362    pub stories: Vec<Story>,
363    /// 床の断面(`with_slabs` のときだけ 1 枚)。
364    pub sections: Vec<Section>,
365    /// 床のコンクリート(`with_slabs` のときだけ 1 つ。断面が参照する)。
366    pub materials: Vec<Material>,
367    /// 床領域(大梁の 1 スパン区画。格子状の 1 マスにつき 1 つずつ)。
368    pub floor_regions: Vec<FloorRegion>,
369    /// 床板(`with_slabs` のときだけ、床領域 1 つにつき 1 枚)。
370    pub slabs: Vec<Slab>,
371}
372
373/// スパンと階高から架構(節点・柱・大梁・柱脚支点・通り芯・階)を生成する。
374///
375/// 生成規則:
376///
377/// 1. **節点**は全格子点(X 通り × Y 通り × レベル)に置く。ID は
378///    X → Y → レベルの順の連番で、`ID = 配列添字`の不変条件を満たす。
379/// 2. **柱**は各格子点で上下に隣り合うレベルを結ぶ。基部から最上階まで通す。
380/// 3. **基礎梁**は基部レベルで、隣り合う通りの間に架ける。日本の建築構造では
381///    基礎梁は必ず設けるため、オプションを設けず常に生成する(`with_girders` に
382///    依存しない)。
383/// 4. **大梁**は基部より上の各レベルで、隣り合う通りの間に架ける
384///    (`with_girders` が false なら作らない)。
385/// 5. **柱脚支点**は最下レベルの節点へ [`FrameSpec::base_support`] の拘束を与える。
386/// 6. **通り芯**は X 方向・Y 方向のグループを新設し、通り名は `{接頭辞}{番号}`
387///    (座標の昇順に 1 から)とする。所属節点はその通りの全格子点。出所は
388///    [`AxisSource::Manual`] とし、あとで柱位置からの自動生成を実行しても
389///    作り直されないようにする。
390/// 7. **階**は全レベルに 1 つずつ作る(**先頭は基部の床**)。階は床であり、
391///    その列の先頭が基部であることが `squid_n_core::model::story` の不変条件である。
392///    階名は入力を優先し、空なら [`default_story_name`] で補う。所属節点・剛床・
393///    地震用重量は準備計算が算定する派生値のため、ここでは空のままとする。
394/// 8. **床**は全レベルで、隣り合う通りに囲まれた格子床領域へ 1 枚ずつ作る
395///    (`with_slabs` が false、または片方向の通りが 1 本のときは作らない)。
396///    板厚 [`FrameSpec::slab_thickness`] の断面 [`SLAB_SECTION_NAME`] を 1 枚だけ
397///    作り、全階の床で共有する。断面には [`FrameSpec::slab_concrete`] の
398///    コンクリートを 1 つ作って割り当てる。
399///
400/// 部材(柱・大梁)の断面・材料は割り当てない(モジュールドキュメント参照)。
401/// 入力が不正な場合([`FrameSpec::validate`])はその説明を `Err` で返す。
402pub fn generate_frame(spec: &FrameSpec) -> Result<FrameGenResult, String> {
403    if let Some(msg) = spec.validate() {
404        return Err(msg);
405    }
406    let xs = spec.x_coords();
407    let ys = spec.y_coords();
408    let zs = spec.levels();
409    let (nx, ny, nz) = (xs.len(), ys.len(), zs.len());
410
411    // 節点 ID は X → Y → レベルの順の連番。格子の添字から ID を引けるようにする。
412    let nid = |ix: usize, iy: usize, iz: usize| NodeId(((ix * ny + iy) * nz + iz) as u32);
413
414    let mut nodes = Vec::with_capacity(nx * ny * nz);
415    for (ix, x) in xs.iter().enumerate() {
416        for (iy, y) in ys.iter().enumerate() {
417            for (iz, z) in zs.iter().enumerate() {
418                nodes.push(Node {
419                    id: nid(ix, iy, iz),
420                    coord: [*x, *y, *z],
421                    restraint: if iz == 0 {
422                        spec.base_support.mask()
423                    } else {
424                        Dof6Mask::FREE
425                    },
426                    mass: None,
427                    story: None,
428                    support_spring: None,
429                });
430            }
431        }
432    }
433
434    let mut elements: Vec<ElementData> = Vec::new();
435    let push_member = |a: NodeId, b: NodeId, vertical: bool, elements: &mut Vec<ElementData>| {
436        elements.push(ElementData {
437            id: ElemId(elements.len() as u32),
438            kind: ElementKind::Beam,
439            nodes: [a, b].into_iter().collect(),
440            // 柱は材軸が鉛直なので、局所 y 軸の基準ベクトルに鉛直を使えない。
441            // 柱はグローバル X、梁はグローバル Z を基準とする(線材の局所座標系の
442            // 一般的な取り方)。
443            local_axis: LocalAxis {
444                ref_vector: default_local_ref_vector(vertical),
445            },
446            section: None,
447            end_cond: [EndCondition::Fixed, EndCondition::Fixed],
448            force_regime: ForceRegime::Auto,
449            rigid_zone: Default::default(),
450            plastic_zone: None,
451            spring: None,
452        });
453    };
454
455    // 柱(各格子点で上下に隣り合うレベルを結ぶ)。
456    for ix in 0..nx {
457        for iy in 0..ny {
458            for iz in 0..nz - 1 {
459                push_member(nid(ix, iy, iz), nid(ix, iy, iz + 1), true, &mut elements);
460            }
461        }
462    }
463    // 水平材(各レベルで隣り合う通りを結ぶ)。基部レベル(iz == 0)は基礎梁で、
464    // 日本の建築構造では必ず設けるため常に生成する。基部より上の大梁は
465    // `with_girders` に従う。
466    for iz in 0..nz {
467        if iz > 0 && !spec.with_girders {
468            continue;
469        }
470        for iy in 0..ny {
471            for ix in 0..nx - 1 {
472                push_member(nid(ix, iy, iz), nid(ix + 1, iy, iz), false, &mut elements);
473            }
474        }
475        for ix in 0..nx {
476            for iy in 0..ny - 1 {
477                push_member(nid(ix, iy, iz), nid(ix, iy + 1, iz), false, &mut elements);
478            }
479        }
480    }
481
482    // 通り芯。X 方向グループは離れを +X 向きに測る(方向角 270°)。
483    let x_group = AxisGroup {
484        name: spec.x_group_name.clone(),
485        kind: AxisGroupKind::Parallel {
486            origin: spec.origin,
487            angle_deg: 270.0,
488        },
489        axes: xs
490            .iter()
491            .enumerate()
492            .map(|(ix, x)| Axis {
493                name: format!("{}{}", spec.x_group_name, ix + 1),
494                distance: Some(x - spec.origin[0]),
495                nodes: (0..ny)
496                    .flat_map(|iy| (0..nz).map(move |iz| nid(ix, iy, iz)))
497                    .collect(),
498                source: AxisSource::Manual,
499            })
500            .collect(),
501    };
502    let y_group = AxisGroup {
503        name: spec.y_group_name.clone(),
504        kind: AxisGroupKind::Parallel {
505            origin: spec.origin,
506            angle_deg: 0.0,
507        },
508        axes: ys
509            .iter()
510            .enumerate()
511            .map(|(iy, y)| Axis {
512                name: format!("{}{}", spec.y_group_name, iy + 1),
513                distance: Some(y - spec.origin[1]),
514                nodes: (0..nx)
515                    .flat_map(|ix| (0..nz).map(move |iz| nid(ix, iy, iz)))
516                    .collect(),
517                source: AxisSource::Manual,
518            })
519            .collect(),
520    };
521
522    // 階(全レベル。先頭が基部の床)。階は床であり、基部の床も階として作る
523    // (`squid_n_core::model::story` の不変条件)。
524    let stories = zs
525        .iter()
526        .enumerate()
527        .map(|(si, z)| {
528            let name = spec
529                .story_names
530                .get(si)
531                .map(|s| s.trim().to_string())
532                .filter(|s| !s.is_empty())
533                .unwrap_or_else(|| default_story_name(si));
534            Story {
535                id: StoryId(si as u32),
536                name,
537                elevation: *z,
538                node_ids: Vec::new(),
539                seismic_weight: None,
540                weight_override: None,
541                structure: Default::default(),
542                level_kind: Default::default(),
543            }
544        })
545        .collect();
546
547    // 床(全レベルで、隣り合う通りに囲まれた格子区画 1 枚ずつ)。
548    // 板厚と自重は断面と材料からしか解決できないため、床を作るなら断面とコンクリートも
549    // あわせて作る(モジュールドキュメント参照)。
550    let mut sections = Vec::new();
551    let mut materials = Vec::new();
552    let mut floor_regions = Vec::new();
553    let mut slabs = Vec::new();
554    if spec.with_slabs && nx >= 2 && ny >= 2 {
555        let sec_id = SectionId(0);
556        let mat_id = MaterialId(0);
557        // `validate` が名称を確かめているため、ここでプリセットは必ず見つかる。
558        let preset = spec
559            .slab_concrete_preset()
560            .expect("床のコンクリートは validate で検査済み");
561        materials.push(Material {
562            id: mat_id,
563            name: preset.name.to_string(),
564            category: preset.category,
565            young: preset.young,
566            poisson: preset.poisson,
567            density: preset.density,
568            shear: None,
569            fc: preset.fc,
570            fy: preset.fy,
571            concrete_class: Default::default(),
572            strength_factor: None,
573        });
574        let mut section = crate::section_shape::SectionShape::RcSlab {
575            thickness: spec.slab_thickness,
576        }
577        .to_section(sec_id, SLAB_SECTION_NAME.to_string());
578        section.material = Some(mat_id);
579        sections.push(section);
580        for iz in 0..nz {
581            for ix in 0..nx - 1 {
582                for iy in 0..ny - 1 {
583                    // 境界は反時計回り(面積算定の巻き方向をそろえる)。
584                    let boundary = vec![
585                        nid(ix, iy, iz),
586                        nid(ix + 1, iy, iz),
587                        nid(ix + 1, iy + 1, iz),
588                        nid(ix, iy + 1, iz),
589                    ];
590                    let region_id = FloorRegionId(floor_regions.len() as u32);
591                    let slab_id = SlabId(slabs.len() as u32);
592                    slabs.push(Slab {
593                        id: slab_id,
594                        shape: SlabShape::Enclosed {
595                            boundary: boundary.clone(),
596                        },
597                        plate: SlabPlate {
598                            section: Some(sec_id),
599                            usage: spec.slab_usage,
600                            ..Default::default()
601                        },
602                    });
603                    let mut region = FloorRegion::new(region_id, boundary);
604                    region.slab_ids.push(slab_id);
605                    floor_regions.push(region);
606                }
607            }
608        }
609    }
610
611    Ok(FrameGenResult {
612        nodes,
613        elements,
614        axes: vec![x_group, y_group],
615        stories,
616        sections,
617        materials,
618        floor_regions,
619        slabs,
620    })
621}
622
623/// 生成した架構を標準の荷重ケース付きの新規モデルとして組み立てる。
624///
625/// 架構作成ウィザードは新規モデルを作る操作なので、既存モデルへの追記ではなく
626/// モデルの差し替えとして扱う(呼び出し側が undo 履歴ごと入れ替える)。
627pub fn frame_model(spec: &FrameSpec) -> Result<Model, String> {
628    let gen = generate_frame(spec)?;
629    Ok(Model {
630        nodes: gen.nodes,
631        elements: gen.elements,
632        axes: gen.axes,
633        stories: gen.stories,
634        sections: gen.sections,
635        materials: gen.materials,
636        floor_regions: gen.floor_regions,
637        slabs: gen.slabs,
638        ..Model::with_default_load_cases()
639    })
640}
641
642#[cfg(test)]
643mod tests {
644    use super::*;
645
646    /// 2×1 スパン・3 階の架構が、格子どおりの節点・柱・大梁と柱脚支点を持つ。
647    #[test]
648    fn test_generate_frame_counts_and_supports() {
649        let spec = FrameSpec {
650            base_support: BaseSupport::Fixed,
651            ..FrameSpec::default()
652        };
653        let counts = spec.counts();
654        let model = frame_model(&spec).unwrap();
655
656        // 3 通り × 2 通り × 4 レベル。
657        assert_eq!(model.nodes.len(), 3 * 2 * 4);
658        assert_eq!(model.nodes.len(), counts.nodes);
659        // 柱 = 格子点 6 × 3 層。梁 = (2×2 + 1×3) × 4 レベル
660        //(基部レベルの基礎梁を含む。基礎梁は常に生成する)。
661        assert_eq!(counts.columns, 6 * 3);
662        assert_eq!(counts.girders, (2 * 2 + 3) * 4);
663        assert_eq!(model.elements.len(), counts.columns + counts.girders);
664
665        // 基礎梁は基部レベルに架かる。
666        let base_beams = model
667            .elements
668            .iter()
669            .filter(|e| {
670                e.nodes.len() == 2
671                    && e.nodes
672                        .iter()
673                        .all(|n| model.nodes[n.index()].coord[2] == 0.0)
674            })
675            .count();
676        assert_eq!(base_beams, 2 * 2 + 3, "基礎梁が基部レベルに架かる");
677
678        // 柱脚は基部レベルの 6 節点だけが固定。
679        let fixed: Vec<&Node> = model
680            .nodes
681            .iter()
682            .filter(|n| n.restraint == Dof6Mask::FIXED)
683            .collect();
684        assert_eq!(fixed.len(), 6);
685        assert!(fixed.iter().all(|n| n.coord[2] == 0.0));
686
687        // ID = 配列添字・参照整合。
688        assert!(model.validate().is_ok(), "{:?}", model.validate());
689    }
690
691    /// 部材の断面は割り当てない(利用者が決めるため)。
692    #[test]
693    fn test_generated_members_have_no_section() {
694        let model = frame_model(&FrameSpec::default()).unwrap();
695        assert!(model.elements.iter().all(|e| e.section.is_none()));
696    }
697
698    /// 柱脚の既定はピンで、並進 3 自由度だけが拘束される。
699    #[test]
700    fn test_pinned_base_is_default() {
701        let spec = FrameSpec::default();
702        assert_eq!(spec.base_support, BaseSupport::Pinned);
703        let model = frame_model(&spec).unwrap();
704        let base = model.nodes.iter().find(|n| n.coord[2] == 0.0).unwrap();
705        assert!(base.restraint.is_fixed(Dof::Ux));
706        assert!(base.restraint.is_fixed(Dof::Uz));
707        assert!(!base.restraint.is_fixed(Dof::Rx));
708    }
709
710    /// 通り芯と階が入力どおりに作られ、通り名は座標の昇順に 1 から振られる。
711    #[test]
712    fn test_generated_axes_and_stories() {
713        let model = frame_model(&FrameSpec::default()).unwrap();
714        let names: Vec<&str> = model.axes[0].axes.iter().map(|a| a.name.as_str()).collect();
715        assert_eq!(names, vec!["X1", "X2", "X3"]);
716        let dists: Vec<f64> = model.axes[0]
717            .axes
718            .iter()
719            .filter_map(|a| a.distance)
720            .collect();
721        assert_eq!(dists, vec![0.0, 6000.0, 12000.0]);
722        let names: Vec<&str> = model.axes[1].axes.iter().map(|a| a.name.as_str()).collect();
723        assert_eq!(names, vec!["Y1", "Y2"]);
724
725        // 階は床であり、先頭は基部の床。床基準の既定名で標高の昇順に並ぶ。
726        let stories: Vec<(&str, f64)> = model
727            .stories
728            .iter()
729            .map(|s| (s.name.as_str(), s.elevation))
730            .collect();
731        assert_eq!(
732            stories,
733            vec![("1F", 0.0), ("2F", 4000.0), ("3F", 7500.0), ("4F", 11000.0)]
734        );
735        // 不変条件: 階の列の先頭は基部。
736        assert_eq!(model.stories[0].elevation, model.base_elevation());
737        // 3 階建ての層は 3 つ。名前は下端の階(法令の「i 階」)。
738        let layers: Vec<(String, f64)> = model
739            .layers()
740            .into_iter()
741            .map(|l| (l.name, l.height))
742            .collect();
743        assert_eq!(
744            layers,
745            vec![
746                ("1F".to_string(), 4000.0),
747                ("2F".to_string(), 3500.0),
748                ("3F".to_string(), 3500.0)
749            ]
750        );
751    }
752
753    /// 階名を入力すればそれを使い、空欄は既定名で補う。
754    /// 階名は床ごとなので、階高より 1 つ多く入力できる(先頭が基部の床)。
755    #[test]
756    fn test_story_names_from_spec() {
757        let spec = FrameSpec {
758            story_names: vec!["GL".into(), "2FL".into(), "  ".into(), "RFL".into()],
759            ..FrameSpec::default()
760        };
761        let model = frame_model(&spec).unwrap();
762        let names: Vec<&str> = model.stories.iter().map(|s| s.name.as_str()).collect();
763        assert_eq!(names, vec!["GL", "2FL", "3F", "RFL"]);
764    }
765
766    /// スパン・階高が 0 以下の入力は、長さ 0 の部材を作らずエラーにする。
767    #[test]
768    fn test_invalid_spec_is_rejected() {
769        let zero_span = FrameSpec {
770            x_spans: vec![6000.0, 0.0],
771            ..FrameSpec::default()
772        };
773        assert!(frame_model(&zero_span).is_err());
774        let no_story = FrameSpec {
775            story_heights: Vec::new(),
776            ..FrameSpec::default()
777        };
778        assert!(frame_model(&no_story).is_err());
779        let zero_thickness = FrameSpec {
780            slab_thickness: 0.0,
781            ..FrameSpec::default()
782        };
783        assert!(frame_model(&zero_thickness).is_err());
784        // 標準材料にないグレードは、床の材料が決まらないため生成しない。
785        let unknown_concrete = FrameSpec {
786            slab_concrete: "Fc999".into(),
787            ..FrameSpec::default()
788        };
789        assert!(frame_model(&unknown_concrete).is_err());
790        // 床を作らないなら床のコンクリートは使わないので、名称は問わない。
791        let no_slabs = FrameSpec {
792            with_slabs: false,
793            slab_concrete: String::new(),
794            ..FrameSpec::default()
795        };
796        assert!(frame_model(&no_slabs).is_ok());
797    }
798
799    /// 床は各レベルの各格子区画に 1 枚ずつ作り、板厚 150 mm の断面 `S15` を共有する。
800    ///
801    /// 床の板厚と自重は断面と材料からしか解決できないため、床を作るなら断面と
802    /// コンクリートもあわせて作る。
803    #[test]
804    fn test_generated_slabs_share_one_section() {
805        let spec = FrameSpec::default();
806        let model = frame_model(&spec).unwrap();
807        // 2×1 スパン × 4 レベル(基部を含む)= 8 枚。
808        assert_eq!(model.floor_regions.len(), 2 * 4);
809        assert_eq!(model.floor_regions.len(), spec.counts().slabs);
810        assert_eq!(model.sections.len(), 1, "床の断面は 1 枚だけ");
811        let sec = &model.sections[0];
812        assert_eq!(sec.name, SLAB_SECTION_NAME);
813        assert_eq!(sec.floor, None, "階を持たない断面として作る");
814        assert_eq!(sec.thickness, Some(150.0));
815        assert_eq!(sec.material, Some(crate::ids::MaterialId(0)));
816        assert_eq!(
817            model.slabs.len(),
818            model.floor_regions.len(),
819            "床領域ごとに床板 1 枚"
820        );
821        assert!(model
822            .slabs
823            .iter()
824            .all(|sl| sl.section() == Some(crate::ids::SectionId(0))));
825        assert!(model.slabs.iter().all(|sl| sl.usage() == spec.slab_usage));
826        for sl in &model.floor_regions {
827            let boundary = &sl.boundary;
828            assert_eq!(boundary.len(), 4);
829            let zs: Vec<f64> = boundary
830                .iter()
831                .map(|n| model.nodes[n.index()].coord[2])
832                .collect();
833            assert!(zs.windows(2).all(|w| w[0] == w[1]), "床は水平");
834        }
835        assert!(
836            model
837                .floor_regions
838                .iter()
839                .any(|sl| sl.level(&model) == Some(0.0)),
840            "基部レベルにも床を作る"
841        );
842        assert!(model.validate().is_ok(), "{:?}", model.validate());
843    }
844
845    /// 床のコンクリートは既定で Fc21 の 1 つだけを作り、標準材料の規格値を持つ。
846    ///
847    /// 床は解析対象外の二次部材で、材料は自重を決める入力である。既定のまま生成しても
848    /// 自重が入り、解析前チェックの「断面に材料が未割当」で止まらない。
849    #[test]
850    fn test_slab_concrete_is_created() {
851        let model = frame_model(&FrameSpec::default()).unwrap();
852        assert_eq!(model.materials.len(), 1, "床のコンクリートだけを作る");
853        let mat = &model.materials[0];
854        assert_eq!(mat.name, DEFAULT_SLAB_CONCRETE);
855        assert_eq!(mat.category, crate::model::MaterialCategory::Concrete);
856        assert_eq!(mat.fc, Some(21.0));
857        assert!(mat.density > 0.0, "自重が 0 にならない");
858        assert!(model.validate().is_ok(), "{:?}", model.validate());
859
860        // グレードを変えれば、その規格値の材料になる。
861        let spec = FrameSpec {
862            slab_concrete: "Fc30".into(),
863            ..FrameSpec::default()
864        };
865        let model = frame_model(&spec).unwrap();
866        assert_eq!(model.materials[0].name, "Fc30");
867        assert_eq!(model.materials[0].fc, Some(30.0));
868    }
869
870    /// 床を作らない設定では断面もコンクリートも作らない。
871    #[test]
872    fn test_frame_without_slabs() {
873        let spec = FrameSpec {
874            with_slabs: false,
875            ..FrameSpec::default()
876        };
877        let model = frame_model(&spec).unwrap();
878        assert!(model.floor_regions.is_empty());
879        assert!(model.sections.is_empty());
880        assert!(model.materials.is_empty());
881        assert_eq!(spec.counts().slabs, 0);
882    }
883
884    /// 基礎梁は常に生成する(オプションを設けない)。大梁を作らない設定でも残る。
885    ///
886    /// 日本の RC・S 造では基礎梁は必ず設けるため、基礎梁のない架構は出発点の
887    /// モデルとして不正である。
888    #[test]
889    fn test_foundation_girders_are_always_generated() {
890        let spec = FrameSpec {
891            with_girders: false,
892            ..FrameSpec::default()
893        };
894        let model = frame_model(&spec).unwrap();
895        let at_base = |e: &crate::model::ElementData| {
896            e.nodes.len() == 2
897                && e.nodes
898                    .iter()
899                    .all(|n| model.nodes[n.index()].coord[2] == 0.0)
900        };
901        let base_beams = model.elements.iter().filter(|e| at_base(e)).count();
902        assert_eq!(base_beams, 2 * 2 + 3, "大梁 OFF でも基礎梁は作る");
903        assert_eq!(spec.counts().girders, base_beams);
904        // 基部より上には梁がない。
905        let upper = model.elements.iter().filter(|e| {
906            e.nodes.len() == 2 && {
907                let a = model.nodes[e.nodes[0].index()].coord[2];
908                let b = model.nodes[e.nodes[1].index()].coord[2];
909                (a - b).abs() < 1e-9 && a > 0.0
910            }
911        });
912        assert_eq!(upper.count(), 0, "大梁 OFF なら基部より上に梁はない");
913    }
914
915    /// 伏図(階の切り出し)が、準備計算を通していない生成直後のモデルでも成立する。
916    ///
917    /// 帰属は幾何([`Model::node_stories`])から引くため、`Node::story` が
918    /// 未設定でも階を指定すれば部材が出る。基部の階には柱脚と基礎梁が属する。
919    #[test]
920    fn test_generated_frame_is_visible_in_story_frames() {
921        let model = frame_model(&FrameSpec::default()).unwrap();
922        assert!(
923            model.nodes.iter().all(|n| n.story.is_none()),
924            "前提: 生成直後は所属階が未設定(準備計算が埋める派生値)"
925        );
926        for story in &model.stories {
927            let frame =
928                crate::frame::build_frame(&model, crate::frame::FrameTarget::Story(story.id))
929                    .expect("階の構面を切り出せる");
930            assert!(
931                frame.elem_count() > 0,
932                "階 {} の伏図に部材が出る",
933                story.name
934            );
935        }
936        // 基部の階には基礎梁が属する。
937        let base = crate::frame::build_frame(
938            &model,
939            crate::frame::FrameTarget::Story(model.stories[0].id),
940        )
941        .unwrap();
942        let base_beams = model
943            .elements
944            .iter()
945            .enumerate()
946            .filter(|(i, e)| {
947                base.elem_on[*i]
948                    && e.nodes.len() == 2
949                    && e.nodes
950                        .iter()
951                        .all(|n| model.nodes[n.index()].coord[2] == 0.0)
952            })
953            .count();
954        assert_eq!(base_beams, 2 * 2 + 3, "基礎伏図に基礎梁が出る");
955    }
956
957    /// 片方向の通りが 1 本だけなら格子区画ができないため、床は作らない。
958    #[test]
959    fn test_no_slabs_without_grid_region() {
960        let spec = FrameSpec {
961            y_spans: Vec::new(),
962            ..FrameSpec::default()
963        };
964        let model = frame_model(&spec).unwrap();
965        assert!(model.floor_regions.is_empty());
966        assert!(model.sections.is_empty());
967        assert!(model.materials.is_empty());
968    }
969
970    /// 生成した架構の通り芯と階から、元の格子が復元できる。
971    /// 3D ビューのグリッド描画・スナップはこの復元結果を使う。
972    #[test]
973    fn test_space_grid_round_trips_generated_frame() {
974        let model = frame_model(&FrameSpec::default()).unwrap();
975        let grid = super::space_grid(&model);
976        let xs: Vec<f64> = grid.x_lines.iter().map(|l| l.coord).collect();
977        assert_eq!(xs, vec![0.0, 6000.0, 12000.0]);
978        let ys: Vec<f64> = grid.y_lines.iter().map(|l| l.coord).collect();
979        assert_eq!(ys, vec![0.0, 6000.0]);
980        let levels: Vec<(&str, f64)> = grid
981            .levels
982            .iter()
983            .map(|l| (l.name.as_str(), l.elevation))
984            .collect();
985        assert_eq!(
986            levels,
987            vec![("GL", 0.0), ("2F", 4000.0), ("3F", 7500.0), ("4F", 11000.0)]
988        );
989        // 格子点は 3 × 2 × 4。
990        assert_eq!(grid.points().count(), 24);
991        assert!(!grid.is_empty());
992    }
993
994    /// 通り芯を持たないモデルの格子は空(描く格子がない)。
995    #[test]
996    fn test_space_grid_without_axes_is_empty() {
997        let model = Model::default();
998        assert!(super::space_grid(&model).is_empty());
999    }
1000
1001    /// 方向角 90°(離れを -X 向きに測る)のグループでも、グローバル座標へ
1002    /// 正しく直してから昇順に並べる。
1003    #[test]
1004    fn test_space_grid_handles_reversed_offset_direction() {
1005        let mut model = frame_model(&FrameSpec::default()).unwrap();
1006        model.axes = vec![
1007            AxisGroup {
1008                name: "X".into(),
1009                kind: AxisGroupKind::Parallel {
1010                    origin: [0.0, 0.0],
1011                    angle_deg: 90.0,
1012                },
1013                axes: vec![
1014                    Axis {
1015                        name: "X3".into(),
1016                        distance: Some(-12000.0),
1017                        nodes: Vec::new(),
1018                        source: AxisSource::Manual,
1019                    },
1020                    Axis {
1021                        name: "X1".into(),
1022                        distance: Some(0.0),
1023                        nodes: Vec::new(),
1024                        source: AxisSource::Manual,
1025                    },
1026                ],
1027            },
1028            AxisGroup {
1029                name: "Y".into(),
1030                kind: AxisGroupKind::Parallel {
1031                    origin: [0.0, 0.0],
1032                    angle_deg: 0.0,
1033                },
1034                axes: vec![Axis {
1035                    name: "Y1".into(),
1036                    distance: Some(0.0),
1037                    nodes: Vec::new(),
1038                    source: AxisSource::Manual,
1039                }],
1040            },
1041        ];
1042        let grid = super::space_grid(&model);
1043        let xs: Vec<(&str, f64)> = grid
1044            .x_lines
1045            .iter()
1046            .map(|l| (l.name.as_str(), l.coord))
1047            .collect();
1048        assert_eq!(xs, vec![("X1", 0.0), ("X3", 12000.0)]);
1049    }
1050
1051    /// 同じ位置の通りが重複しているモデルでは、格子線を 1 本にまとめる。
1052    #[test]
1053    fn test_space_grid_merges_duplicate_lines() {
1054        let mut model = frame_model(&FrameSpec::default()).unwrap();
1055        let dup = model.axes[0].axes[1].clone();
1056        model.axes[0].axes.push(Axis {
1057            name: "X2b".into(),
1058            ..dup
1059        });
1060        let grid = super::space_grid(&model);
1061        let xs: Vec<(&str, f64)> = grid
1062            .x_lines
1063            .iter()
1064            .map(|l| (l.name.as_str(), l.coord))
1065            .collect();
1066        assert_eq!(xs, vec![("X1", 0.0), ("X2", 6000.0), ("X3", 12000.0)]);
1067    }
1068}