squid_n_core/model/axis.rs
1//! 通り芯(軸)の型。
2//!
3//! 通り芯は日本の構造設計で各通りを識別するための呼称であり、**構造計算には
4//! 一切用いない**(応力・断面算定・保有耐力のいずれにも入らない)。モデルの
5//! 節点をまとまりとして名付けるためのデータとして保持する。
6//!
7//! - [`AxisSource`] — 通りの出所(自動生成/利用者)。
8//! - [`AxisGroupKind`] — グループの幾何(平行芯/それ以外)。
9//! - [`AxisPlanDir`] — 平行芯グループが表す平面上の位置の向き。
10//! - [`Axis`] — 1 本の通り芯。
11//! - [`AxisGroup`] — 同じ幾何規則を共有する通り芯のまとまり。
12//!
13//! 通り芯が持つのは所属節点だけで、要素は持たない。所属要素は「すべての材端節点が
14//! その通りに属する要素」として [`super::Model::axis_elements`] が算出する
15//! (算出規則を呼び出し側へ散らさないため、ヘルパーを情報源とする)。
16
17use super::*;
18
19/// 通り芯の出所。
20#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
21pub enum AxisSource {
22 /// 柱位置からの自動生成([`crate::axis_gen::generate_axes`])が作った通り。
23 /// 再生成のたびに作り直される。
24 #[default]
25 Auto,
26 /// 利用者が作成・改名した通り、および ST-Bridge から取り込んだ通り。
27 /// 自動生成では作り直さず、そのまま保持する。
28 Manual,
29}
30
31/// 平行芯グループが表す、平面上の位置の向き。
32///
33/// 平行芯は「芯線に直交する向きの離れ」で位置が決まるため、離れを測る向きが
34/// グローバル X 軸に沿うグループは X 方向(=芯線は Y 軸に平行)、
35/// Y 軸に沿うグループは Y 方向(=芯線は X 軸に平行)を表す。
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum AxisPlanDir {
38 X,
39 Y,
40}
41
42/// 平行芯グループの向き判定に用いる、単位ベクトル成分の許容差。
43const DIR_EPS: f64 = 1e-6;
44
45/// 方向角 [度] の `(sin, cos)`。
46///
47/// 90° の倍数では厳密値(0 / ±1)へ丸める。`270f64.to_radians().sin()` は
48/// -1 ちょうどにならないため、そのまま使うと直交グリッドの通りの離れが
49/// `5999.999999999999` のような値になり、一覧表示にも ST-Bridge の書き出しにも
50/// 残差が現れる。直交グリッドは通り芯の大多数を占めるため、ここで吸収する。
51fn sin_cos_deg(angle_deg: f64) -> (f64, f64) {
52 let quadrant = angle_deg / 90.0;
53 if (quadrant - quadrant.round()).abs() < 1e-9 {
54 return match (quadrant.round() as i64).rem_euclid(4) {
55 0 => (0.0, 1.0),
56 1 => (1.0, 0.0),
57 2 => (0.0, -1.0),
58 _ => (-1.0, 0.0),
59 };
60 }
61 let rad = angle_deg.to_radians();
62 (rad.sin(), rad.cos())
63}
64
65/// 通り芯グループの幾何。
66#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
67pub enum AxisGroupKind {
68 /// 平行芯。原点 `origin` と芯線の方向角 `angle_deg`(度、グローバル X 軸から
69 /// 反時計回り)を持ち、各通りは原点からの符号付き離れ([`Axis::distance`])で
70 /// 位置が決まる。芯線の方向は `(cos θ, sin θ)`、離れを測る向きは
71 /// それを 90° 回した `(−sin θ, cos θ)`([`AxisGroupKind::offset_dir`])。
72 Parallel { origin: [f64; 2], angle_deg: f64 },
73 /// 平行芯以外(円弧芯・放射芯・作図芯)。幾何は保持せず、所属節点の
74 /// まとまりとしてのみ扱う。この種のグループの通りは [`Axis::distance`] が
75 /// `None` になる。
76 Other,
77}
78
79impl AxisGroupKind {
80 /// 離れを測る向きの単位ベクトル `(−sin θ, cos θ)`。`Other` は `None`。
81 pub fn offset_dir(&self) -> Option<[f64; 2]> {
82 match *self {
83 AxisGroupKind::Parallel { angle_deg, .. } => {
84 let (sin, cos) = sin_cos_deg(angle_deg);
85 Some([-sin, cos])
86 }
87 AxisGroupKind::Other => None,
88 }
89 }
90
91 /// 平面上の点 `(x, y)` の、このグループにおける符号付き離れ。`Other` は `None`。
92 pub fn distance_of(&self, x: f64, y: f64) -> Option<f64> {
93 match *self {
94 AxisGroupKind::Parallel { origin, .. } => {
95 let d = self.offset_dir()?;
96 Some((x - origin[0]) * d[0] + (y - origin[1]) * d[1])
97 }
98 AxisGroupKind::Other => None,
99 }
100 }
101
102 /// このグループが表す平面上の位置の向き([`AxisPlanDir`])。
103 /// 離れを測る向きがグローバル軸に沿っていない斜めのグループ、および
104 /// `Other` は `None`。
105 pub fn plan_dir(&self) -> Option<AxisPlanDir> {
106 let [dx, dy] = self.offset_dir()?;
107 if dx.abs() > DIR_EPS && dy.abs() < DIR_EPS {
108 Some(AxisPlanDir::X)
109 } else if dy.abs() > DIR_EPS && dx.abs() < DIR_EPS {
110 Some(AxisPlanDir::Y)
111 } else {
112 None
113 }
114 }
115}
116
117/// 1 本の通り芯。
118#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
119pub struct Axis {
120 /// 通り名(`X1`・`Y2a` など)。図面・一覧での識別に用いる。
121 pub name: String,
122 /// 平行芯グループでの、グループ原点からの符号付き離れ [mm]。
123 /// [`AxisGroupKind::Other`] のグループでは `None`。
124 pub distance: Option<f64>,
125 /// この通りに属する節点。節点の座標が通り線上にある保証はない
126 /// (芯ずれした柱をその通りの所属とするモデル化が実務で行われるため、
127 /// 所属は座標から導かず、このリストを正とする)。
128 pub nodes: Vec<NodeId>,
129 /// 出所。[`AxisSource::Manual`] の通りは自動生成で作り直さない。
130 pub source: AxisSource,
131}
132
133/// 同じ幾何規則を共有する通り芯のまとまり(X 通り・Y 通りなど)。
134#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
135pub struct AxisGroup {
136 /// グループ名(`X`・`Y` など)。自動生成の通り名の接頭辞にもなる。
137 pub name: String,
138 pub kind: AxisGroupKind,
139 /// このグループの通り。[`AxisGroupKind::Parallel`] のグループでは
140 /// [`Axis::distance`] の昇順に保つ([`AxisGroup::sort_axes`])。
141 pub axes: Vec<Axis>,
142}
143
144impl AxisGroup {
145 /// 平行芯グループの通りを [`Axis::distance`] の昇順へ整える
146 /// ([`AxisGroupKind::Other`] のグループは並びに意味がないため何もしない)。
147 ///
148 /// 通りを追加・取り込みしたあとに呼び、一覧表示と ST-Bridge の書き出しが
149 /// そのまま座標順になるようにする。この不変条件を保つ場所を 1 つにするため、
150 /// 並べ替えは常にこのメソッドを使う。
151 pub fn sort_axes(&mut self) {
152 if self.kind == AxisGroupKind::Other {
153 return;
154 }
155 // 離れを持たない通りは末尾へ寄せる(平行芯では通常は起こらないが、
156 // `distance` を欠いた ST-Bridge ファイルでも順序を決められるようにする)。
157 self.axes.sort_by(|a, b| match (a.distance, b.distance) {
158 (Some(x), Some(y)) => x.total_cmp(&y),
159 (Some(_), None) => std::cmp::Ordering::Less,
160 (None, Some(_)) => std::cmp::Ordering::Greater,
161 (None, None) => std::cmp::Ordering::Equal,
162 });
163 }
164}
165
166impl Model {
167 /// 通り芯に属する要素(すべての材端節点がその通りに属する要素)の ID を、
168 /// 要素 ID 昇順で返す。
169 ///
170 /// 通り芯自身は所属節点しか持たないため、「その通りで構成される構面の要素」は
171 /// 常にこのヘルパーで求める。柱・梁・ブレースのような線材のほか、4 節点すべてが
172 /// その通りに属する壁・シェルも該当する。
173 /// 材端 2 節点の距離 [mm](材長)。
174 ///
175 /// 部材荷重の載荷位置は i 端からの mm で持つため、その妥当性を判定するところと
176 /// 階への複製が材長を必要とする。判定の情報源を 1 つに保つため、材長が要る
177 /// ところは常にこのメソッドを使う。2 節点に満たない要素・節点参照が範囲外の
178 /// 要素は 0 を返す。
179 pub fn member_length(&self, elem: &ElementData) -> f64 {
180 if elem.nodes.len() < 2 {
181 return 0.0;
182 }
183 let (Some(i), Some(j)) = (
184 self.nodes.get(elem.nodes[0].index()),
185 self.nodes.get(elem.nodes[1].index()),
186 ) else {
187 return 0.0;
188 };
189 let (a, b) = (i.coord, j.coord);
190 ((b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2) + (b[2] - a[2]).powi(2)).sqrt()
191 }
192
193 /// 部材の所属階(材端節点のうち**最も高い節点**の所属階)。
194 ///
195 /// 階 \\(i\\) の階高区間にある柱は上端が階 \\(i\\) に属し、階 \\(i\\) のレベルにある梁も
196 /// 階 \\(i\\) に属する、という数え方であり、階の主要構造種別の判定
197 /// (準備計算の階生成)と同じ規則である。判定の情報源を 1 つに保つため、
198 /// 部材の所属階が要るところは常にこのメソッドを使う。
199 ///
200 /// 節点が階に割り当てられていない場合は `None`。
201 pub fn member_story(&self, elem: &ElementData) -> Option<crate::ids::StoryId> {
202 elem.nodes
203 .iter()
204 .filter_map(|nid| self.nodes.get(nid.index()))
205 .max_by(|a, b| a.coord[2].total_cmp(&b.coord[2]))
206 .and_then(|n| n.story)
207 }
208
209 pub fn axis_elements(&self, axis: &Axis) -> Vec<ElemId> {
210 let on_axis: std::collections::HashSet<NodeId> = axis.nodes.iter().copied().collect();
211 let mut ids: Vec<ElemId> = self
212 .elements
213 .iter()
214 .filter(|e| !e.nodes.is_empty() && e.nodes.iter().all(|n| on_axis.contains(n)))
215 .map(|e| e.id)
216 .collect();
217 ids.sort();
218 ids
219 }
220}