Skip to main content

squid_n_core/
frame.rs

1//! 構面(通り・階)の切り出し。
2//!
3//! 通り芯 1 本、または階 1 つに属する節点・要素を選び出し、それを正対で見るための
4//! 面の法線を返す。2D の軸組図・伏図を描くための下ごしらえであり、構造計算には
5//! 用いない(描画対象の絞り込みのみ)。
6//!
7//! - [`FrameTarget`] — 見る対象(通り/階)。
8//! - [`Frame`] — 切り出した構面(所属節点・所属要素・法線・表示名)。
9//! - [`build_frame`] — モデルから構面を切り出す。
10
11use crate::geom::{best_fit_plane_normal, is_vertical_pair};
12use crate::ids::StoryId;
13use crate::model::{ElementKind, Model};
14
15/// 通り上とみなす、通りからの離れの許容差 [mm]。
16/// 通り芯の生成([`crate::axis_gen::AXIS_TOL_MM`])と同じ値を使う。
17pub const FRAME_TOL_MM: f64 = crate::axis_gen::AXIS_TOL_MM;
18
19/// 2D で見る構面の対象。
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum FrameTarget {
22    /// 通り芯 1 本(軸組図)。`group` は [`Model::axes`] の添字、`axis` はその
23    /// グループ内の添字。
24    Axis { group: usize, axis: usize },
25    /// 階 1 つ(伏図)。
26    Story(StoryId),
27}
28
29/// 切り出した構面。
30#[derive(Clone, Debug, PartialEq)]
31pub struct Frame {
32    /// 表示名(`X1 通り`・`2FL` など)。
33    pub label: String,
34    /// 構面の単位法線。正対で見る向きを決める。
35    pub normal: [f64; 3],
36    /// 節点が構面に属するか([`Model::nodes`] と同順・同長)。
37    pub node_on: Vec<bool>,
38    /// 要素が構面に属するか([`Model::elements`] と同順・同長)。
39    pub elem_on: Vec<bool>,
40}
41
42impl Frame {
43    /// 構面に属する要素の数。
44    pub fn elem_count(&self) -> usize {
45        self.elem_on.iter().filter(|&&b| b).count()
46    }
47}
48
49/// モデルから構面を切り出す。対象が存在しない場合は `None`。
50///
51/// **通り**(軸組図)に属する節点は、「通り芯の所属節点リストに載っている **または**
52/// そのグループでの離れが [`FRAME_TOL_MM`] 以内」のいずれかを満たすものとする。
53/// リストだけでは、柱間の大梁が中間節点で分割されているときに中間節点が柱節点で
54/// ないため大梁が丸ごと落ちる。座標だけでは、通りの位置と柱の芯がずれた
55/// **芯ずれ**(ST-Bridge の実ファイルで見られる)の柱が落ちる。両方を拾うため和を採る。
56///
57/// **階**(伏図)に属する節点は、[`Model::node_stories`](帰属区間による幾何判定)で
58/// その階に属する節点とする。`Node::story` のキャッシュではなく幾何を見るのは、
59/// キャッシュを埋めるのが準備計算だけであり、架構ウィザードで作った直後のモデルや、
60/// 節点を足した/動かした直後のモデルでは空または古いままだからである。伏図は
61/// 描画対象の絞り込みのみで構造計算には用いないため、幾何から引いてよい。
62///
63/// 要素は「全材端節点がその階に属するもの」に加え、**上端節点がその階に属する柱**を
64/// 含める(柱は上下 2 つの階レベルにまたがるため、前者だけでは伏図から柱が消え、
65/// 梁の支持位置が読めなくなる)。柱の所属階の規則は階の主要構造種別の判定と同じ
66/// 「材端節点のうち最も高い節点の所属階」([`Model::member_story`])。
67pub fn build_frame(model: &Model, target: FrameTarget) -> Option<Frame> {
68    match target {
69        FrameTarget::Axis { group, axis } => build_axis_frame(model, group, axis),
70        FrameTarget::Story(id) => build_story_frame(model, id),
71    }
72}
73
74fn build_axis_frame(model: &Model, gi: usize, ai: usize) -> Option<Frame> {
75    let group = model.axes.get(gi)?;
76    let ax = group.axes.get(ai)?;
77
78    let listed: std::collections::HashSet<_> = ax.nodes.iter().copied().collect();
79    let node_on: Vec<bool> = model
80        .nodes
81        .iter()
82        .map(|n| {
83            if listed.contains(&n.id) {
84                return true;
85            }
86            match (ax.distance, group.kind.distance_of(n.coord[0], n.coord[1])) {
87                (Some(want), Some(got)) => (got - want).abs() <= FRAME_TOL_MM,
88                _ => false,
89            }
90        })
91        .collect();
92
93    // 法線: 平行芯は幾何から厳密に決まる(離れを測る向き=面の法線で、厳密に鉛直な面
94    // になる)。平行芯以外(円弧芯・放射芯・作図芯)は幾何を持たないため、所属節点群へ
95    // 平面を当てはめて求める。
96    let normal = match group.kind.offset_dir() {
97        Some(d) => [d[0], d[1], 0.0],
98        None => {
99            let pts: Vec<[f64; 3]> = ax
100                .nodes
101                .iter()
102                .filter_map(|id| model.nodes.get(id.index()).map(|n| n.coord))
103                .collect();
104            best_fit_plane_normal(&pts).unwrap_or([1.0, 0.0, 0.0])
105        }
106    };
107
108    let elem_on = elements_fully_on(model, &node_on);
109    Some(Frame {
110        label: format!("{} 通り", ax.name),
111        normal,
112        node_on,
113        elem_on,
114    })
115}
116
117fn build_story_frame(model: &Model, id: StoryId) -> Option<Frame> {
118    let story = model.stories.iter().find(|s| s.id == id)?;
119    let node_stories = model.node_stories();
120    let node_on: Vec<bool> = node_stories.iter().map(|s| *s == Some(id)).collect();
121
122    let mut elem_on = elements_fully_on(model, &node_on);
123    // 上端節点がその階に属する柱を加える(伏図の柱位置)。
124    for (i, e) in model.elements.iter().enumerate() {
125        if elem_on[i] || !is_column(model, e) {
126            continue;
127        }
128        // 柱の所属階=材端節点のうち最も高い節点の所属階(`Model::member_story`
129        // と同じ規則)。ここでも幾何から引いた `node_stories` を情報源とする。
130        let top = e
131            .nodes
132            .iter()
133            .filter_map(|nid| model.nodes.get(nid.index()))
134            .max_by(|a, b| a.coord[2].total_cmp(&b.coord[2]))
135            .and_then(|n| node_stories.get(n.id.index()).copied().flatten());
136        if top == Some(id) {
137            elem_on[i] = true;
138        }
139    }
140
141    Some(Frame {
142        label: story.name.clone(),
143        normal: [0.0, 0.0, 1.0],
144        node_on,
145        elem_on,
146    })
147}
148
149/// 全材端節点が構面に属する要素を選ぶ。
150fn elements_fully_on(model: &Model, node_on: &[bool]) -> Vec<bool> {
151    model
152        .elements
153        .iter()
154        .map(|e| {
155            !e.nodes.is_empty()
156                && e.nodes
157                    .iter()
158                    .all(|n| node_on.get(n.index()).copied().unwrap_or(false))
159        })
160        .collect()
161}
162
163/// 柱(鉛直な線材)か。
164fn is_column(model: &Model, e: &crate::model::ElementData) -> bool {
165    if !matches!(e.kind, ElementKind::Beam) || e.nodes.len() != 2 {
166        return false;
167    }
168    let (Some(a), Some(b)) = (
169        model.nodes.get(e.nodes[0].index()),
170        model.nodes.get(e.nodes[1].index()),
171    ) else {
172        return false;
173    };
174    is_vertical_pair(a.coord, b.coord)
175}
176
177#[cfg(test)]
178mod tests;