1use crate::geom::{self, MemberAxisClass};
65use crate::ids::{ElemId, NodeId};
66use crate::model::{ElementData, ElementKind, Model, Section};
67use crate::section_shape::SectionShape;
68use crate::structure_kind::member_structure_kind;
69
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75pub enum MemberOrientation {
76 Column,
78 Beam,
80}
81
82pub fn member_unit_axis(model: &Model, elem: &ElementData) -> Option<[f64; 3]> {
84 if !matches!(elem.kind, ElementKind::Beam) || elem.nodes.len() < 2 {
85 return None;
86 }
87 let p0 = model.nodes.get(elem.nodes[0].index())?.coord;
88 let p1 = model.nodes.get(elem.nodes[1].index())?.coord;
89 geom::vec3::unit_from(p0, p1)
90}
91
92pub fn member_orientation(model: &Model, elem: &ElementData) -> Option<MemberOrientation> {
94 let ez = member_unit_axis(model, elem)?[2].abs();
95 match geom::classify_member_ez(ez) {
96 MemberAxisClass::Column => Some(MemberOrientation::Column),
97 MemberAxisClass::Beam => Some(MemberOrientation::Beam),
98 MemberAxisClass::Diagonal => None,
99 }
100}
101
102#[derive(Clone, Copy, Debug, Default, PartialEq)]
121pub struct PanelHalfExtent {
122 pub column_half: f64,
124 pub beam_half: f64,
126}
127
128impl PanelHalfExtent {
129 pub fn offset_for(&self, orientation: MemberOrientation) -> f64 {
131 match orientation {
132 MemberOrientation::Beam => self.column_half,
133 MemberOrientation::Column => self.beam_half,
134 }
135 }
136}
137
138pub fn panel_half_extent<'a>(
143 model: &Model,
144 node: NodeId,
145 members: impl IntoIterator<Item = &'a ElementData>,
146) -> PanelHalfExtent {
147 let mut extent = PanelHalfExtent::default();
148 for e in members {
149 if !e.nodes.iter().take(2).any(|n| *n == node) {
150 continue;
151 }
152 let Some(orientation) = member_orientation(model, e) else {
153 continue;
154 };
155 let Some(sec) = e.section.and_then(|sid| model.sections.get(sid.index())) else {
156 continue;
157 };
158 let half = sec.depth / 2.0;
159 match orientation {
160 MemberOrientation::Column => extent.column_half = extent.column_half.max(half),
161 MemberOrientation::Beam => extent.beam_half = extent.beam_half.max(half),
162 }
163 }
164 extent
165}
166
167#[derive(Clone, Copy, Debug, PartialEq)]
169pub enum PanelShapeKind {
170 H { bc: f64, tf: f64 },
172 Box { bc: f64 },
174 Pipe,
176}
177
178#[derive(Clone, Copy, Debug, PartialEq)]
180pub struct PanelGeometry {
181 pub kind: PanelShapeKind,
182 pub dc: f64,
184 pub tp: f64,
186 pub filled: bool,
191}
192
193impl PanelGeometry {
194 pub fn from_column(sec: &Section) -> Option<Self> {
201 let (kind, dc, tp, filled) = match sec.shape {
202 Some(SectionShape::SteelH {
203 height,
204 width,
205 web_thick,
206 flange_thick,
207 }) => (
208 PanelShapeKind::H {
209 bc: width,
210 tf: flange_thick,
211 },
212 height - flange_thick,
213 web_thick,
214 false,
215 ),
216 Some(SectionShape::SteelBox {
217 height,
218 width,
219 thick,
220 ..
221 }) => (
222 PanelShapeKind::Box { bc: width },
223 height - thick,
224 thick,
225 false,
226 ),
227 Some(SectionShape::CftBox {
228 height,
229 width,
230 thick,
231 }) => (
232 PanelShapeKind::Box { bc: width },
233 height - thick,
234 thick,
235 true,
236 ),
237 Some(SectionShape::SteelPipe { outer_dia, thick }) => {
238 (PanelShapeKind::Pipe, outer_dia - thick, thick, false)
239 }
240 Some(SectionShape::CftPipe { outer_dia, thick }) => {
241 (PanelShapeKind::Pipe, outer_dia - thick, thick, true)
242 }
243 _ => return None,
244 };
245 let tp = match sec.panel_thickness {
246 Some(t) if t > 0.0 => t,
247 _ => tp,
248 };
249 Some(Self {
250 kind,
251 dc,
252 tp,
253 filled,
254 })
255 }
256
257 pub fn kappa(&self) -> f64 {
263 match self.kind {
264 PanelShapeKind::H { bc, tf } => {
265 1.0 / (2.0 / 3.0 + (4.0 * bc * tf) / (self.dc * self.tp))
266 + 1.0 / (1.0 + (self.dc * self.tp) / (6.0 * bc * tf))
267 }
268 PanelShapeKind::Box { bc } => {
269 1.0 / (2.0 / 3.0 + 2.0 * bc / self.dc) + 1.0 / (1.0 + self.dc / (3.0 * bc))
270 }
271 PanelShapeKind::Pipe => 4.0 / std::f64::consts::PI,
272 }
273 }
274
275 pub fn effective_volume(&self, db: f64) -> f64 {
285 let base = self.dc * db * self.tp;
286 match self.kind {
287 PanelShapeKind::H { .. } => base,
288 PanelShapeKind::Box { .. } | PanelShapeKind::Pipe => 2.0 * base,
289 }
290 }
291}
292
293#[derive(Clone, Copy, Debug, PartialEq)]
295pub struct PanelJoint {
296 pub geometry: PanelGeometry,
298 pub db: f64,
300 pub ve: f64,
302 pub column: ElemId,
304 pub has_filled_column: bool,
310}
311
312pub fn resolve_panel_joint<'a>(
321 model: &Model,
322 node: NodeId,
323 members: impl IntoIterator<Item = &'a ElementData>,
324) -> Option<PanelJoint> {
325 let mut columns: Vec<&ElementData> = Vec::new();
326 let mut beams: Vec<&ElementData> = Vec::new();
327 for e in members {
328 if !e.nodes.iter().take(2).any(|n| *n == node) {
329 continue;
330 }
331 match member_orientation(model, e) {
333 Some(MemberOrientation::Column) => columns.push(e),
334 Some(MemberOrientation::Beam) => beams.push(e),
335 None => {}
336 }
337 }
338 if columns.is_empty() || beams.is_empty() {
339 return None;
340 }
341 if columns
345 .iter()
346 .chain(beams.iter())
347 .any(|e| !member_structure_kind(model, e).is_steel_like())
348 {
349 return None;
350 }
351
352 let section_of = |e: &ElementData| e.section.and_then(|sid| model.sections.get(sid.index()));
353 let db = beams
354 .iter()
355 .filter_map(|e| section_of(e))
356 .map(beam_panel_depth)
357 .fold(0.0_f64, f64::max);
358 if db <= 0.0 {
359 return None;
360 }
361
362 let geometry_of = |e: &&ElementData| section_of(e).and_then(PanelGeometry::from_column);
363 let has_filled_column = columns.iter().filter_map(geometry_of).any(|g| g.filled);
364
365 let (column, geometry, ve) = columns
368 .iter()
369 .filter_map(|e| {
370 let geometry = geometry_of(e)?;
371 let ve = geometry.effective_volume(db);
372 (ve > 0.0).then_some((e.id, geometry, ve))
373 })
374 .min_by(|a, b| a.2.total_cmp(&b.2))?;
375
376 Some(PanelJoint {
377 geometry,
378 db,
379 ve,
380 column,
381 has_filled_column,
382 })
383}
384
385pub fn beam_panel_depth(sec: &Section) -> f64 {
390 match sec.shape {
391 Some(SectionShape::SteelH { flange_thick, .. }) => sec.depth - flange_thick,
392 _ => 0.9 * sec.depth,
393 }
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399 use crate::ids::SectionId;
400 use crate::model::MaterialCategory;
401 use crate::section_shape::{BarSet, RcRebar, ShearBar};
402
403 fn sec(shape: SectionShape, depth: f64, panel_thickness: Option<f64>) -> Section {
404 Section {
405 id: SectionId(0),
406 name: String::new(),
407 floor: None,
408 area: 1.0e4,
409 iy: 1.0e8,
410 iz: 1.0e8,
411 j: 1.0e8,
412 depth,
413 width: depth,
414 as_y: 0.0,
415 as_z: 0.0,
416 panel_thickness,
417 thickness: None,
418 shape: Some(shape),
419 material: None,
420 rebar_material: None,
421 shear_rebar_material: None,
422 steel_material: None,
423 }
424 }
425
426 fn sec_with_mat(
428 shape: SectionShape,
429 depth: f64,
430 panel_thickness: Option<f64>,
431 id: u32,
432 mat: u32,
433 ) -> Section {
434 Section {
435 id: SectionId(id),
436 material: Some(crate::ids::MaterialId(mat)),
437 ..sec(shape, depth, panel_thickness)
438 }
439 }
440
441 #[test]
443 fn test_h_column_geometry() {
444 let s = sec(
445 SectionShape::SteelH {
446 height: 400.0,
447 width: 400.0,
448 web_thick: 13.0,
449 flange_thick: 21.0,
450 },
451 400.0,
452 None,
453 );
454 let g = PanelGeometry::from_column(&s).expect("H 形は対象");
455 assert!((g.dc - (400.0 - 21.0)).abs() < 1e-9);
456 assert!((g.tp - 13.0).abs() < 1e-9);
457 let db = 600.0;
458 assert!((g.effective_volume(db) - g.dc * db * g.tp).abs() < 1e-6);
459 }
460
461 #[test]
463 fn test_box_and_pipe_double_volume() {
464 let b = sec(
465 SectionShape::SteelBox {
466 height: 400.0,
467 width: 400.0,
468 thick: 16.0,
469 corner_r: 0.0,
470 },
471 400.0,
472 None,
473 );
474 let g = PanelGeometry::from_column(&b).expect("角形は対象");
475 let db = 500.0;
476 assert!((g.effective_volume(db) - 2.0 * g.dc * db * g.tp).abs() < 1e-6);
477
478 let p = sec(
479 SectionShape::SteelPipe {
480 outer_dia: 400.0,
481 thick: 12.0,
482 },
483 400.0,
484 None,
485 );
486 let gp = PanelGeometry::from_column(&p).expect("円形は対象");
487 assert!((gp.dc - (400.0 - 12.0)).abs() < 1e-9);
488 assert!((gp.effective_volume(db) - 2.0 * gp.dc * db * gp.tp).abs() < 1e-6);
489 assert!((gp.kappa() - 4.0 / std::f64::consts::PI).abs() < 1e-12);
490 }
491
492 #[test]
495 fn test_panel_thickness_overrides_shape() {
496 let s = sec(
497 SectionShape::SteelH {
498 height: 400.0,
499 width: 400.0,
500 web_thick: 13.0,
501 flange_thick: 21.0,
502 },
503 400.0,
504 Some(25.0),
505 );
506 let g = PanelGeometry::from_column(&s).expect("H 形は対象");
507 assert!((g.tp - 25.0).abs() < 1e-9, "明示入力が優先される: {}", g.tp);
508
509 let z = sec(
511 SectionShape::SteelH {
512 height: 400.0,
513 width: 400.0,
514 web_thick: 13.0,
515 flange_thick: 21.0,
516 },
517 400.0,
518 Some(0.0),
519 );
520 let gz = PanelGeometry::from_column(&z).expect("H 形は対象");
521 assert!((gz.tp - 13.0).abs() < 1e-9);
522 }
523
524 #[test]
527 fn test_cft_resolves_but_is_not_modeling_target() {
528 let cases = [
529 (
530 SectionShape::CftBox {
531 height: 400.0,
532 width: 400.0,
533 thick: 16.0,
534 },
535 PanelShapeKind::Box { bc: 400.0 },
536 ),
537 (
538 SectionShape::CftPipe {
539 outer_dia: 400.0,
540 thick: 12.0,
541 },
542 PanelShapeKind::Pipe,
543 ),
544 ];
545 for (shape, kind) in cases {
546 let s = sec(shape, 400.0, None);
547 let g = PanelGeometry::from_column(&s).expect("CFT も諸元は解決できる");
548 assert_eq!(g.kind, kind);
549 assert!(g.filled, "CFT は充填断面");
550 assert!(g.filled, "CFT はモデル化の対象外");
551 assert!(g.effective_volume(500.0) > 0.0);
553 assert!(g.kappa() > 0.0);
554 }
555 }
556
557 #[test]
559 fn test_steel_sections_are_modeling_targets() {
560 let shapes = [
561 SectionShape::SteelH {
562 height: 400.0,
563 width: 400.0,
564 web_thick: 13.0,
565 flange_thick: 21.0,
566 },
567 SectionShape::SteelBox {
568 height: 400.0,
569 width: 400.0,
570 thick: 16.0,
571 corner_r: 0.0,
572 },
573 SectionShape::SteelPipe {
574 outer_dia: 400.0,
575 thick: 12.0,
576 },
577 ];
578 for shape in shapes {
579 let s = sec(shape, 400.0, None);
580 let g = PanelGeometry::from_column(&s).expect("S 造は対象");
581 assert!(!g.filled);
582 assert!(!g.filled);
583 }
584 }
585
586 #[test]
589 fn test_cft_and_steel_tube_share_check_properties() {
590 let steel = sec(
591 SectionShape::SteelBox {
592 height: 400.0,
593 width: 400.0,
594 thick: 16.0,
595 corner_r: 0.0,
596 },
597 400.0,
598 None,
599 );
600 let cft = sec(
601 SectionShape::CftBox {
602 height: 400.0,
603 width: 400.0,
604 thick: 16.0,
605 },
606 400.0,
607 None,
608 );
609 let (gs, gc) = (
610 PanelGeometry::from_column(&steel).expect("角形"),
611 PanelGeometry::from_column(&cft).expect("CFT 角形"),
612 );
613 assert!((gs.dc - gc.dc).abs() < 1e-12);
614 assert!((gs.tp - gc.tp).abs() < 1e-12);
615 assert!((gs.kappa() - gc.kappa()).abs() < 1e-12);
616 assert!((gs.effective_volume(500.0) - gc.effective_volume(500.0)).abs() < 1e-9);
617 }
618
619 #[test]
621 fn test_rc_column_is_not_panel_target() {
622 let s = sec(rc_rect_shape(700.0, 700.0), 700.0, None);
623 assert!(PanelGeometry::from_column(&s).is_none());
624 }
625
626 fn node(id: u32, coord: [f64; 3]) -> crate::model::Node {
629 crate::model::Node {
630 id: NodeId(id),
631 coord,
632 restraint: crate::dof::Dof6Mask::FREE,
633 mass: None,
634 story: None,
635 support_spring: None,
636 }
637 }
638
639 fn member(id: u32, n0: u32, n1: u32, sec: u32) -> ElementData {
640 ElementData {
641 id: ElemId(id),
642 kind: ElementKind::Beam,
643 nodes: smallvec::smallvec![NodeId(n0), NodeId(n1)],
644 section: Some(crate::ids::SectionId(sec)),
645 local_axis: crate::model::LocalAxis {
646 ref_vector: [0.0, 1.0, 0.0],
647 },
648 end_cond: [
649 crate::model::EndCondition::Fixed,
650 crate::model::EndCondition::Fixed,
651 ],
652 force_regime: crate::model::ForceRegime::Auto,
653 rigid_zone: Default::default(),
654 plastic_zone: None,
655 spring: None,
656 }
657 }
658
659 fn h_col() -> SectionShape {
660 SectionShape::SteelH {
661 height: 400.0,
662 width: 400.0,
663 web_thick: 13.0,
664 flange_thick: 21.0,
665 }
666 }
667
668 fn h_beam() -> SectionShape {
669 SectionShape::SteelH {
670 height: 600.0,
671 width: 200.0,
672 web_thick: 11.0,
673 flange_thick: 17.0,
674 }
675 }
676
677 fn rc_rect_shape(b: f64, d: f64) -> SectionShape {
679 let bars = BarSet {
680 dia: 25.0,
681 count: 4,
682 layers: 1,
683 };
684 SectionShape::RcRect {
685 b,
686 d,
687 rebar: RcRebar {
688 main_x: bars.clone(),
689 main_y: bars,
690 cover: 40.0,
691 shear: ShearBar {
692 dia: 10.0,
693 pitch: 100.0,
694 legs: 2,
695 },
696 },
697 }
698 }
699
700 fn mat(id: u32, category: MaterialCategory) -> crate::model::Material {
701 crate::model::Material {
702 strength_factor: None,
703 concrete_class: Default::default(),
704 id: crate::ids::MaterialId(id),
705 name: String::new(),
706 category,
707 young: 205_000.0,
708 poisson: 0.3,
709 density: 0.0,
710 shear: None,
711 fc: None,
712 fy: None,
713 }
714 }
715
716 fn joint_model(beam: SectionShape, beam_depth: f64, col: SectionShape) -> Model {
719 joint_model_with_mats(beam, beam_depth, col, 0, 0)
720 }
721
722 fn joint_model_with_mats(
724 beam: SectionShape,
725 beam_depth: f64,
726 col: SectionShape,
727 beam_mat: u32,
728 col_mat: u32,
729 ) -> Model {
730 Model {
731 nodes: vec![
732 node(0, [0.0, 0.0, 3000.0]),
733 node(1, [6000.0, 0.0, 3000.0]),
734 node(2, [0.0, 0.0, 0.0]),
735 ],
736 sections: vec![
738 sec_with_mat(beam, beam_depth, None, 0, beam_mat),
739 sec_with_mat(col, 400.0, None, 1, col_mat),
740 ],
741 materials: vec![
742 mat(0, MaterialCategory::Steel),
743 mat(1, MaterialCategory::Concrete),
744 ],
745 elements: vec![member(0, 0, 1, 0), member(1, 2, 0, 1)],
746 ..Default::default()
747 }
748 }
749
750 #[test]
752 fn test_all_steel_joint_is_target() {
753 let m = joint_model(h_beam(), 600.0, h_col());
754 let joint = resolve_panel_joint(&m, NodeId(0), &m.elements).expect("S 造接合部");
755 assert!((joint.db - (600.0 - 17.0)).abs() < 1e-9);
756 assert_eq!(joint.column, ElemId(1));
757 assert!(!joint.has_filled_column);
758 }
759
760 #[test]
763 fn test_rc_beam_disqualifies_joint() {
764 let m = joint_model_with_mats(rc_rect_shape(400.0, 700.0), 700.0, h_col(), 1, 0);
765 assert!(resolve_panel_joint(&m, NodeId(0), &m.elements).is_none());
766 }
767
768 #[test]
770 fn test_rc_column_disqualifies_joint() {
771 let m = joint_model_with_mats(h_beam(), 600.0, rc_rect_shape(400.0, 700.0), 0, 1);
772 assert!(resolve_panel_joint(&m, NodeId(0), &m.elements).is_none());
773 }
774
775 #[test]
778 fn test_steel_shape_with_concrete_material_is_excluded() {
779 let m = joint_model_with_mats(h_beam(), 600.0, h_col(), 1, 0);
780 assert!(
781 resolve_panel_joint(&m, NodeId(0), &m.elements).is_none(),
782 "H 形でも材料がコンクリートなら S 造の接合部ではない"
783 );
784 }
785
786 #[test]
788 fn test_column_or_beam_only_node_is_not_target() {
789 let mut m = joint_model(h_beam(), 600.0, h_col());
790 let beam_only = {
791 let mut mm = m.clone();
792 mm.elements.retain(|e| e.id != ElemId(1));
793 mm
794 };
795 assert!(resolve_panel_joint(&beam_only, NodeId(0), &beam_only.elements).is_none());
796 m.elements.retain(|e| e.id != ElemId(0));
797 assert!(resolve_panel_joint(&m, NodeId(0), &m.elements).is_none());
798 }
799
800 #[test]
802 fn test_smallest_ve_column_is_selected() {
803 let thin = SectionShape::SteelH {
804 height: 400.0,
805 width: 400.0,
806 web_thick: 9.0,
807 flange_thick: 21.0,
808 };
809 let build = |upper_first: bool| {
810 let mut m = joint_model(h_beam(), 600.0, h_col());
811 m.nodes.push(node(3, [0.0, 0.0, 6000.0]));
812 m.sections.push(sec(thin.clone(), 400.0, None));
813 let upper = member(2, 0, 3, 2);
814 if upper_first {
815 m.elements.insert(0, upper);
816 } else {
817 m.elements.push(upper);
818 }
819 m
820 };
821 let a = build(true);
822 let b = build(false);
823 let ja = resolve_panel_joint(&a, NodeId(0), &a.elements).expect("接合部");
824 let jb = resolve_panel_joint(&b, NodeId(0), &b.elements).expect("接合部");
825 assert!((ja.geometry.tp - 9.0).abs() < 1e-9, "Ve 最小の柱を採る");
826 assert_eq!(ja.ve, jb.ve, "要素の並び順に依存しない");
827 assert_eq!(ja.column, jb.column);
828 }
829
830 #[test]
832 fn test_cft_column_flags_filled() {
833 let m = joint_model(
834 h_beam(),
835 600.0,
836 SectionShape::CftBox {
837 height: 400.0,
838 width: 400.0,
839 thick: 16.0,
840 },
841 );
842 let joint = resolve_panel_joint(&m, NodeId(0), &m.elements).expect("検定の対象にはなる");
843 assert!(joint.has_filled_column, "モデル化からは除外する");
844 }
845
846 #[test]
850 fn test_panel_half_extent_uses_member_depths() {
851 let mut m = joint_model(h_beam(), 600.0, h_col());
852 for e in &mut m.elements {
853 e.rigid_zone.face_i = Some(9999.0);
854 e.rigid_zone.face_j = Some(9999.0);
855 }
856 let extent = panel_half_extent(&m, NodeId(0), &m.elements);
857 assert!((extent.column_half - 200.0).abs() < 1e-9);
858 assert!((extent.beam_half - 300.0).abs() < 1e-9);
859 assert!((extent.offset_for(MemberOrientation::Beam) - 200.0).abs() < 1e-9);
860 assert!((extent.offset_for(MemberOrientation::Column) - 300.0).abs() < 1e-9);
861 }
862
863 #[test]
865 fn test_diagonal_member_has_no_orientation() {
866 let mut m = joint_model(h_beam(), 600.0, h_col());
867 m.nodes[1].coord = [4000.0, 0.0, 6000.0];
868 assert!(member_orientation(&m, &m.elements[0]).is_none());
869 }
870
871 #[test]
873 fn test_beam_panel_depth() {
874 let h = sec(
875 SectionShape::SteelH {
876 height: 600.0,
877 width: 200.0,
878 web_thick: 11.0,
879 flange_thick: 17.0,
880 },
881 600.0,
882 None,
883 );
884 assert!((beam_panel_depth(&h) - (600.0 - 17.0)).abs() < 1e-9);
885
886 let b = sec(
887 SectionShape::SteelBox {
888 height: 500.0,
889 width: 300.0,
890 thick: 12.0,
891 corner_r: 0.0,
892 },
893 500.0,
894 None,
895 );
896 assert!((beam_panel_depth(&b) - 0.9 * 500.0).abs() < 1e-9);
897 }
898}