Skip to main content

squid_n_core/
axis_gen.rs

1//! 通り芯([`AxisGroup`])の自動生成。
2//!
3//! 柱が立つ平面位置から X 方向・Y 方向の通り芯を作る。通り芯は構造計算に用いない
4//! 識別用のデータであり、生成は準備計算の一部ではなく利用者が明示的に実行する
5//! 操作(モデルタブの「柱位置から自動生成」)である。
6//!
7//! 生成規則:
8//!
9//! 1. **対象**は柱(線材のうち両端の水平距離が [`crate::geom::VERTICAL_TOL_MM`]
10//!    未満のもの=[`crate::geom::is_vertical_pair`])の材端節点のみ。梁・ブレース・
11//!    二次部材・壁は対象にしない(通り芯を「柱が立つ位置」と定義する)。剛床代表節点は
12//!    要素が接続しないため自然に対象外となる。
13//! 2. **追加先グループ**は幾何で突き合わせる。離れを測る向きがグローバル X 軸に沿う
14//!    平行芯グループ([`AxisGroupKind::plan_dir`] が [`AxisPlanDir::X`])があれば
15//!    そこへ追加し、なければ `X` グループ(原点 `(0,0)`・方向角 270°)を新設する。
16//!    Y 方向も同様(新設時の方向角は 0°)。離れはそのグループの原点・方向角で測るため、
17//!    取り込んだグループの符号規約にそのまま従う。
18//! 3. **クラスタリング**は離れの昇順に [`AXIS_TOL_MM`] の許容差で行う。
19//! 4. **既存優先**。同一グループ内に許容差以内の通りが既にあれば、その位置には
20//!    新しい通りを作らない([`AxisSource::Manual`] の通り=手動作成・ST-Bridge
21//!    取り込み・利用者が改名したものを保護する)。
22//! 5. **採番**は離れの昇順に、そのグループ内で未使用の最小の正整数 `n` を用いて
23//!    `{グループ名}{n}` とする。
24//!
25//! 既存の [`AxisSource::Auto`] の通りは毎回破棄して作り直す。
26//! [`AxisGroupKind::Other`] のグループ(円弧芯・放射芯・作図芯)は対象外で、
27//! そのまま保持する。
28
29use crate::geom::is_vertical_pair;
30use crate::ids::NodeId;
31use crate::model::{Axis, AxisGroup, AxisGroupKind, AxisPlanDir, AxisSource, ElementKind, Model};
32
33/// 同一の通りとみなす離れの差 [mm]。
34pub const AXIS_TOL_MM: f64 = 1.0;
35
36/// 新設する X 方向グループの名前と方向角 [度](芯線は Y 軸に平行、離れは +X 向き)。
37const X_GROUP: (&str, f64) = ("X", 270.0);
38/// 新設する Y 方向グループの名前と方向角 [度](芯線は X 軸に平行、離れは +Y 向き)。
39const Y_GROUP: (&str, f64) = ("Y", 0.0);
40
41/// 柱位置から通り芯を自動生成し、適用後の [`Model::axes`] の全量を返す。
42///
43/// モデルは変更しない(呼び出し側が編集コマンド経由で適用する)。
44/// 生成規則はモジュールドキュメントを参照。
45pub fn generate_axes(model: &Model) -> Vec<AxisGroup> {
46    // 柱の材端節点(平面位置つき)。同じ位置に上下階の柱が積まれるため節点は重複し得る。
47    let mut column_nodes: Vec<NodeId> = Vec::new();
48    for e in &model.elements {
49        if !matches!(e.kind, ElementKind::Beam) || e.nodes.len() != 2 {
50            continue;
51        }
52        let (Some(a), Some(b)) = (
53            model.nodes.get(e.nodes[0].index()),
54            model.nodes.get(e.nodes[1].index()),
55        ) else {
56            continue;
57        };
58        if !is_vertical_pair(a.coord, b.coord) {
59            continue;
60        }
61        column_nodes.push(a.id);
62        column_nodes.push(b.id);
63    }
64    column_nodes.sort();
65    column_nodes.dedup();
66
67    // 既存の自動生成分を捨てる(手動・取り込み由来は保持)。
68    let mut groups: Vec<AxisGroup> = model.axes.clone();
69    for g in &mut groups {
70        g.axes.retain(|a| a.source == AxisSource::Manual);
71    }
72
73    for (dir, (default_name, default_angle)) in
74        [(AxisPlanDir::X, X_GROUP), (AxisPlanDir::Y, Y_GROUP)]
75    {
76        let gi = match groups.iter().position(|g| g.kind.plan_dir() == Some(dir)) {
77            Some(i) => i,
78            None => {
79                if column_nodes.is_empty() {
80                    continue;
81                }
82                groups.push(AxisGroup {
83                    name: unused_group_name(&groups, default_name),
84                    kind: AxisGroupKind::Parallel {
85                        origin: [0.0, 0.0],
86                        angle_deg: default_angle,
87                    },
88                    axes: Vec::new(),
89                });
90                groups.len() - 1
91            }
92        };
93        add_generated_axes(&mut groups[gi], model, &column_nodes);
94    }
95
96    groups
97}
98
99/// 既存グループと重複しないグループ名を選ぶ(`X` が使われていれば `X2`, `X3`…)。
100fn unused_group_name(groups: &[AxisGroup], base: &str) -> String {
101    if !groups.iter().any(|g| g.name == base) {
102        return base.to_string();
103    }
104    (2..)
105        .map(|n| format!("{base}{n}"))
106        .find(|name| !groups.iter().any(|g| &g.name == name))
107        .expect("無限イテレータから必ず見つかる")
108}
109
110/// 1 つのグループへ、柱位置から作った通りを追加する。
111fn add_generated_axes(group: &mut AxisGroup, model: &Model, column_nodes: &[NodeId]) {
112    // 柱の材端節点を、このグループの原点・方向角で測った離れへ写す。
113    let mut by_distance: Vec<(f64, NodeId)> = column_nodes
114        .iter()
115        .filter_map(|&id| {
116            let n = model.nodes.get(id.index())?;
117            let d = group.kind.distance_of(n.coord[0], n.coord[1])?;
118            Some((d, id))
119        })
120        .collect();
121    by_distance.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)));
122
123    // 離れの昇順にクラスタリングする(代表値はクラスタ先頭の離れ。
124    // 階生成の Z レベル判定と同じく、代表値との差で判定して連鎖的な流れを防ぐ)。
125    let mut clusters: Vec<(f64, Vec<NodeId>)> = Vec::new();
126    for (d, id) in by_distance {
127        match clusters.last_mut() {
128            Some((rep, nodes)) if (d - *rep).abs() <= AXIS_TOL_MM => nodes.push(id),
129            _ => clusters.push((d, vec![id])),
130        }
131    }
132
133    // 既存の通り(Manual)と同じ位置は作らない(既存優先)。
134    clusters.retain(|(d, _)| {
135        !group
136            .axes
137            .iter()
138            .any(|a| a.distance.is_some_and(|ad| (ad - d).abs() <= AXIS_TOL_MM))
139    });
140
141    for (d, nodes) in clusters {
142        let name = unused_axis_name(group);
143        group.axes.push(Axis {
144            name,
145            distance: Some(d),
146            nodes,
147            source: AxisSource::Auto,
148        });
149    }
150    group.sort_axes();
151}
152
153/// グループ内で未使用の最小の正整数 `n` による通り名 `{グループ名}{n}`。
154fn unused_axis_name(group: &AxisGroup) -> String {
155    (1..)
156        .map(|n| format!("{}{n}", group.name))
157        .find(|name| !group.axes.iter().any(|a| &a.name == name))
158        .expect("無限イテレータから必ず見つかる")
159}
160
161#[cfg(test)]
162mod tests;