1use crate::geom::{best_fit_plane_normal, is_vertical_pair};
12use crate::ids::StoryId;
13use crate::model::{ElementKind, Model};
14
15pub const FRAME_TOL_MM: f64 = crate::axis_gen::AXIS_TOL_MM;
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum FrameTarget {
22 Axis { group: usize, axis: usize },
25 Story(StoryId),
27}
28
29#[derive(Clone, Debug, PartialEq)]
31pub struct Frame {
32 pub label: String,
34 pub normal: [f64; 3],
36 pub node_on: Vec<bool>,
38 pub elem_on: Vec<bool>,
40}
41
42impl Frame {
43 pub fn elem_count(&self) -> usize {
45 self.elem_on.iter().filter(|&&b| b).count()
46 }
47}
48
49pub 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 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 for (i, e) in model.elements.iter().enumerate() {
125 if elem_on[i] || !is_column(model, e) {
126 continue;
127 }
128 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
149fn 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
163fn 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;