1use super::{scan_faces, Edge};
36use crate::geom::polygon;
37use crate::geom::{vec3, LEVEL_TOL_MM, MEMBER_AXIS_TOL_MM};
38use crate::ids::{ElemId, NodeId};
39use crate::model::{ElementKind, Model};
40
41const MIN_EDGE_LEN_MM: f64 = 1.0;
43
44#[derive(Clone, Debug, PartialEq)]
46pub struct RegionBoundary {
47 pub level: f64,
49 pub boundary: Vec<NodeId>,
51 pub edges: Vec<ElemId>,
53}
54
55impl RegionBoundary {
56 fn polygon(&self, model: &Model) -> Option<Vec<[f64; 2]>> {
58 self.boundary
59 .iter()
60 .map(|n| model.nodes.get(n.index()).map(|n| [n.coord[0], n.coord[1]]))
61 .collect()
62 }
63
64 pub fn area(&self, model: &Model) -> f64 {
66 self.polygon(model)
67 .map(|pts| polygon::area(&pts))
68 .unwrap_or(0.0)
69 }
70
71 pub fn contains(&self, model: &Model, p: [f64; 2]) -> bool {
79 let Some(poly) = self.polygon(model) else {
80 return false;
81 };
82 polygon::contains_excluding_boundary(&poly, p)
83 }
84
85 pub fn is_same_level(&self, z: f64) -> bool {
87 (self.level - z).abs() <= LEVEL_TOL_MM
88 }
89}
90
91#[derive(Clone, Debug, Default, PartialEq)]
93pub struct RegionBoundaryScan {
94 pub boundaries: Vec<RegionBoundary>,
96 pub crossings: Vec<(ElemId, ElemId)>,
102 pub unclosed: usize,
107}
108
109pub fn scan_region_boundaries(model: &Model) -> RegionBoundaryScan {
114 let mut scan = RegionBoundaryScan::default();
115 for (level, edges) in horizontal_beams_by_level(model) {
116 scan.crossings.extend(crossing_pairs(model, &edges));
117 let (boundaries, unclosed) = faces_of_level(model, level, &edges);
118 scan.boundaries.extend(boundaries);
119 scan.unclosed += unclosed;
120 }
121 scan
122}
123
124pub fn generate_region_boundaries(model: &Model) -> Vec<RegionBoundary> {
126 scan_region_boundaries(model).boundaries
127}
128
129pub fn crossing_beams(model: &Model) -> Vec<(ElemId, ElemId)> {
137 let mut out = Vec::new();
138 for (_, edges) in horizontal_beams_by_level(model) {
139 out.extend(crossing_pairs(model, &edges));
140 }
141 out
142}
143
144fn crossing_pairs(model: &Model, edges: &[Edge]) -> Vec<(ElemId, ElemId)> {
150 let coords = |n: NodeId| model.nodes.get(n.index()).map(|x| [x.coord[0], x.coord[1]]);
151
152 struct Box {
156 idx: usize,
157 min: [f64; 2],
158 max: [f64; 2],
159 }
160 let mut boxes: Vec<Box> = Vec::with_capacity(edges.len());
161 for (idx, e) in edges.iter().enumerate() {
162 let (Some(a), Some(b)) = (coords(e.a), coords(e.b)) else {
163 continue;
164 };
165 boxes.push(Box {
166 idx,
167 min: [
168 a[0].min(b[0]) - MEMBER_AXIS_TOL_MM,
169 a[1].min(b[1]) - MEMBER_AXIS_TOL_MM,
170 ],
171 max: [
172 a[0].max(b[0]) + MEMBER_AXIS_TOL_MM,
173 a[1].max(b[1]) + MEMBER_AXIS_TOL_MM,
174 ],
175 });
176 }
177 boxes.sort_by(|x, y| x.min[0].total_cmp(&y.min[0]));
178
179 let mut out = Vec::new();
180 for (i, bi) in boxes.iter().enumerate() {
181 for bj in boxes.iter().skip(i + 1) {
182 if bj.min[0] > bi.max[0] {
183 break; }
185 if bj.min[1] > bi.max[1] || bi.min[1] > bj.max[1] {
186 continue; }
188 let (e1, e2) = (&edges[bi.idx], &edges[bj.idx]);
189 if e1.a == e2.a || e1.a == e2.b || e1.b == e2.a || e1.b == e2.b {
190 continue; }
192 let (Some(p1), Some(p2), Some(q1), Some(q2)) =
193 (coords(e1.a), coords(e1.b), coords(e2.a), coords(e2.b))
194 else {
195 continue;
196 };
197 if segments_touch(p1, p2, q1, q2) {
198 out.push((e1.elem, e2.elem));
199 }
200 }
201 }
202 out.sort_by_key(|(a, b)| (a.0, b.0));
203 out
204}
205
206fn segments_touch(p1: [f64; 2], p2: [f64; 2], q1: [f64; 2], q2: [f64; 2]) -> bool {
213 let d = |a: [f64; 2], b: [f64; 2], c: [f64; 2]| {
214 (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
215 };
216 let (d1, d2, d3, d4) = (d(p1, p2, q1), d(p1, p2, q2), d(q1, q2, p1), d(q1, q2, p2));
217 if ((d1 > 0.0) != (d2 > 0.0)) && ((d3 > 0.0) != (d4 > 0.0)) {
218 return true;
219 }
220 let on = |a: [f64; 2], b: [f64; 2], c: [f64; 2]| {
222 polygon::point_segment_dist(c, a, b) <= MEMBER_AXIS_TOL_MM
223 };
224 on(p1, p2, q1) || on(p1, p2, q2) || on(q1, q2, p1) || on(q1, q2, p2)
225}
226
227fn horizontal_beams_by_level(model: &Model) -> Vec<(f64, Vec<Edge>)> {
229 let mut buckets: Vec<(f64, Vec<Edge>)> = Vec::new();
230 for e in &model.elements {
231 if e.kind != ElementKind::Beam || e.nodes.len() != 2 {
232 continue;
233 }
234 let (Some(na), Some(nb)) = (
235 model.nodes.get(e.nodes[0].index()),
236 model.nodes.get(e.nodes[1].index()),
237 ) else {
238 continue;
239 };
240 if (na.coord[2] - nb.coord[2]).abs() > LEVEL_TOL_MM {
241 continue; }
243 if vec3::dist(na.coord, nb.coord) < MIN_EDGE_LEN_MM {
244 continue;
245 }
246 let z = (na.coord[2] + nb.coord[2]) / 2.0;
247 let edge = Edge {
248 a: e.nodes[0],
249 b: e.nodes[1],
250 elem: e.id,
251 };
252 match buckets
253 .iter_mut()
254 .find(|(level, _)| (*level - z).abs() <= LEVEL_TOL_MM)
255 {
256 Some((_, list)) => list.push(edge),
257 None => buckets.push((z, vec![edge])),
258 }
259 }
260 buckets.sort_by(|a, b| a.0.total_cmp(&b.0));
261 buckets
262}
263
264fn faces_of_level(model: &Model, level: f64, edges: &[Edge]) -> (Vec<RegionBoundary>, usize) {
266 let proj = |n: NodeId| {
267 model
268 .nodes
269 .get(n.index())
270 .map(|nd| [nd.coord[0], nd.coord[1]])
271 };
272 let (faces, unclosed) = scan_faces(edges, proj);
273
274 let mut boundaries: Vec<RegionBoundary> = faces
276 .into_iter()
277 .filter(|f| f.signed_area > 0.0)
278 .map(|f| RegionBoundary {
279 level,
280 boundary: f.boundary,
281 edges: f.edges,
282 })
283 .collect();
284
285 boundaries.sort_by(|a, b| b.area(model).total_cmp(&a.area(model)));
286 (boundaries, unclosed)
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292 use crate::model::{ElementData, EndCondition, ForceRegime, LocalAxis, Node};
293
294 fn node(id: u32, x: f64, y: f64, z: f64) -> Node {
295 Node {
296 id: NodeId(id),
297 coord: [x, y, z],
298 restraint: Default::default(),
299 mass: None,
300 story: None,
301 support_spring: None,
302 }
303 }
304
305 fn beam(id: u32, i: u32, j: u32) -> ElementData {
306 ElementData {
307 id: ElemId(id),
308 kind: ElementKind::Beam,
309 nodes: [NodeId(i), NodeId(j)].into_iter().collect(),
310 section: None,
311 local_axis: LocalAxis {
312 ref_vector: [0.0, 0.0, 1.0],
313 },
314 end_cond: [EndCondition::Fixed, EndCondition::Fixed],
315 force_regime: ForceRegime::Auto,
316 rigid_zone: Default::default(),
317 plastic_zone: None,
318 spring: None,
319 }
320 }
321
322 fn grid(nx: usize, ny: usize, pitch: f64, z: f64) -> Model {
324 let mut model = Model::default();
325 let idx = |ix: usize, iy: usize| (iy * (nx + 1) + ix) as u32;
326 for iy in 0..=ny {
327 for ix in 0..=nx {
328 model
329 .nodes
330 .push(node(idx(ix, iy), ix as f64 * pitch, iy as f64 * pitch, z));
331 }
332 }
333 let mut eid = 0;
334 for iy in 0..=ny {
335 for ix in 0..nx {
336 model.elements.push(beam(eid, idx(ix, iy), idx(ix + 1, iy)));
337 eid += 1;
338 }
339 }
340 for ix in 0..=nx {
341 for iy in 0..ny {
342 model.elements.push(beam(eid, idx(ix, iy), idx(ix, iy + 1)));
343 eid += 1;
344 }
345 }
346 model
347 }
348
349 #[test]
350 fn test_single_boundary() {
351 let model = grid(1, 1, 4000.0, 0.0);
352 let boundaries = generate_region_boundaries(&model);
353 assert_eq!(boundaries.len(), 1, "1 床領域なら面は 1 つ");
354 assert_eq!(boundaries[0].boundary.len(), 4);
355 assert!((boundaries[0].area(&model) - 4000.0 * 4000.0).abs() < 1.0);
356 assert!((boundaries[0].level - 0.0).abs() < 1e-9);
357 assert_eq!(boundaries[0].edges.len(), 4, "境界の梁が辺と同数");
358 }
359
360 #[test]
361 fn test_grid_boundaries() {
362 let model = grid(3, 2, 3000.0, 4000.0);
363 let boundaries = generate_region_boundaries(&model);
364 assert_eq!(boundaries.len(), 6, "3×2 の格子は 6 面");
365 for p in &boundaries {
366 assert!((p.area(&model) - 3000.0 * 3000.0).abs() < 1.0);
367 assert!((p.level - 4000.0).abs() < 1e-9);
368 }
369 }
370
371 #[test]
373 fn test_boundary_is_counter_clockwise() {
374 let model = grid(1, 1, 4000.0, 0.0);
375 let p = &generate_region_boundaries(&model)[0];
376 let pts: Vec<[f64; 2]> = p
377 .boundary
378 .iter()
379 .map(|n| {
380 let c = model.nodes[n.index()].coord;
381 [c[0], c[1]]
382 })
383 .collect();
384 assert!(polygon::signed_area(&pts) > 0.0, "反時計回りで返る");
385 }
386
387 #[test]
389 fn test_dangling_beam_makes_no_boundary() {
390 let mut model = grid(1, 1, 4000.0, 0.0);
391 model.nodes.push(node(4, 6000.0, 0.0, 0.0));
392 let eid = model.elements.len() as u32;
393 model.elements.push(beam(eid, 1, 4)); let boundaries = generate_region_boundaries(&model);
395 assert_eq!(boundaries.len(), 1, "片持ち梁は面を作らない");
396 assert!((boundaries[0].area(&model) - 4000.0 * 4000.0).abs() < 1.0);
397 }
398
399 #[test]
401 fn test_levels_are_separated() {
402 let mut model = grid(1, 1, 4000.0, 0.0);
403 let base_nodes = model.nodes.len() as u32;
404 let base_elems = model.elements.len() as u32;
405 let upper = grid(1, 1, 4000.0, 4000.0);
406 for (k, n) in upper.nodes.iter().enumerate() {
407 model.nodes.push(node(
408 base_nodes + k as u32,
409 n.coord[0],
410 n.coord[1],
411 n.coord[2],
412 ));
413 }
414 for (k, e) in upper.elements.iter().enumerate() {
415 model.elements.push(beam(
416 base_elems + k as u32,
417 base_nodes + e.nodes[0].0,
418 base_nodes + e.nodes[1].0,
419 ));
420 }
421 let boundaries = generate_region_boundaries(&model);
422 assert_eq!(boundaries.len(), 2, "レベルごとに 1 面ずつ");
423 assert!(
424 (boundaries[0].level - 0.0).abs() < 1e-9,
425 "レベルの昇順で返る"
426 );
427 assert!((boundaries[1].level - 4000.0).abs() < 1e-9);
428 }
429
430 #[test]
432 fn test_only_horizontal_beams_are_used() {
433 let mut model = grid(1, 1, 4000.0, 0.0);
434 model.nodes.push(node(4, 0.0, 0.0, 4000.0));
436 let eid = model.elements.len() as u32;
437 model.elements.push(beam(eid, 0, 4));
438 model.nodes.push(node(5, 4000.0, 0.0, 2000.0));
440 model.elements.push(beam(eid + 1, 1, 5));
441 assert_eq!(generate_region_boundaries(&model).len(), 1);
442 }
443
444 #[test]
446 fn test_crossing_beams_are_reported() {
447 let mut model = grid(1, 1, 4000.0, 0.0);
448 model.nodes.push(node(4, 1000.0, 1000.0, 0.0));
450 model.nodes.push(node(5, 3000.0, 3000.0, 0.0));
451 model.nodes.push(node(6, 1000.0, 3000.0, 0.0));
452 model.nodes.push(node(7, 3000.0, 1000.0, 0.0));
453 let eid = model.elements.len() as u32;
454 model.elements.push(beam(eid, 4, 5));
455 model.elements.push(beam(eid + 1, 6, 7));
456
457 let scan = scan_region_boundaries(&model);
458 assert_eq!(scan.crossings.len(), 1, "交差する 1 組を報告する");
459 assert_eq!(scan.unclosed, 0, "走査自体は閉じる");
460 assert_eq!(scan.boundaries.len(), 1);
462 }
463
464 #[test]
466 fn test_touching_beam_without_shared_node_is_reported() {
467 let mut model = grid(1, 1, 4000.0, 0.0);
468 model.nodes.push(node(4, 2000.0, 0.0, 0.0));
470 model.nodes.push(node(5, 2000.0, 2000.0, 0.0));
471 let eid = model.elements.len() as u32;
472 model.elements.push(beam(eid, 4, 5));
473
474 let scan = scan_region_boundaries(&model);
475 assert_eq!(scan.crossings.len(), 1);
476 }
477
478 #[test]
480 fn test_shared_node_is_not_a_crossing() {
481 let model = grid(2, 2, 3000.0, 0.0);
482 let scan = scan_region_boundaries(&model);
483 assert!(scan.crossings.is_empty(), "{:?}", scan.crossings);
484 assert_eq!(scan.boundaries.len(), 4);
485 assert_eq!(scan.unclosed, 0);
486 }
487
488 #[test]
493 fn test_near_collinear_touch_is_reported() {
494 let mut model = grid(1, 1, 4000.0, 0.0);
495 model.nodes.push(node(4, 2000.0, 5.0, 0.0));
497 model.nodes.push(node(5, 2000.0, 2000.0, 0.0));
498 let eid = model.elements.len() as u32;
499 model.elements.push(beam(eid, 4, 5));
500 assert_eq!(
501 scan_region_boundaries(&model).crossings.len(),
502 1,
503 "5mm のずれは交差として報告する"
504 );
505
506 model.nodes[4].coord[1] = 100.0;
508 assert!(scan_region_boundaries(&model).crossings.is_empty());
509 }
510
511 #[test]
513 fn test_region_boundary_contains() {
514 let model = grid(1, 1, 4000.0, 0.0);
515 let p = &generate_region_boundaries(&model)[0];
516 assert!(p.contains(&model, [2000.0, 2000.0]), "内部");
517 assert!(!p.contains(&model, [5000.0, 2000.0]), "外部");
518 assert!(!p.contains(&model, [2000.0, 0.0]), "辺上は含めない");
519 assert!(p.is_same_level(0.0));
520 assert!(!p.is_same_level(4000.0));
521 }
522
523 #[test]
525 fn test_concave_boundary() {
526 let mut model = Model::default();
528 let pts = [
529 (0.0, 0.0),
530 (4000.0, 0.0),
531 (8000.0, 0.0),
532 (0.0, 4000.0),
533 (4000.0, 4000.0),
534 (8000.0, 4000.0),
535 (0.0, 8000.0),
536 (4000.0, 8000.0),
537 ];
538 for (i, (x, y)) in pts.iter().enumerate() {
539 model.nodes.push(node(i as u32, *x, *y, 0.0));
540 }
541 let ring = [(0, 1), (1, 2), (2, 5), (5, 4), (4, 7), (7, 6), (6, 0)];
543 for (k, (i, j)) in ring.iter().enumerate() {
544 model.elements.push(beam(k as u32, *i, *j));
545 }
546 let boundaries = generate_region_boundaries(&model);
547 assert_eq!(boundaries.len(), 1, "L 形でも面は 1 つ");
548 assert!((boundaries[0].area(&model) - 48.0e6).abs() < 1.0);
550 assert_eq!(boundaries[0].boundary.len(), 7);
551 }
552}