1use crate::geom::is_vertical_pair;
30use crate::ids::NodeId;
31use crate::model::{Axis, AxisGroup, AxisGroupKind, AxisPlanDir, AxisSource, ElementKind, Model};
32
33pub const AXIS_TOL_MM: f64 = 1.0;
35
36const X_GROUP: (&str, f64) = ("X", 270.0);
38const Y_GROUP: (&str, f64) = ("Y", 0.0);
40
41pub fn generate_axes(model: &Model) -> Vec<AxisGroup> {
46 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 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
99fn 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
110fn add_generated_axes(group: &mut AxisGroup, model: &Model, column_nodes: &[NodeId]) {
112 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 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 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
153fn 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;