1use super::{scan_faces, Edge};
62use crate::geom::{is_vertical_pair, MEMBER_AXIS_TOL_MM};
63use crate::ids::{ElemId, NodeId};
64use crate::model::{ElementKind, Model};
65use std::collections::HashMap;
66
67const MIN_PROJECTED_EDGE_LEN_MM: f64 = 1.0;
83
84const MIN_FACE_AREA_MM2: f64 = MEMBER_AXIS_TOL_MM * MEMBER_AXIS_TOL_MM;
101
102#[derive(Clone, Debug, PartialEq)]
104pub struct WallRegionBoundary {
105 pub plane_origin: [f64; 2],
107 pub plane_direction: [f64; 2],
109 pub boundary: Vec<NodeId>,
111 pub edges: Vec<ElemId>,
113}
114
115impl WallRegionBoundary {
116 fn coords(&self, model: &Model) -> Option<Vec<[f64; 3]>> {
118 self.boundary
119 .iter()
120 .map(|n| model.nodes.get(n.index()).map(|n| n.coord))
121 .collect()
122 }
123
124 pub fn area(&self, model: &Model) -> f64 {
127 self.coords(model)
128 .map(|pts| crate::geom::polygon::area_3d(&pts))
129 .unwrap_or(0.0)
130 }
131
132 pub fn project(&self, coord: [f64; 3]) -> [f64; 2] {
141 project(self.plane_origin, self.plane_direction, coord)
142 }
143
144 fn local_polygon(&self, model: &Model) -> Option<Vec<[f64; 2]>> {
146 self.boundary
147 .iter()
148 .map(|n| model.nodes.get(n.index()).map(|nd| self.project(nd.coord)))
149 .collect()
150 }
151
152 pub fn contains(&self, model: &Model, p: [f64; 2]) -> bool {
157 let Some(poly) = self.local_polygon(model) else {
158 return false;
159 };
160 crate::geom::polygon::contains_excluding_boundary(&poly, p)
161 }
162
163 pub fn is_same_plane(&self, origin: [f64; 2]) -> bool {
170 is_same_line(self.plane_origin, self.plane_direction, &[origin])
171 }
172}
173
174#[derive(Clone, Debug, Default, PartialEq)]
179pub struct WallRegionBoundaryScan {
180 pub boundaries: Vec<WallRegionBoundary>,
182 pub unclosed: usize,
184}
185
186pub fn scan_wall_region_boundaries(model: &Model) -> WallRegionBoundaryScan {
190 let mut scan = WallRegionBoundaryScan::default();
191 for (origin, direction) in wall_planes(model) {
192 let edges = members_on_plane(model, origin, direction);
193 let proj = |n: NodeId| -> Option<[f64; 2]> {
194 model
195 .nodes
196 .get(n.index())
197 .map(|nd| project(origin, direction, nd.coord))
198 };
199 let (faces, unclosed) = scan_faces(&edges, proj);
200 scan.unclosed += unclosed;
201
202 let mut boundaries: Vec<WallRegionBoundary> = faces
203 .into_iter()
204 .filter(|f| f.signed_area > MIN_FACE_AREA_MM2)
205 .map(|f| WallRegionBoundary {
206 plane_origin: origin,
207 plane_direction: direction,
208 boundary: f.boundary,
209 edges: f.edges,
210 })
211 .collect();
212 boundaries.sort_by(|a, b| b.area(model).total_cmp(&a.area(model)));
213 scan.boundaries.extend(boundaries);
214 }
215 scan
216}
217
218pub fn generate_wall_region_boundaries(model: &Model) -> Vec<WallRegionBoundary> {
220 scan_wall_region_boundaries(model).boundaries
221}
222
223fn wall_planes(model: &Model) -> Vec<([f64; 2], [f64; 2])> {
235 let footprints = column_footprints(model);
236 if footprints.len() < 2 {
237 return Vec::new();
238 }
239 let mut lines: Vec<([f64; 2], [f64; 2])> = Vec::new();
240 let mut index = LineIndex::new(&footprints);
241 for i in 0..footprints.len() {
242 for j in (i + 1)..footprints.len() {
243 let (p, q) = (footprints[i], footprints[j]);
244 let Some(direction) = canonical_direction(p, q) else {
245 continue; };
247 let found = index
248 .nearby(p, direction)
249 .into_iter()
250 .find(|&idx| is_same_line(lines[idx].0, lines[idx].1, &[p, q]));
251 if found.is_some() {
252 continue;
253 }
254 let idx = lines.len();
255 lines.push((p, direction));
256 index.insert(idx, p, direction);
257 }
258 }
259 lines
260}
261
262struct LineIndex {
273 reference: [f64; 2],
275 ang_res: f64,
277 buckets: HashMap<(i64, i64, i64), Vec<usize>>,
278}
279
280impl LineIndex {
281 fn new(footprints: &[[f64; 2]]) -> Self {
282 let reference = centroid(footprints);
283 let r_max = footprints
284 .iter()
285 .map(|&p| dist2(p, reference).sqrt())
286 .fold(0.0_f64, f64::max)
287 .max(MEMBER_AXIS_TOL_MM);
288 let ang_res = MEMBER_AXIS_TOL_MM / r_max;
291 LineIndex {
292 reference,
293 ang_res,
294 buckets: HashMap::new(),
295 }
296 }
297
298 fn key(&self, line_point: [f64; 2], direction: [f64; 2]) -> (i64, i64, i64) {
299 let theta = direction[1].atan2(direction[0]);
300 let two_theta = 2.0 * theta;
301 let cell = (2.0 * self.ang_res).max(1e-12);
302 let bc = (two_theta.cos() / cell).floor() as i64;
303 let bs = (two_theta.sin() / cell).floor() as i64;
304 let offset = point_to_line_dist(line_point, direction, self.reference);
305 let bo = (offset / MEMBER_AXIS_TOL_MM).floor() as i64;
306 (bc, bs, bo)
307 }
308
309 fn nearby(&self, line_point: [f64; 2], direction: [f64; 2]) -> Vec<usize> {
312 let (bc, bs, bo) = self.key(line_point, direction);
313 let mut out = Vec::new();
314 for dc in -1..=1 {
315 for ds in -1..=1 {
316 for doff in -1..=1 {
317 if let Some(v) = self.buckets.get(&(bc + dc, bs + ds, bo + doff)) {
318 out.extend_from_slice(v);
319 }
320 }
321 }
322 }
323 out
324 }
325
326 fn insert(&mut self, idx: usize, line_point: [f64; 2], direction: [f64; 2]) {
327 let key = self.key(line_point, direction);
328 self.buckets.entry(key).or_default().push(idx);
329 }
330}
331
332fn centroid(pts: &[[f64; 2]]) -> [f64; 2] {
334 let n = pts.len().max(1) as f64;
335 let sum = pts
336 .iter()
337 .fold([0.0, 0.0], |a, p| [a[0] + p[0], a[1] + p[1]]);
338 [sum[0] / n, sum[1] / n]
339}
340
341fn column_footprints(model: &Model) -> Vec<[f64; 2]> {
349 let mut pts: Vec<[f64; 2]> = Vec::new();
350 for e in &model.elements {
351 if e.kind != ElementKind::Beam || e.nodes.len() != 2 {
352 continue;
353 }
354 let (Some(a), Some(b)) = (
355 model.nodes.get(e.nodes[0].index()),
356 model.nodes.get(e.nodes[1].index()),
357 ) else {
358 continue;
359 };
360 if !is_vertical_pair(a.coord, b.coord) {
361 continue;
362 }
363 let p = [a.coord[0], a.coord[1]];
364 let dup = pts
365 .iter()
366 .any(|&q| dist2(p, q) <= MEMBER_AXIS_TOL_MM * MEMBER_AXIS_TOL_MM);
367 if !dup {
368 pts.push(p);
369 }
370 }
371 pts
372}
373
374fn canonical_direction(p: [f64; 2], q: [f64; 2]) -> Option<[f64; 2]> {
380 let d = [q[0] - p[0], q[1] - p[1]];
381 let len = (d[0] * d[0] + d[1] * d[1]).sqrt();
382 if len <= f64::EPSILON {
383 return None;
384 }
385 let mut u = [d[0] / len, d[1] / len];
386 if u[0] < 0.0 || (u[0] == 0.0 && u[1] < 0.0) {
387 u = [-u[0], -u[1]];
388 }
389 Some(u)
390}
391
392fn is_same_line(origin: [f64; 2], direction: [f64; 2], defining_points: &[[f64; 2]]) -> bool {
398 defining_points
399 .iter()
400 .all(|&p| point_to_line_dist(origin, direction, p) <= MEMBER_AXIS_TOL_MM)
401}
402
403fn point_to_line_dist(origin: [f64; 2], direction: [f64; 2], p: [f64; 2]) -> f64 {
405 let v = [p[0] - origin[0], p[1] - origin[1]];
406 (v[0] * direction[1] - v[1] * direction[0]).abs()
407}
408
409fn dist2(a: [f64; 2], b: [f64; 2]) -> f64 {
410 (a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2)
411}
412
413fn members_on_plane(model: &Model, origin: [f64; 2], direction: [f64; 2]) -> Vec<Edge> {
416 let mut edges = Vec::new();
417 for e in &model.elements {
418 if e.kind != ElementKind::Beam || e.nodes.len() != 2 {
419 continue;
420 }
421 let (Some(a), Some(b)) = (
422 model.nodes.get(e.nodes[0].index()),
423 model.nodes.get(e.nodes[1].index()),
424 ) else {
425 continue;
426 };
427 let (pa, pb) = ([a.coord[0], a.coord[1]], [b.coord[0], b.coord[1]]);
428 if point_to_line_dist(origin, direction, pa) > MEMBER_AXIS_TOL_MM
429 || point_to_line_dist(origin, direction, pb) > MEMBER_AXIS_TOL_MM
430 {
431 continue;
432 }
433 let (sa, sb) = (
434 project(origin, direction, a.coord),
435 project(origin, direction, b.coord),
436 );
437 let proj_len = ((sa[0] - sb[0]).powi(2) + (sa[1] - sb[1]).powi(2)).sqrt();
438 if proj_len < MIN_PROJECTED_EDGE_LEN_MM {
439 continue; }
441 edges.push(Edge {
442 a: e.nodes[0],
443 b: e.nodes[1],
444 elem: e.id,
445 });
446 }
447 edges
448}
449
450fn project(origin: [f64; 2], direction: [f64; 2], coord: [f64; 3]) -> [f64; 2] {
456 let v = [coord[0] - origin[0], coord[1] - origin[1]];
457 let s = v[0] * direction[0] + v[1] * direction[1];
458 [s, coord[2]]
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464 use crate::model::{ElementData, EndCondition, ForceRegime, LocalAxis, Node};
465
466 fn node(id: u32, x: f64, y: f64, z: f64) -> Node {
467 Node {
468 id: NodeId(id),
469 coord: [x, y, z],
470 restraint: Default::default(),
471 mass: None,
472 story: None,
473 support_spring: None,
474 }
475 }
476
477 fn beam(id: u32, i: u32, j: u32) -> ElementData {
478 ElementData {
479 id: ElemId(id),
480 kind: ElementKind::Beam,
481 nodes: [NodeId(i), NodeId(j)].into_iter().collect(),
482 section: None,
483 local_axis: LocalAxis {
484 ref_vector: [0.0, 0.0, 1.0],
485 },
486 end_cond: [EndCondition::Fixed, EndCondition::Fixed],
487 force_regime: ForceRegime::Auto,
488 rigid_zone: Default::default(),
489 plastic_zone: None,
490 spring: None,
491 }
492 }
493
494 struct PlaneFrameSpec {
496 origin: [f64; 2],
498 heading_deg: f64,
500 bay: f64,
502 story_h: f64,
504 n_bay: usize,
506 n_story: usize,
508 }
509
510 fn add_plane_frame(model: &mut Model, next_id: &mut u32, spec: PlaneFrameSpec) {
513 let PlaneFrameSpec {
514 origin,
515 heading_deg,
516 bay,
517 story_h,
518 n_bay,
519 n_story,
520 } = spec;
521 let theta = heading_deg.to_radians();
522 let (dx, dy) = (theta.cos(), theta.sin());
523 let base_node = model.nodes.len() as u32;
524 let idx = |ix: usize, iz: usize| base_node + (iz * (n_bay + 1) + ix) as u32;
525 for iz in 0..=n_story {
526 for ix in 0..=n_bay {
527 let s = ix as f64 * bay;
528 model.nodes.push(node(
529 idx(ix, iz),
530 origin[0] + s * dx,
531 origin[1] + s * dy,
532 iz as f64 * story_h,
533 ));
534 }
535 }
536 for ix in 0..=n_bay {
538 for iz in 0..n_story {
539 model
540 .elements
541 .push(beam(*next_id, idx(ix, iz), idx(ix, iz + 1)));
542 *next_id += 1;
543 }
544 }
545 for iz in 0..=n_story {
547 for ix in 0..n_bay {
548 model
549 .elements
550 .push(beam(*next_id, idx(ix, iz), idx(ix + 1, iz)));
551 *next_id += 1;
552 }
553 }
554 }
555
556 #[test]
558 fn test_x_direction_plane() {
559 let mut model = Model::default();
560 let mut next_id = 0u32;
561 add_plane_frame(
562 &mut model,
563 &mut next_id,
564 PlaneFrameSpec {
565 origin: [0.0, 0.0],
566 heading_deg: 0.0,
567 bay: 4000.0,
568 story_h: 3000.0,
569 n_bay: 2,
570 n_story: 2,
571 },
572 );
573 let scan = scan_wall_region_boundaries(&model);
574 assert_eq!(scan.unclosed, 0, "半辺の後続は一意に定まるはず");
575 assert_eq!(scan.boundaries.len(), 4, "2×2 の壁構面は 4 面");
576 for b in &scan.boundaries {
577 assert!(
578 (b.area(&model) - 4000.0 * 3000.0).abs() < 1.0,
579 "面積 {}",
580 b.area(&model)
581 );
582 }
583 }
584
585 #[test]
587 fn test_y_direction_plane() {
588 let mut model = Model::default();
589 let mut next_id = 0u32;
590 add_plane_frame(
591 &mut model,
592 &mut next_id,
593 PlaneFrameSpec {
594 origin: [0.0, 0.0],
595 heading_deg: 90.0,
596 bay: 4000.0,
597 story_h: 3000.0,
598 n_bay: 2,
599 n_story: 2,
600 },
601 );
602 let scan = scan_wall_region_boundaries(&model);
603 assert_eq!(scan.unclosed, 0, "半辺の後続は一意に定まるはず");
604 assert_eq!(scan.boundaries.len(), 4, "2×2 の壁構面は 4 面");
605 for b in &scan.boundaries {
606 assert!(
607 (b.area(&model) - 4000.0 * 3000.0).abs() < 1.0,
608 "面積 {}",
609 b.area(&model)
610 );
611 }
612 }
613
614 #[test]
616 fn test_oblique_plane() {
617 let mut model = Model::default();
618 let mut next_id = 0u32;
619 add_plane_frame(
620 &mut model,
621 &mut next_id,
622 PlaneFrameSpec {
623 origin: [0.0, 0.0],
624 heading_deg: 30.0,
625 bay: 4000.0,
626 story_h: 3000.0,
627 n_bay: 2,
628 n_story: 2,
629 },
630 );
631 let scan = scan_wall_region_boundaries(&model);
632 assert_eq!(scan.unclosed, 0, "半辺の後続は一意に定まるはず");
633 assert_eq!(scan.boundaries.len(), 4, "2×2 の壁構面は 4 面(斜め)");
634 for b in &scan.boundaries {
635 assert!(
636 (b.area(&model) - 4000.0 * 3000.0).abs() < 1.0,
637 "面積 {}",
638 b.area(&model)
639 );
640 }
641 }
642
643 #[test]
645 fn test_single_boundary_shape() {
646 let mut model = Model::default();
647 let mut next_id = 0u32;
648 add_plane_frame(
649 &mut model,
650 &mut next_id,
651 PlaneFrameSpec {
652 origin: [0.0, 0.0],
653 heading_deg: 0.0,
654 bay: 4000.0,
655 story_h: 3000.0,
656 n_bay: 1,
657 n_story: 1,
658 },
659 );
660 let boundaries = generate_wall_region_boundaries(&model);
661 assert_eq!(boundaries.len(), 1);
662 assert_eq!(boundaries[0].boundary.len(), 4);
663 assert_eq!(boundaries[0].edges.len(), 4);
664 }
665
666 #[test]
669 fn test_is_same_line_uses_absolute_distance_not_angle() {
670 let origin = [0.0, 0.0];
671 let direction = [1.0, 0.0];
672 let near = [50_000.0, 9.0];
673 let far = [50_000.0, 11.0];
674 assert!(is_same_line(origin, direction, &[near]), "9mm は同一直線");
675 assert!(!is_same_line(origin, direction, &[far]), "11mm は別の直線");
676 }
677
678 #[test]
681 fn test_canonical_direction_is_order_independent_near_seam() {
682 let p = [0.0, 0.0];
683 let q = [-1000.0, 0.2]; let d1 = canonical_direction(p, q).expect("方向が求まる");
685 let d2 = canonical_direction(q, p).expect("方向が求まる");
686 assert!((d1[0] - d2[0]).abs() < 1e-9 && (d1[1] - d2[1]).abs() < 1e-9);
687 }
688
689 #[test]
692 fn test_perpendicular_beam_does_not_change_face_count() {
693 let mut model = Model::default();
694 let mut next_id = 0u32;
695 add_plane_frame(
696 &mut model,
697 &mut next_id,
698 PlaneFrameSpec {
699 origin: [0.0, 0.0],
700 heading_deg: 0.0,
701 bay: 4000.0,
702 story_h: 3000.0,
703 n_bay: 1,
704 n_story: 1,
705 },
706 );
707 let extra_node = model.nodes.len() as u32;
709 model.nodes.push(node(extra_node, 4000.0, 2000.0, 3000.0));
710 model.elements.push(beam(next_id, 3, extra_node));
711 let boundaries = generate_wall_region_boundaries(&model);
712 assert_eq!(boundaries.len(), 1, "構面に垂直な部材は面の数を変えない");
713 }
714
715 #[test]
724 fn test_area_uses_3d_newell_not_projected_area() {
725 let mut model = Model::default();
726 let mut next_id = 0u32;
727 add_plane_frame(
728 &mut model,
729 &mut next_id,
730 PlaneFrameSpec {
731 origin: [0.0, 0.0],
732 heading_deg: 0.0,
733 bay: 4000.0,
734 story_h: 3000.0,
735 n_bay: 2,
736 n_story: 1,
737 },
738 );
739 let boundaries = generate_wall_region_boundaries(&model);
740 assert_eq!(boundaries.len(), 2, "2 スパンの壁構面は 2 面");
741 for b in &boundaries {
742 assert!(
743 (b.area(&model) - 4000.0 * 3000.0).abs() < 1.0,
744 "平面のときは投影面積と一致: {}",
745 b.area(&model)
746 );
747 }
748
749 let mid_top = 4; model.nodes[mid_top].coord[1] += 5.0;
754 let boundaries = generate_wall_region_boundaries(&model);
755 assert_eq!(boundaries.len(), 2, "5mm のずれは面走査の結果を左右しない");
756 let newell_areas: Vec<f64> = boundaries.iter().map(|b| b.area(&model)).collect();
757 assert!(
758 newell_areas
759 .iter()
760 .any(|a| (a - 4000.0 * 3000.0).abs() > 1.0),
761 "少なくとも 1 面はニューエル面積が投影面積からずれる: {newell_areas:?}"
762 );
763 }
764
765 #[test]
785 fn test_isolated_dangling_column_does_not_create_spurious_boundary() {
786 let mut model = Model::default();
787 model.nodes.push(node(0, 1000.0, 1000.0, 0.0));
790 model.nodes.push(node(1, 1000.0, 1000.0, 3000.0));
791 model.elements.push(beam(0, 0, 1));
792
793 for (k, z) in [200.0, 4700.0, 8700.0, 12700.0].into_iter().enumerate() {
795 model.nodes.push(node(2 + k as u32, 13800.0, 7400.0, z));
796 }
797 for k in 0..3u32 {
798 model.elements.push(beam(1 + k, 2 + k, 3 + k));
799 }
800
801 let boundaries = generate_wall_region_boundaries(&model);
802 assert!(
803 boundaries.is_empty(),
804 "互いに孤立した柱どうしは境界を作らない: {boundaries:?}"
805 );
806 }
807}