squid_n_core/model/region.rs
1//! 床領域([`FloorRegion`])。大梁の 1 スパン区画。
2//!
3//! 床領域は「大梁が囲む区画」であり、その中に小梁([`SecondaryMember`])と
4//! 床板([`Slab`]、[`super::slab`])をまとめる単位である。設計の経緯と決定事項は
5//! `dev_docs/handoff/床領域・壁領域の再設計_申し送り.md` を参照。
6//!
7//! # 床領域と床板の違い
8//!
9//! - **床領域**(本モジュール): 大梁が囲む 1 区画そのもの。境界は大梁の閉路(節点列)で、
10//! [`crate::region_gen`] の走査から作る。**1 つの閉領域につき 1 つ**とする(D1)。
11//! 版の仕様は持たない。小梁([`FloorRegion::secondary_joists`])と、
12//! 床領域内の床板一覧([`FloorRegion::slab_ids`])を持つ。
13//! - **床板**([`Slab`]、[`super::slab`]): 大梁または小梁で囲まれた版、
14//! または主架構に取り付く版(片持ち・バルコニー・出隅)。厚さ・材料・仕上げ荷重・
15//! 室用途はここが持つ。1 つの床領域は複数の床板を持ちうる
16//! (床領域内が小梁でさらに細かい打設単位に分かれている場合)。
17//! 片持ちスラブはどの床領域からも参照されない独立した床板として存在する。
18
19use super::*;
20
21/// 取り付く床板の取付き先。
22///
23/// 節点で指すのは、節点を動かしても追随し、取付き先の大梁を分割しても外れないためである
24/// (区間は節点対の間の相対位置なので、間に節点が増えても変わらない)。
25#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
26pub enum RegionAnchor {
27 /// 線に取り付く(片持ちスラブ・バルコニー)。
28 ///
29 /// `nodes` は取付き線の両端、`span` はその線上の無次元区間 `[t_i, t_j]`(0.0〜1.0、
30 /// 全長は `[0.0, 1.0]`)。梁の一部だけに載る場合に用いる。
31 ///
32 /// 張り出し量 `extent` は `[d_i, d_j]`(区間の始端側・終端側)で、
33 /// **符号は取付き線 `nodes[0]`→`nodes[1]` の左側を正とする**。
34 ///
35 /// 荷重の出口(`transfer`)を選べるのはこの形だけである。点に取り付く床板は
36 /// その節点への集中しかありえないため、値を持たせると無意味な組み合わせを
37 /// 表現できてしまう。
38 Line {
39 nodes: [NodeId; 2],
40 span: [f64; 2],
41 transfer: LoadTransfer,
42 },
43 /// 点(柱)に取り付く(出隅の片持ちスラブ)。荷重はその節点へ集中する。
44 ///
45 /// 張り出し量 `extent` は全体座標の `[X 方向, Y 方向]` で、符号が向きを表す。
46 Point(NodeId),
47 /// 床領域に取り付く(自立壁。床の上に立つ間仕切り等)。荷重は壁が載っている
48 /// 床領域へ渡し、等価な面荷重へならして分配する(D17)。
49 ///
50 /// `nodes` は壁の始点・終点(節点参照。`Line` と同じ理由で節点移動に追随させる)。
51 /// 面積(=自重の計算)にはこの2点間の距離を使う。
52 ///
53 /// **荷重を渡す床領域は保存せず、壁の位置から都度求める**
54 /// ([`Model::self_standing_wall_coverage`])。床領域は主架構から作り直される
55 /// 派生的な入力(D10)で、`FloorRegionId` は作り直しのたびに面走査順で振り直される。
56 /// ID を保存すると、モデルの位相が変わった瞬間に別の床領域を指すようになり、
57 /// 自立壁の自重が黙って別の階へならされる。保存しなければこの陳腐化は起こりえない。
58 ///
59 /// 壁が複数の床領域にまたがる場合は、床領域の境界で内部的に分割して、
60 /// それぞれの床領域へ ∫|立ち上がり高さ| の比で重量を配る。矩形なら長さ比と一致する。
61 /// 両端で高さが符号反転するときは高さ 0 で折る(端点絶対値の台形ではない)。
62 /// どの床領域にも載らない部分が残る壁は、荷重の行き先がないモデルの不備として
63 /// 解析前チェックが止める。
64 ///
65 /// **面荷重へならす時点で、壁が床領域内のどこにあるかという位置情報は失われる**
66 /// (厳密に扱いたい場合は壁の直下に小梁を入れて主架構へ取り付ける運用とする)。
67 /// 失われるのは位置であって長さではない。
68 ///
69 /// 床板([`super::Slab`])の取付き先としては使わない。床板が「床領域に取り付く」
70 /// 構図は D13 の 2 種別(囲まれた領域・取り付き領域)のどちらにも該当せず生じない
71 /// (床は主架構またはほかの床板の辺に取り付くのであって、床領域そのものには
72 /// 取り付かない)。壁側([`super::WallPlate`] の `Attached` 形)専用のアンカーである。
73 FloorRegion { nodes: [NodeId; 2] },
74}
75
76/// 取付き線の無次元区間 `span = [t_i, t_j]` が規約 `0.0 <= t_i < t_j <= 1.0`
77/// を満たすか。
78///
79/// 判定の情報源を 1 つに保つため、区間の妥当性が要るところは常にこの関数を使う。
80/// `Model::validate`(保存時の最終防衛線)・`squid-n-edit` の編集コマンド・
81/// GUI の入力欄がそれぞれ同じ式を書いていたが、GUI 側だけ下の許容を持たず、
82/// 「保存はできるが GUI で編集し直すと弾かれる」非対称になっていた。
83///
84/// 両端に `1e-9` の許容を持たせるのは、`span` が浮動小数の無次元比であり、
85/// `1.0` を意図した値が計算経路で `1.0 + ε` になりうるためである。区間が
86/// つぶれている(`t_j - t_i` が許容以下)ものは、載る範囲が無いので弾く。
87pub fn span_is_valid(span: [f64; 2]) -> bool {
88 span[0].is_finite()
89 && span[1].is_finite()
90 && span[0] >= -1e-9
91 && span[1] <= 1.0 + 1e-9
92 && span[1] - span[0] > 1e-9
93}
94
95/// 取り付く床板の荷重の出口([`RegionAnchor::Line`] が持つ)。
96#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
97pub enum LoadTransfer {
98 /// 取付き線へ分布させる(既定。片持ちスラブ・梁に載るパラペット)。
99 #[default]
100 Anchor,
101 /// 取付き線の両端(柱)へ集中させる(出隅・雑壁の柱伝達)。
102 Columns,
103}
104
105/// 床領域。大梁が囲む 1 スパン区画(D1)。版の仕様は持たない([`Slab`] が持つ)。
106#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
107pub struct FloorRegion {
108 /// 床領域 ID(`Model::floor_regions` の配列インデックスと一致すること)。
109 pub id: FloorRegionId,
110 /// 表示名(ナビゲータ・診断で領域を指し示すために用いる)。空文字は名前なし。
111 #[serde(default)]
112 pub name: String,
113 /// 境界の節点列(大梁の閉路。反時計回り、始点は繰り返さない)。
114 pub boundary: Vec<NodeId>,
115 /// この床領域に属する小梁(`SecondaryMember::Joist`)の実体。
116 /// リスト内の順序は任意。
117 #[serde(default)]
118 pub secondary_joists: Vec<SecondaryMember>,
119 /// この床領域に属する床板の ID リスト。床領域内が小梁で複数の打設単位に
120 /// 分かれている場合、複数持ちうる。順序は任意。重複・他領域との共有は許さない
121 /// (`Model::validate` が確認)。
122 #[serde(default)]
123 pub slab_ids: Vec<SlabId>,
124}
125
126impl Model {
127 /// 床領域の境界節点が**すべて同一の剛床**に属するか。
128 ///
129 /// 小梁の断面検定(分配 Span 検定)は、曲げ・せん断・たわみだけを見て軸力を
130 /// 見ない。これは「その小梁が載る床面が 1 枚の剛体で、面内力を剛床が処理して
131 /// いる」ことを暗黙の前提にしている。剛床でない床の小梁は面内力を負担する
132 /// ため、この前提が成り立たず検定できない(検定側は「未」にする)。
133 ///
134 /// **判定は節点の集合で行う。** 剛床のスレーブ集合に境界節点がすべて含まれる
135 /// ことを求める。将来、節点ごとに剛床の対象を変えられるようにしても、この
136 /// 判定はそのまま使える。
137 ///
138 /// 複数の剛床にまたがる床領域(段差床の階は 1 階に複数の剛床を持つ)は偽と
139 /// する。境界で相対変位が生じうるため、面内力が小梁へ回る可能性が残る。
140 pub fn floor_region_on_single_diaphragm(&self, region: &FloorRegion) -> bool {
141 if region.boundary.is_empty() {
142 return false;
143 }
144 self.constraints.iter().any(|c| match c {
145 Constraint::RigidDiaphragm { master, slaves, .. } => region
146 .boundary
147 .iter()
148 .all(|n| n == master || slaves.contains(n)),
149 _ => false,
150 })
151 }
152
153 /// 二次部材(端点対で識別)が属する床領域。どこにも属さなければ `None`。
154 pub fn floor_region_of_joist(&self, nodes: [NodeId; 2]) -> Option<&FloorRegion> {
155 let key = |a: NodeId, b: NodeId| (a.0.min(b.0), a.0.max(b.0));
156 let want = key(nodes[0], nodes[1]);
157 self.floor_regions.iter().find(|r| {
158 r.secondary_joists
159 .iter()
160 .any(|j| key(j.nodes[0], j.nodes[1]) == want)
161 })
162 }
163}
164
165impl FloorRegion {
166 /// 床領域を作る(版なし・小梁なし)。
167 pub fn new(id: FloorRegionId, boundary: Vec<NodeId>) -> Self {
168 FloorRegion {
169 id,
170 name: String::new(),
171 boundary,
172 secondary_joists: Vec::new(),
173 slab_ids: Vec::new(),
174 }
175 }
176
177 /// 境界多角形の座標列 [mm]。節点が引けない(陳腐化した参照)場合は `None`。
178 pub fn boundary_coords(&self, model: &Model) -> Option<Vec<[f64; 3]>> {
179 self.boundary
180 .iter()
181 .map(|n| model.nodes.get(n.index()).map(|n| n.coord))
182 .collect()
183 }
184
185 /// 境界の辺 `k` の両端節点。
186 pub fn edge_nodes(&self, k: usize) -> Option<[NodeId; 2]> {
187 let n = self.boundary.len();
188 (n >= 3 && k < n).then(|| [self.boundary[k], self.boundary[(k + 1) % n]])
189 }
190
191 /// 領域を代表する節点(境界の先頭)。階の帰属や診断の表示など、
192 /// 「この領域はどこにあるか」を 1 点で示す用途に使う。
193 pub fn reference_node(&self) -> Option<NodeId> {
194 self.boundary.first().copied()
195 }
196
197 /// 領域のレベル Z [mm](境界座標の Z の平均)。境界が引けなければ `None`。
198 pub fn level(&self, model: &Model) -> Option<f64> {
199 let coords = self.boundary_coords(model)?;
200 if coords.is_empty() {
201 return None;
202 }
203 Some(coords.iter().map(|c| c[2]).sum::<f64>() / coords.len() as f64)
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use crate::ids::{FloorRegionId, NodeId};
211
212 fn model_with_nodes(pts: &[[f64; 3]]) -> Model {
213 let mut m = Model::default();
214 for (i, p) in pts.iter().enumerate() {
215 m.nodes.push(Node {
216 id: NodeId(i as u32),
217 coord: *p,
218 restraint: Default::default(),
219 mass: None,
220 story: None,
221 support_spring: None,
222 });
223 }
224 m
225 }
226
227 #[test]
228 fn test_boundary_coords() {
229 let m = model_with_nodes(&[
230 [0.0, 0.0, 0.0],
231 [4000.0, 0.0, 0.0],
232 [4000.0, 4000.0, 0.0],
233 [0.0, 4000.0, 0.0],
234 ]);
235 let r = FloorRegion::new(
236 FloorRegionId(0),
237 vec![NodeId(0), NodeId(1), NodeId(2), NodeId(3)],
238 );
239 let coords = r.boundary_coords(&m).expect("境界座標");
240 assert_eq!(coords.len(), 4);
241 assert_eq!(r.level(&m), Some(0.0));
242 assert_eq!(r.reference_node(), Some(NodeId(0)));
243 assert_eq!(r.edge_nodes(0), Some([NodeId(0), NodeId(1)]));
244 }
245
246 #[test]
247 fn span_is_valid_は規約0_0以上t_i未満t_j以下1_0を判定する() {
248 // 代表的な妥当値。
249 assert!(span_is_valid([0.0, 1.0]));
250 assert!(span_is_valid([0.25, 0.75]));
251
252 // 上下端は 1e-9 の許容つき。span は無次元比のため、1.0 を意図した値が
253 // 計算経路で 1.0 + ε になりうる。
254 assert!(span_is_valid([-1e-10, 1.0 + 1e-10]));
255 assert!(!span_is_valid([-1e-3, 1.0]));
256 assert!(!span_is_valid([0.0, 1.0 + 1e-3]));
257
258 // つぶれた区間・逆転した区間は載る範囲が無い。
259 assert!(!span_is_valid([0.5, 0.5]));
260 assert!(!span_is_valid([0.75, 0.25]));
261
262 // 非有限は弾く。
263 assert!(!span_is_valid([f64::NAN, 1.0]));
264 assert!(!span_is_valid([0.0, f64::INFINITY]));
265 }
266}