1use crate::dof::Dof6Mask;
11use crate::geom::polygon::{self, BOUNDARY_TOL_MM};
12use crate::geom::{LEVEL_TOL_MM, MEMBER_AXIS_TOL_MM};
13use crate::ids::{FloorRegionId, NodeId};
14use crate::model::{ElementKind, FloorRegion, LoadTransfer, Model, RegionAnchor, SlabShape};
15use crate::region_gen::{generate_region_boundaries, scan_region_boundaries};
16
17pub const CENTROID_MATCH_AREA_REL: f64 = 1e-3;
19
20#[derive(Clone, Debug, Default, PartialEq, Eq)]
22pub struct FloorRegionRebuildReport {
23 pub regions: usize,
25 pub inherited: usize,
27 pub new_regions: usize,
29 pub unmatched_old_regions: usize,
31 pub slabs_assigned: usize,
33 pub slabs_converted_to_attached: usize,
35 pub unassigned_slabs: usize,
37 pub unassigned_joists: usize,
39 pub deleted_nodes: usize,
41}
42
43pub fn rebuild_floor_regions(model: &mut Model) -> FloorRegionRebuildReport {
50 let scan = scan_region_boundaries(model);
51 for r in &mut model.floor_regions {
53 model.unassigned_joists.append(&mut r.secondary_joists);
54 }
55 let old_regions = std::mem::take(&mut model.floor_regions);
56 let mut report = FloorRegionRebuildReport::default();
57
58 let mut matched_old = vec![false; old_regions.len()];
61 let mut new_regions: Vec<FloorRegion> = Vec::with_capacity(scan.boundaries.len());
62 for rb in &scan.boundaries {
63 let mut region = FloorRegion::new(FloorRegionId(0), rb.boundary.clone());
64 let rb_area = rb.area(model);
65 if let Some((oi, old_area)) = old_regions
66 .iter()
67 .enumerate()
68 .filter(|(oi, _)| !matched_old[*oi])
69 .filter_map(|(oi, old)| {
70 let (cxy, z, area) = boundary_centroid_area(model, &old.boundary)?;
71 (rb.is_same_level(z) && rb.contains(model, cxy)).then_some((oi, area))
72 })
73 .min_by(|(_, a), (_, b)| a.total_cmp(b))
74 {
75 matched_old[oi] = true;
76 let denom = rb_area.abs().max(f64::EPSILON);
77 if (old_area - rb_area).abs() / denom < CENTROID_MATCH_AREA_REL {
78 region.name = old_regions[oi].name.clone();
79 report.inherited += 1;
80 } else {
81 report.new_regions += 1;
82 }
83 } else {
84 report.new_regions += 1;
85 }
86 new_regions.push(region);
87 }
88 report.regions = new_regions.len();
89 report.unmatched_old_regions = matched_old.iter().filter(|m| !**m).count();
90
91 let beams = horizontal_girders(model);
94 let mut owner: Vec<Option<usize>> = vec![None; model.slabs.len()];
95 for (si, slab) in model.slabs.iter().enumerate() {
96 let SlabShape::Enclosed { boundary } = &slab.shape else {
97 continue; };
99 let Some((cxy, z, _)) = boundary_centroid_area(model, boundary) else {
100 continue;
101 };
102 if let Some((ri, _)) = new_regions
103 .iter()
104 .enumerate()
105 .filter(|(_, r)| region_is_same_level(model, r, z) && region_contains(model, r, cxy))
106 .min_by(|(_, a), (_, b)| region_area(model, a).total_cmp(®ion_area(model, b)))
107 {
108 owner[si] = Some(ri);
109 report.slabs_assigned += 1;
110 }
111 }
112 let mut converted = Vec::new();
113 let mut discarded_by_conversion: Vec<NodeId> = Vec::new();
117 for (si, slab) in model.slabs.iter().enumerate() {
118 if owner[si].is_some() || slab.is_attached() {
119 continue;
120 }
121 let SlabShape::Enclosed { boundary } = &slab.shape else {
122 continue;
123 };
124 if let Some(attached) = try_convert_cantilever(model, boundary, &beams) {
125 discarded_by_conversion.extend(boundary.iter().copied());
126 converted.push((si, attached));
127 report.slabs_converted_to_attached += 1;
128 } else {
129 report.unassigned_slabs += 1;
130 }
131 }
132 for (si, shape) in converted {
133 model.slabs[si].shape = shape;
134 }
135 for (si, ri) in owner.into_iter().enumerate() {
136 if let Some(ri) = ri {
137 new_regions[ri].slab_ids.push(model.slabs[si].id);
138 }
139 }
140
141 for (i, r) in new_regions.iter_mut().enumerate() {
142 r.id = FloorRegionId(i as u32);
143 }
144 model.floor_regions = new_regions;
145
146 report.unassigned_joists = assign_joists(model);
148
149 report.deleted_nodes = delete_unref_nodes(model, &discarded_by_conversion);
153
154 report
155}
156
157pub fn unassigned_joist_count(model: &Model) -> usize {
159 model.unassigned_joists.len()
160}
161
162pub fn floating_slab_count(model: &Model) -> usize {
164 let regions = generate_region_boundaries(model);
165 model
166 .slabs
167 .iter()
168 .filter(|s| matches!(s.shape, SlabShape::Enclosed { .. }))
169 .filter(|s| {
170 let Some((cxy, z, _)) = s
171 .boundary_nodes()
172 .and_then(|b| boundary_centroid_area(model, b))
173 else {
174 return true;
175 };
176 !regions
177 .iter()
178 .any(|r| r.is_same_level(z) && r.contains(model, cxy))
179 })
180 .count()
181}
182
183fn boundary_centroid_area(model: &Model, boundary: &[NodeId]) -> Option<([f64; 2], f64, f64)> {
185 let mut pts = Vec::with_capacity(boundary.len());
186 let mut z_sum = 0.0;
187 for id in boundary {
188 let n = model.nodes.get(id.index())?;
189 pts.push([n.coord[0], n.coord[1]]);
190 z_sum += n.coord[2];
191 }
192 if pts.len() < 3 {
193 return None;
194 }
195 let area = polygon::signed_area(&pts);
196 let cxy = polygon::centroid(&pts);
197 let z = z_sum / pts.len() as f64;
198 Some((cxy, z, area.abs()))
199}
200
201fn region_is_same_level(model: &Model, region: &FloorRegion, z: f64) -> bool {
202 region
203 .level(model)
204 .is_some_and(|rz| (rz - z).abs() <= LEVEL_TOL_MM)
205}
206
207fn region_contains(model: &Model, region: &FloorRegion, p: [f64; 2]) -> bool {
208 let Some(coords) = region.boundary_coords(model) else {
209 return false;
210 };
211 let poly: Vec<[f64; 2]> = coords.iter().map(|c| [c[0], c[1]]).collect();
212 polygon::contains_excluding_boundary(&poly, p)
213}
214
215fn region_area(model: &Model, region: &FloorRegion) -> f64 {
216 boundary_centroid_area(model, ®ion.boundary)
217 .map(|(_, _, a)| a)
218 .unwrap_or(f64::MAX)
219}
220
221pub(crate) struct GirderSeg {
225 a: [f64; 2],
226 b: [f64; 2],
227 z: f64,
228}
229
230pub(crate) fn horizontal_girders(model: &Model) -> Vec<GirderSeg> {
231 let mut out = Vec::new();
232 for e in &model.elements {
233 if e.kind != ElementKind::Beam || e.nodes.len() != 2 {
234 continue;
235 }
236 let (Some(na), Some(nb)) = (
237 model.nodes.get(e.nodes[0].index()),
238 model.nodes.get(e.nodes[1].index()),
239 ) else {
240 continue;
241 };
242 if (na.coord[2] - nb.coord[2]).abs() > LEVEL_TOL_MM {
243 continue;
244 }
245 out.push(GirderSeg {
246 a: [na.coord[0], na.coord[1]],
247 b: [nb.coord[0], nb.coord[1]],
248 z: (na.coord[2] + nb.coord[2]) / 2.0,
249 });
250 }
251 out
252}
253
254fn try_convert_cantilever(
257 model: &Model,
258 boundary: &[NodeId],
259 beams: &[GirderSeg],
260) -> Option<SlabShape> {
261 if boundary.len() < 3 {
262 return None;
263 }
264 let mut xy = Vec::with_capacity(boundary.len());
265 let mut z_sum = 0.0;
266 for id in boundary {
267 let n = model.nodes.get(id.index())?;
268 xy.push([n.coord[0], n.coord[1]]);
269 z_sum += n.coord[2];
270 }
271 let z = z_sum / xy.len() as f64;
272 let n = boundary.len();
273 let mut covered = Vec::new();
274 for i in 0..n {
275 let a = xy[i];
276 let b = xy[(i + 1) % n];
277 if edge_fully_covered(a, b, z, beams) {
278 covered.push(i);
279 }
280 }
281 if covered.len() != 1 {
282 return None;
283 }
284 let ei = covered[0];
285 let n0 = boundary[ei];
286 let n1 = boundary[(ei + 1) % n];
287 let a = xy[ei];
288 let b = xy[(ei + 1) % n];
289 let dx = b[0] - a[0];
290 let dy = b[1] - a[1];
291 let len = (dx * dx + dy * dy).sqrt();
292 if len <= f64::EPSILON {
293 return None;
294 }
295 let ux = dx / len;
296 let uy = dy / len;
297 let nx = -uy;
299 let ny = ux;
300
301 let mut free = Vec::new();
302 for p in &xy {
303 if polygon::point_segment_dist(*p, a, b) <= BOUNDARY_TOL_MM {
304 continue;
305 }
306 let d = (p[0] - a[0]) * nx + (p[1] - a[1]) * ny;
307 let t = (p[0] - a[0]) * ux + (p[1] - a[1]) * uy;
308 free.push((t, d));
309 }
310 let extent = match free.len() {
311 1 => [free[0].1, free[0].1],
312 2 => {
313 if free[0].0 <= free[1].0 {
314 [free[0].1, free[1].1]
315 } else {
316 [free[1].1, free[0].1]
317 }
318 }
319 _ => return None,
320 };
321
322 Some(SlabShape::Attached {
323 anchor: RegionAnchor::Line {
324 nodes: [n0, n1],
325 span: [0.0, 1.0],
326 transfer: LoadTransfer::Anchor,
327 },
328 extent,
329 })
330}
331
332pub(crate) fn edge_fully_covered(a: [f64; 2], b: [f64; 2], z: f64, beams: &[GirderSeg]) -> bool {
333 let dx = b[0] - a[0];
334 let dy = b[1] - a[1];
335 let len = (dx * dx + dy * dy).sqrt();
336 if len <= f64::EPSILON {
337 return false;
338 }
339 let ux = dx / len;
340 let uy = dy / len;
341 let nx = -uy;
342 let ny = ux;
343 let mut intervals: Vec<(f64, f64)> = Vec::new();
344 for beam in beams {
345 if (beam.z - z).abs() > LEVEL_TOL_MM {
346 continue;
347 }
348 let da = (beam.a[0] - a[0]) * nx + (beam.a[1] - a[1]) * ny;
349 let db = (beam.b[0] - a[0]) * nx + (beam.b[1] - a[1]) * ny;
350 if da.abs() > MEMBER_AXIS_TOL_MM || db.abs() > MEMBER_AXIS_TOL_MM {
351 continue;
352 }
353 let ta = (beam.a[0] - a[0]) * ux + (beam.a[1] - a[1]) * uy;
354 let tb = (beam.b[0] - a[0]) * ux + (beam.b[1] - a[1]) * uy;
355 let t0 = ta.min(tb).clamp(0.0, len);
356 let t1 = ta.max(tb).clamp(0.0, len);
357 if t1 > t0 {
358 intervals.push((t0, t1));
359 }
360 }
361 if intervals.is_empty() {
362 return false;
363 }
364 intervals.sort_by(|x, y| x.0.total_cmp(&y.0));
365 let mut merged = vec![intervals[0]];
366 for &(s, e) in intervals.iter().skip(1) {
367 let last = merged.last_mut().expect("merged is non-empty");
368 if s <= last.1 + MEMBER_AXIS_TOL_MM {
369 last.1 = last.1.max(e);
370 } else {
371 merged.push((s, e));
372 }
373 }
374 let covered: f64 = merged.iter().map(|(s, e)| e - s).sum();
375 covered >= len - MEMBER_AXIS_TOL_MM
376}
377
378fn joist_midpoint(model: &Model, nodes: [NodeId; 2]) -> Option<([f64; 2], f64)> {
379 let a = model.nodes.get(nodes[0].index())?;
380 let b = model.nodes.get(nodes[1].index())?;
381 Some((
382 [
383 (a.coord[0] + b.coord[0]) * 0.5,
384 (a.coord[1] + b.coord[1]) * 0.5,
385 ],
386 (a.coord[2] + b.coord[2]) * 0.5,
387 ))
388}
389
390fn regions_containing(model: &Model, regions: &[FloorRegion], xy: [f64; 2], z: f64) -> Vec<usize> {
391 let mut hits = Vec::new();
392 for (i, r) in regions.iter().enumerate() {
393 if !region_is_same_level(model, r, z) {
394 continue;
395 }
396 if region_contains(model, r, xy) {
397 hits.push(i);
398 }
399 }
400 hits
401}
402
403fn assign_joists(model: &mut Model) -> usize {
404 let joists: Vec<_> = model
405 .floor_regions
406 .iter_mut()
407 .flat_map(|r| r.secondary_joists.drain(..))
408 .chain(model.unassigned_joists.drain(..))
409 .collect();
410 for r in &mut model.floor_regions {
411 r.secondary_joists.clear();
412 }
413 let mut unassigned = 0;
414 for sm in joists {
415 let nodes = sm.nodes;
416 let Some((xy, z)) = joist_midpoint(model, nodes) else {
417 model.unassigned_joists.push(sm);
418 unassigned += 1;
419 continue;
420 };
421 let hits = regions_containing(model, &model.floor_regions, xy, z);
422 if hits.len() == 1 {
423 model.floor_regions[hits[0]].secondary_joists.push(sm);
424 } else {
425 model.unassigned_joists.push(sm);
426 unassigned += 1;
427 }
428 }
429 unassigned
430}
431
432fn node_has_structural_ref(model: &Model, id: NodeId) -> bool {
447 if model.elements.iter().any(|e| e.nodes.contains(&id)) {
448 return true;
449 }
450 if model.node_referenced_by_regions_or_plates(id) {
451 return true;
452 }
453 if let Some(node) = model.nodes.get(id.index()) {
454 if node.restraint != Dof6Mask::FREE || node.mass.is_some() || node.support_spring.is_some()
455 {
456 return true;
457 }
458 }
459 if model.generated_masters.contains(&id) {
460 return true;
461 }
462 false
463}
464
465pub(crate) fn delete_unref_nodes(model: &mut Model, candidates: &[NodeId]) -> usize {
471 let n = model.nodes.len();
472 if n == 0 {
473 return 0;
474 }
475 let candidate_set: std::collections::HashSet<NodeId> = candidates.iter().copied().collect();
476 let keep: Vec<bool> = (0..n)
477 .map(|i| {
478 let id = NodeId(i as u32);
479 !candidate_set.contains(&id) || node_has_structural_ref(model, id)
480 })
481 .collect();
482 let deleted = keep.iter().filter(|k| !*k).count();
483 if deleted == 0 {
484 return 0;
485 }
486
487 for story in &mut model.stories {
488 story
489 .node_ids
490 .retain(|id| keep.get(id.index()).copied().unwrap_or(false));
491 }
492 for group in &mut model.axes {
493 for axis in &mut group.axes {
494 axis.nodes
495 .retain(|id| keep.get(id.index()).copied().unwrap_or(false));
496 }
497 }
498
499 let mut map: Vec<Option<u32>> = vec![None; n];
500 let mut new_i = 0u32;
501 for i in 0..n {
502 if keep[i] {
503 map[i] = Some(new_i);
504 new_i += 1;
505 }
506 }
507 model.nodes = model
508 .nodes
509 .drain(..)
510 .enumerate()
511 .filter(|(i, _)| keep[*i])
512 .map(|(_, node)| node)
513 .collect();
514 model.visit_node_ids(|id| {
515 if let Some(new) = map.get(id.0 as usize).and_then(|m| *m) {
516 id.0 = new;
517 }
518 });
519 deleted
520}
521
522#[cfg(test)]
523mod tests {
524 use super::*;
525 use crate::ids::{ElemId, FloorRegionId, NodeId, SectionId, SlabId};
526 use crate::model::{
527 AreaLoad, DistributionMethod, ElementData, ElementKind, EndCondition, ForceRegime,
528 LocalAxis, Node, RegionAnchor, SecondaryMember, SecondaryMemberKind, Slab, SlabPlate,
529 };
530 use crate::region_gen::generate_region_boundaries;
531 use crate::section_shape::SectionShape;
532
533 fn node(id: u32, x: f64, y: f64, z: f64) -> Node {
534 Node {
535 id: NodeId(id),
536 coord: [x, y, z],
537 restraint: Default::default(),
538 mass: None,
539 story: None,
540 support_spring: None,
541 }
542 }
543
544 fn beam(id: u32, i: u32, j: u32) -> ElementData {
545 ElementData {
546 id: ElemId(id),
547 kind: ElementKind::Beam,
548 nodes: [NodeId(i), NodeId(j)].into_iter().collect(),
549 section: None,
550 local_axis: LocalAxis {
551 ref_vector: [0.0, 0.0, 1.0],
552 },
553 end_cond: [EndCondition::Fixed, EndCondition::Fixed],
554 force_regime: ForceRegime::Auto,
555 rigid_zone: Default::default(),
556 plastic_zone: None,
557 spring: None,
558 }
559 }
560
561 fn plate(section: Option<SectionId>, loads: Vec<AreaLoad>) -> SlabPlate {
562 SlabPlate {
563 section,
564 loads,
565 usage: None,
566 method: DistributionMethod::TriTrapezoid,
567 one_way: None,
568 }
569 }
570
571 fn enclosed_slab(id: u32, boundary: Vec<u32>, plate: SlabPlate) -> Slab {
572 Slab {
573 id: SlabId(id),
574 shape: SlabShape::Enclosed {
575 boundary: boundary.into_iter().map(NodeId).collect(),
576 },
577 plate,
578 }
579 }
580
581 fn push_slab_section(model: &mut Model, thickness: f64) -> SectionId {
582 let id = SectionId(model.sections.len() as u32);
583 model
584 .sections
585 .push(SectionShape::RcSlab { thickness }.to_section(id, format!("S{thickness:.0}")));
586 id
587 }
588
589 fn joist(id: u32, i: u32, j: u32) -> SecondaryMember {
590 SecondaryMember {
591 kind: SecondaryMemberKind::Joist,
592 nodes: [NodeId(i), NodeId(j)],
593 section: None,
594 name: format!("J{id}"),
595 }
596 }
597
598 fn has_xy(model: &Model, x: f64, y: f64) -> bool {
599 model.nodes.iter().any(|n| {
600 (n.coord[0] - x).abs() < 1e-6
601 && (n.coord[1] - y).abs() < 1e-6
602 && n.coord[2].abs() < 1e-6
603 })
604 }
605
606 fn two_piece_square() -> Model {
608 let mut model = Model::default();
609 for (i, (x, y)) in [
610 (0.0, 0.0),
611 (2000.0, 0.0),
612 (4000.0, 0.0),
613 (4000.0, 4000.0),
614 (2000.0, 4000.0),
615 (0.0, 4000.0),
616 ]
617 .into_iter()
618 .enumerate()
619 {
620 model.nodes.push(node(i as u32, x, y, 0.0));
621 }
622 model.elements.extend([
623 beam(0, 0, 1),
624 beam(1, 1, 2),
625 beam(2, 2, 3),
626 beam(3, 3, 4),
627 beam(4, 4, 5),
628 beam(5, 5, 0),
629 ]);
630 let sid = push_slab_section(&mut model, 150.0);
631 model.slabs.push(enclosed_slab(
632 0,
633 vec![0, 1, 4, 5],
634 plate(Some(sid), Vec::new()),
635 ));
636 model.slabs.push(enclosed_slab(
637 1,
638 vec![1, 2, 3, 4],
639 plate(Some(sid), Vec::new()),
640 ));
641 model.unassigned_joists.push(joist(0, 1, 4));
642 model
643 }
644
645 fn cantilever_rect() -> Model {
646 let mut model = Model::default();
647 model.nodes.push(node(0, 0.0, 0.0, 0.0));
648 model.nodes.push(node(1, 4000.0, 0.0, 0.0));
649 model.nodes.push(node(2, 4000.0, 1500.0, 0.0));
650 model.nodes.push(node(3, 0.0, 1500.0, 0.0));
651 model.elements.push(beam(0, 0, 1));
652 let sid = push_slab_section(&mut model, 150.0);
653 model.slabs.push(enclosed_slab(
654 0,
655 vec![0, 1, 2, 3],
656 plate(Some(sid), Vec::new()),
657 ));
658 model
659 }
660
661 #[test]
662 fn test_two_piece_square_keeps_both_slabs_in_one_region() {
663 let mut model = two_piece_square();
664 assert_eq!(generate_region_boundaries(&model).len(), 1);
665 let report = rebuild_floor_regions(&mut model);
666 assert_eq!(model.floor_regions.len(), 1, "床領域は 1 つ");
667 assert_eq!(model.slabs.len(), 2, "床板は畳まず 2 枚のまま");
668 assert_eq!(
669 model.floor_regions[0].slab_ids.len(),
670 2,
671 "2 枚とも同じ床領域へ帰属"
672 );
673 assert_eq!(
674 model.floor_regions[0].secondary_joists.len(),
675 1,
676 "中央小梁が属する"
677 );
678 assert_eq!(
679 model.floor_regions[0].secondary_joists[0].nodes,
680 [NodeId(1), NodeId(4)]
681 );
682 assert_eq!(report.regions, 1);
683 assert_eq!(report.slabs_assigned, 2);
684 }
685
686 #[test]
687 fn test_rebuild_is_idempotent_on_two_piece_square() {
688 let mut model = two_piece_square();
689 rebuild_floor_regions(&mut model);
690 let first_regions = model.floor_regions.clone();
691 let first_slabs = model.slabs.clone();
692 let first_nodes = model.nodes.len();
693 rebuild_floor_regions(&mut model);
694 assert_eq!(model.floor_regions, first_regions);
695 assert_eq!(model.slabs, first_slabs);
696 assert_eq!(model.nodes.len(), first_nodes);
697 }
698
699 #[test]
700 fn test_rebuild_preserves_secondary_joists_across_runs() {
701 let mut model = two_piece_square();
702 rebuild_floor_regions(&mut model);
703 assert_eq!(model.joists().count(), 1);
704 let first = model
705 .floor_regions
706 .iter()
707 .flat_map(|r| r.secondary_joists.clone())
708 .collect::<Vec<_>>();
709 rebuild_floor_regions(&mut model);
710 assert_eq!(model.joists().count(), 1);
711 let second = model
712 .floor_regions
713 .iter()
714 .flat_map(|r| r.secondary_joists.clone())
715 .collect::<Vec<_>>();
716 assert_eq!(first, second);
717 }
718
719 #[test]
720 fn test_courtyard_two_internal_regions_no_outer_region() {
721 let mut model = Model::default();
722 let outer = [(0.0, 0.0), (8000.0, 0.0), (8000.0, 8000.0), (0.0, 8000.0)];
723 let inner = [
724 (2000.0, 2000.0),
725 (6000.0, 2000.0),
726 (6000.0, 6000.0),
727 (2000.0, 6000.0),
728 ];
729 for (i, (x, y)) in outer.into_iter().enumerate() {
730 model.nodes.push(node(i as u32, x, y, 0.0));
731 }
732 for (i, (x, y)) in inner.into_iter().enumerate() {
733 model.nodes.push(node(4 + i as u32, x, y, 0.0));
734 }
735 model.elements.extend([
736 beam(0, 0, 1),
737 beam(1, 1, 2),
738 beam(2, 2, 3),
739 beam(3, 3, 0),
740 beam(4, 4, 5),
741 beam(5, 5, 6),
742 beam(6, 6, 7),
743 beam(7, 7, 4),
744 ]);
745 let regions = generate_region_boundaries(&model);
746 assert_eq!(regions.len(), 2, "内部面は外周と中庭の 2 つ");
747 let report = rebuild_floor_regions(&mut model);
748 assert_eq!(model.floor_regions.len(), regions.len());
749 assert_eq!(report.regions, regions.len());
750 }
751
752 #[test]
753 fn test_cantilever_rebuild_node_ids_are_compact() {
754 let mut model = cantilever_rect();
755 rebuild_floor_regions(&mut model);
756 assert!(model.validate().is_ok(), "{:?}", model.validate().err());
757 for (i, n) in model.nodes.iter().enumerate() {
758 assert_eq!(n.id, NodeId(i as u32), "nodes[{i}].id");
759 }
760 }
761
762 #[test]
763 fn test_cantilever_rect_converts_to_attached_line() {
764 let mut model = cantilever_rect();
765 let report = rebuild_floor_regions(&mut model);
766 assert_eq!(model.floor_regions.len(), 0, "囲む大梁がないため床領域は 0");
767 assert_eq!(model.slabs.len(), 1);
768 let s = &model.slabs[0];
769 assert!(s.is_attached());
770 match &s.shape {
771 SlabShape::Attached {
772 anchor:
773 RegionAnchor::Line {
774 nodes,
775 span,
776 transfer,
777 },
778 extent,
779 } => {
780 assert_eq!(*span, [0.0, 1.0]);
781 assert_eq!(*transfer, LoadTransfer::Anchor);
782 let a = model.nodes[nodes[0].index()].coord;
783 let b = model.nodes[nodes[1].index()].coord;
784 let dx = b[0] - a[0];
785 let dy = b[1] - a[1];
786 assert!(dx.abs() > dy.abs(), "取付き線は +X");
787 assert!((extent[0] - 1500.0).abs() < 1e-6, "左正 {extent:?}");
788 assert!((extent[1] - 1500.0).abs() < 1e-6, "{extent:?}");
789 }
790 other => panic!("Line の Attached ではない: {other:?}"),
791 }
792 assert!(has_xy(&model, 0.0, 0.0));
793 assert!(has_xy(&model, 4000.0, 0.0));
794 assert!(!has_xy(&model, 4000.0, 1500.0), "先端節点は削除");
795 assert!(!has_xy(&model, 0.0, 1500.0), "先端節点は削除");
796 assert!(report.slabs_converted_to_attached >= 1);
797 assert!(report.deleted_nodes >= 2);
798 }
799
800 #[test]
801 fn test_inherits_name_and_joists_when_boundary_nodes_change() {
802 let mut model = Model::default();
803 for (i, (x, y)) in [(0.0, 0.0), (4000.0, 0.0), (4000.0, 4000.0), (0.0, 4000.0)]
804 .into_iter()
805 .enumerate()
806 {
807 model.nodes.push(node(i as u32, x, y, 0.0));
808 }
809 model
810 .elements
811 .extend([beam(0, 0, 1), beam(1, 1, 2), beam(2, 2, 3), beam(3, 3, 0)]);
812 let sid = push_slab_section(&mut model, 150.0);
813 model.slabs.push(enclosed_slab(
814 0,
815 vec![0, 1, 2, 3],
816 plate(
817 Some(sid),
818 vec![AreaLoad {
819 kind: "仕上げ".into(),
820 value: 0.001,
821 }],
822 ),
823 ));
824 let mut r = FloorRegion::new(
825 FloorRegionId(0),
826 vec![NodeId(0), NodeId(1), NodeId(2), NodeId(3)],
827 );
828 r.name = "階段室".into();
829 model.floor_regions.push(r);
830
831 model.nodes.push(node(4, 2000.0, 0.0, 0.0));
832 model.elements[0] = beam(0, 0, 4);
833 model.elements.push(beam(4, 4, 1));
834
835 rebuild_floor_regions(&mut model);
836 assert_eq!(model.floor_regions.len(), 1);
837 assert_eq!(model.floor_regions[0].name, "階段室");
838 assert_eq!(model.slabs.len(), 1, "床板は畳まずそのまま残る");
839 assert_eq!(
840 model.slabs[0].plate.loads,
841 vec![AreaLoad {
842 kind: "仕上げ".into(),
843 value: 0.001,
844 }]
845 );
846 }
847
848 #[test]
849 fn test_joist_outside_building_is_unassigned() {
850 let mut model = two_piece_square();
851 model.nodes.push(node(6, 10000.0, 0.0, 0.0));
852 model.nodes.push(node(7, 10000.0, 4000.0, 0.0));
853 model.unassigned_joists.push(joist(1, 6, 7));
854 let report = rebuild_floor_regions(&mut model);
855 assert_eq!(
856 model.floor_regions[0].secondary_joists.len(),
857 1,
858 "領域内小梁"
859 );
860 assert_eq!(
861 model.unassigned_joists.len(),
862 1,
863 "所属なし小梁は unassigned へ"
864 );
865 assert_eq!(report.unassigned_joists, 1);
866 }
867
868 #[test]
869 fn test_floating_slab_stays_enclosed_and_unassigned() {
870 let mut model = Model::default();
871 for (i, (x, y)) in [(0.0, 0.0), (4000.0, 0.0), (4000.0, 4000.0), (0.0, 4000.0)]
872 .into_iter()
873 .enumerate()
874 {
875 model.nodes.push(node(i as u32, x, y, 0.0));
876 }
877 let sid = push_slab_section(&mut model, 150.0);
878 model.slabs.push(enclosed_slab(
879 0,
880 vec![0, 1, 2, 3],
881 plate(Some(sid), Vec::new()),
882 ));
883 let report = rebuild_floor_regions(&mut model);
884 assert_eq!(model.floor_regions.len(), 0, "囲む大梁がないため床領域は 0");
885 assert_eq!(model.slabs.len(), 1, "床板は削除しない");
886 assert!(
887 model.slabs[0].shape
888 == SlabShape::Enclosed {
889 boundary: vec![NodeId(0), NodeId(1), NodeId(2), NodeId(3)],
890 }
891 );
892 assert_eq!(report.unassigned_slabs, 1);
893 }
894
895 #[test]
896 fn test_slab_on_two_girder_edges_stays_enclosed_and_unassigned() {
897 let mut model = Model::default();
898 model.nodes.push(node(0, 0.0, 0.0, 0.0));
899 model.nodes.push(node(1, 4000.0, 0.0, 0.0));
900 model.nodes.push(node(2, 4000.0, 4000.0, 0.0));
901 model.nodes.push(node(3, 0.0, 4000.0, 0.0));
902 model.elements.push(beam(0, 0, 1));
903 model.elements.push(beam(1, 0, 3));
904 let sid = push_slab_section(&mut model, 150.0);
905 model.slabs.push(enclosed_slab(
906 0,
907 vec![0, 1, 2, 3],
908 plate(Some(sid), Vec::new()),
909 ));
910 let report = rebuild_floor_regions(&mut model);
911 assert_eq!(model.slabs.len(), 1);
912 assert!(
913 matches!(model.slabs[0].shape, SlabShape::Enclosed { .. }),
914 "出隅相当は Enclosed のまま"
915 );
916 assert_eq!(report.slabs_converted_to_attached, 0);
917 assert_eq!(report.unassigned_slabs, 1);
918 }
919
920 #[test]
921 fn test_cantilever_tip_that_is_also_girder_end_is_kept() {
922 let mut free = cantilever_rect();
923 rebuild_floor_regions(&mut free);
924 assert!(
925 !has_xy(&free, 4000.0, 1500.0) && !has_xy(&free, 0.0, 1500.0),
926 "参照 0 の先端は消える"
927 );
928
929 let mut shared = cantilever_rect();
930 shared.nodes.push(node(4, 4000.0, 3000.0, 0.0));
931 shared.elements.push(beam(1, 2, 4));
932 let report = rebuild_floor_regions(&mut shared);
933 assert!(
934 has_xy(&shared, 4000.0, 1500.0),
935 "別の大梁端でもある先端は残る"
936 );
937 assert!(!has_xy(&shared, 0.0, 1500.0), "参照 0 の先端だけ消える");
938 assert!(report.slabs_converted_to_attached >= 1);
939 }
940
941 #[test]
948 fn test_story_node_list_membership_does_not_block_deletion() {
949 use crate::model::Story;
950
951 let mut model = cantilever_rect();
952 model.stories.push(Story {
953 id: crate::ids::StoryId(0),
954 name: "1F".into(),
955 elevation: 0.0,
956 node_ids: model.nodes.iter().map(|n| n.id).collect(),
957 seismic_weight: None,
958 weight_override: None,
959 structure: Default::default(),
960 level_kind: Default::default(),
961 });
962
963 let report = rebuild_floor_regions(&mut model);
964 assert!(!has_xy(&model, 4000.0, 1500.0), "先端は消える");
965 assert!(!has_xy(&model, 0.0, 1500.0), "先端は消える");
966 assert!(report.deleted_nodes >= 2);
967 assert!(model.stories[0]
969 .node_ids
970 .iter()
971 .all(|id| model.nodes.iter().any(|n| n.id == *id)));
972 }
973}